> For the complete documentation index, see [llms.txt](https://newdocs.keeper.io/en/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://newdocs.keeper.io/en/keeperpam/secrets-manager/integrations/kubernetes-secrets-injector.md).

# Kubernetes Secrets Injector

### Overview

The **Keeper Injector** automatically injects secrets from Keeper Secrets Manager (KSM) into your Kubernetes pods at runtime. It is a **mutating admission webhook** that adds a lightweight **sidecar** to annotated pods; the sidecar authenticates to Keeper, fetches the requested secrets, and makes them available to your application - by default on a memory-backed (`tmpfs`) volume that never touches `etcd` or disk.

The injector is configured entirely with **pod annotations** - there are no Custom Resources to manage.

{% hint style="info" %}
The injector is distributed as an official Helm chart, published to [ArtifactHub](https://artifacthub.io/packages/helm/keeper-security/keeper-injector). It connects only to Keeper Secrets Manager; if you need a multi-backend operator, see the External Secrets Operator.
{% endhint %}

### How it works

When a pod carries the `keeper.security/inject: "true"` annotation, the webhook mutates it to add:

* an **init container** that fetches the secrets once before your app starts, and
* a **sidecar container** that refreshes them on an interval (unless you opt into init-only).

The sidecar can deliver secrets three ways - you choose per pod with annotations:

<table data-header-hidden><thead><tr><th width="212.83984375"></th><th width="361.171875"></th><th></th></tr></thead><tbody><tr><td>Mode</td><td>How the app consumes it</td><td>Secrets in <code>etcd</code>?</td></tr><tr><td><strong>Files</strong> (default)</td><td>Read files under <code>/keeper/secrets/</code> from a memory-backed <code>tmpfs</code> volume</td><td>No</td></tr><tr><td><strong>Environment variables</strong></td><td>Standard env vars in your container</td><td>Yes (in the pod spec)</td></tr><tr><td><strong>Kubernetes Secret</strong></td><td>A native <code>Secret</code> object via <code>secretKeyRef</code> or a volume</td><td>Yes</td></tr></tbody></table>

File injection is the most secure default (memory-only, per-pod). Use environment variables or a Kubernetes `Secret` when an application or controller specifically requires them.

### Keeper Injector vs. External Secrets Operator

We offer two methods of injecting secrets from Keeper Secrets Manager into Kubernetes - using this Keeper Injector and using the [External Secrets Operator](/en/keeperpam/secrets-manager/integrations/kubernetes-external-secrets-operator.md). The implementation and use case is different in the following ways:

|                                     | **Keeper Injector**             | **External Secrets Operator** |
| ----------------------------------- | ------------------------------- | ----------------------------- |
| Creates Kubernetes `Secret` objects | Optional (default: no)          | Yes (core model)              |
| Where secrets live                  | Pod `tmpfs` (memory) by default | `etcd`                        |
| Configuration                       | Pod annotations                 | Custom Resources (CRDs)       |
| Runtime rotation                    | Yes (sidecar)                   | On sync interval              |
| Per-pod isolation                   | Yes                             | Shared `Secret` objects       |
| Backends                            | Keeper only                     | 35+ providers                 |

**Choose the Keeper Injector** when you want secrets to stay **out of `etcd`** (delivered to pod memory), with **per-pod isolation** and **in-place rotation without pod restarts**, configured with a couple of pod annotations and no custom resources.

**Choose the External Secrets Operator** when it fits *how your cluster already works* - for example, you **already run ESO** as your standard secrets operator (often across multiple backends) and want Keeper to plug into that same CRD workflow; you want secrets reconciled into **long-lived, shared `Secret` objects** consumed by other controllers or GitOps tooling; or you need ESO-specific capabilities such as write-back (`PushSecret`). ESO ships a maintained first-class Keeper provider.

For the legacy init-container approach, see [Kubernetes (alternative)](/en/keeperpam/secrets-manager/integrations/kubernetes.md).

### Prerequisites

* A Kubernetes cluster, version **1.25 or later**
* [Helm 3](https://helm.sh/docs/intro/install/)
* A Keeper Secrets Manager **Application** and a **device configuration** (see Quick Start Guide)
* `kubectl` access to the target cluster

### Install the Keeper Injector

Install the webhook and sidecar controller into a dedicated namespace.

{% tabs %}
{% tab title="Helm Repository" %}

```bash
helm repo add keeper https://keeper-security.github.io/helm-charts
helm repo update

helm upgrade --install keeper-injector keeper/keeper-injector \
  --namespace keeper-security --create-namespace
```

{% endtab %}

{% tab title="OCI Registry (Docker Hub)" %}

```bash
helm upgrade --install keeper-injector oci://registry-1.docker.io/keeper/keeper-injector \
  --namespace keeper-security --create-namespace
```

{% endtab %}
{% endtabs %}

TLS certificates for the webhook are auto-generated by default (`tls.autoGenerate=true`); cert-manager is optionally supported via `tls.certManager.enabled=true`.

### Create the KSM authentication secret

The sidecar authenticates to Keeper with a Secrets Manager **device configuration**, stored in a Kubernetes Secret (key `config`) in the namespace where your workloads run.

The simplest way to get the configuration is from the **Keeper Web Vault**: **Secrets Manager → your Application → add a device → Configuration File method → Base64 type**, and copy the Base64 string. Then:

```bash
kubectl create secret generic keeper-credentials \
  --from-literal=config='<paste-base64-config>' \
  --namespace default
```

{% hint style="info" %}
You can also generate the configuration with the Secrets Manager CLI or Commander and load a JSON file instead (`--from-file=config=ksm-config.json`). Either form works - the Secret's key must be `config`.
{% endhint %}

{% hint style="warning" %}
The configuration is a credential. Create the Secret directly (don't commit it to source control) and restrict access to it with RBAC.
{% endhint %}

### Inject secrets

#### Files (default)

Add the injection annotations to the pod template. The injector adds the sidecar and a shared `tmpfs` volume, and writes each requested record as a file under `/keeper/secrets/`.

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-app
  annotations:
    keeper.security/inject: "true"
    keeper.security/ksm-config: "keeper-credentials"
    keeper.security/secret: "database-credentials"
spec:
  containers:
    - name: app
      image: my-app:latest
```

The record is written to `/keeper/secrets/database-credentials.json`, where your application reads it:

```bash
cat /keeper/secrets/database-credentials.json
# {"login":"app_user","password":"…","host":"db.internal"}
```

{% hint style="info" %}
Injected files are written **read-only (`0440`)** and owned by the sidecar's user. Containers that run as **root** or as the **`nobody`** user read them with no extra configuration; if your application runs as a different fixed non-root UID, set a matching pod-level `securityContext.fsGroup` so the group-readable files are accessible.
{% endhint %}

#### Environment variables

Set `keeper.security/inject-env-vars: "true"` to inject the record's fields as environment variables instead of (or in addition to) files. Field names become uppercased env keys; add a prefix with `keeper.security/env-prefix`.

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: legacy-app
  annotations:
    keeper.security/inject: "true"
    keeper.security/ksm-config: "keeper-credentials"
    keeper.security/inject-env-vars: "true"
    keeper.security/env-prefix: "APP_"
    keeper.security/secret: "database-credentials"
    keeper.security/init-only: "true"   # env vars can't change without a restart
spec:
  containers:
    - name: app
      image: legacy-app:latest
```

The `login` and `password` fields become `APP_LOGIN` and `APP_PASSWORD`.

{% hint style="warning" %}
Environment variables are written into the pod spec, so they are visible in `kubectl get pod -o yaml` and stored in `etcd`. Prefer file injection for sensitive values. Environment variables can't be rotated without restarting the pod, so pair this mode with `keeper.security/init-only: "true"`.
{% endhint %}

#### Kubernetes Secret

Set `keeper.security/inject-as-k8s-secret: "true"` to have the injector create a native Kubernetes `Secret` from the record, which your workloads consume the standard way.

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: app-with-secret
  annotations:
    keeper.security/inject: "true"
    keeper.security/ksm-config: "keeper-credentials"
    keeper.security/inject-as-k8s-secret: "true"
    keeper.security/k8s-secret-name: "app-secrets"
    keeper.security/secret: "database-credentials"
spec:
  containers:
    - name: app
      image: my-app:latest
      env:
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: password
```

Each record field becomes a key in the `Secret`. Use `keeper.security/k8s-secret-mode` to control conflicts (`overwrite`, `merge`, `skip-if-exists`, `fail`) and `keeper.security/k8s-secret-type` to set the type (for example `kubernetes.io/tls`).

{% hint style="info" %}
The created `Secret` is written to the **pod's own namespace** and is **not** automatically deleted when the pod is removed - delete it explicitly or remove the namespace when you no longer need it.
{% endhint %}

### Output formats and templates

For anything beyond the default JSON file, use the `keeper.security/config` annotation - a small YAML document that describes one or more secrets, each with its own output `path`, `format`, or Go `template`.

```yaml
metadata:
  annotations:
    keeper.security/inject: "true"
    keeper.security/ksm-config: "keeper-credentials"
    keeper.security/config: |
      secrets:
        # Render a single field, raw
        - record: database-credentials
          path: /keeper/secrets/password.txt
          fields: [password]
          format: raw

        # Render as a .env file
        - record: database-credentials
          path: /keeper/secrets/db.env
          format: env

        # Build a connection string with a Go template
        - record: database-credentials
          path: /keeper/secrets/db-url.txt
          template: |
            postgresql://{{ .login }}:{{ .password }}@{{ .host }}:5432/app
```

Supported `format` values: `json` (default), `env`, `yaml`, `properties`, `ini`, and `raw`. Templates use Go `text/template` with the [Sprig](https://masterminds.github.io/sprig/) function library (string manipulation, base64/hex, hashing, conditionals, and more), with the record's fields available as top-level variables (`.login`, `.password`, …).

### Multiple secrets, folders, and Keeper Notation

Inject several records, a whole folder, or a single field with Keeper Notation:

```yaml
metadata:
  annotations:
    keeper.security/inject: "true"
    keeper.security/ksm-config: "keeper-credentials"

    # Several records by title (comma-separated)
    keeper.security/secrets: "database-credentials, api-keys"

    # A whole folder → one file per record under the output path
    keeper.security/folder: "Production/Databases"
    keeper.security/folder-path: "/keeper/secrets/db"

    # One field, by Keeper Notation, to a chosen path
    keeper.security/secret-token: "keeper://Rh1k…UID/field/password:/keeper/secrets/token.txt"
```

{% hint style="info" %}
Keeper Notation (`keeper://…`) is parsed by the **per-secret** `keeper.security/secret-<name>` annotation and inside the `keeper.security/config` block — not by the plain `keeper.security/secret` annotation, which always treats its value as a record **title**. A folder can be referenced by path (`keeper.security/folder`) or by UID (`keeper.security/folder-uid`).
{% endhint %}

### File attachments

Download a file attached to a Keeper record with `keeper.security/file-<name>`, in the form `record:filename:/output/path`:

```yaml
metadata:
  annotations:
    keeper.security/inject: "true"
    keeper.security/ksm-config: "keeper-credentials"
    keeper.security/file-cert: "tls-record:server.crt:/keeper/secrets/tls/server.crt"
    keeper.security/file-key: "tls-record:server.key:/keeper/secrets/tls/server.key"
```

### Rotation and refresh

In sidecar mode the agent re-fetches secrets on an interval and rewrites the files **in place** - no pod restart is required, and applications that re-read the files pick up the new values automatically.

```yaml
metadata:
  annotations:
    keeper.security/inject: "true"
    keeper.security/ksm-config: "keeper-credentials"
    keeper.security/secret: "database-credentials"
    keeper.security/refresh-interval: "5m"   # default is 5m
```

Set `keeper.security/init-only: "true"` to fetch once at startup with no refresh sidecar - appropriate for environment-variable injection, or when you don't need rotation.

### Corporate proxy / custom CA

If outbound traffic is intercepted by an SSL-inspecting proxy (Zscaler, Palo Alto, Cisco Umbrella, …), provide the proxy's CA certificate so the sidecar trusts it. Store the CA in a Secret or ConfigMap and reference it:

```yaml
metadata:
  annotations:
    keeper.security/inject: "true"
    keeper.security/ksm-config: "keeper-credentials"
    keeper.security/secret: "database-credentials"
    keeper.security/ca-cert-configmap: "corporate-ca"   # or keeper.security/ca-cert-secret
    keeper.security/ca-cert-key: "ca.crt"               # key within the ConfigMap/Secret (default: ca.crt)
```

### Cloud authentication (optional)

Instead of a Kubernetes Secret, the sidecar can fetch the KSM configuration from a cloud secret store at runtime. Set `keeper.security/auth-method` and the provider's locator; **omit** `keeper.security/ksm-config`.

| Provider            | Annotations                                                                                                                                    |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| AWS Secrets Manager | `keeper.security/auth-method: "aws-secrets-manager"`, `keeper.security/aws-secret-id: "<id/arn>"`, `keeper.security/aws-region: "<region>"`    |
| GCP Secret Manager  | `keeper.security/auth-method: "gcp-secret-manager"`, `keeper.security/gcp-secret-id: "<resource>"`                                             |
| Azure Key Vault     | `keeper.security/auth-method: "azure-key-vault"`, `keeper.security/azure-vault-name: "<vault>"`, `keeper.security/azure-secret-name: "<name>"` |

{% hint style="info" %}
Cloud authentication pairs with **file-based** injection. It uses the cloud provider's own workload identity (IRSA, Workload Identity, Azure Workload Identity) to read the stored KSM config.
{% endhint %}

### Annotation reference

<table><thead><tr><th>Annotation</th><th width="315.54296875">Description</th><th>Example</th></tr></thead><tbody><tr><td><code>keeper.security/inject</code></td><td>Enable injection for this pod</td><td><code>"true"</code></td></tr><tr><td><code>keeper.security/ksm-config</code></td><td>Kubernetes Secret holding the KSM config (key <code>config</code>)</td><td><code>"keeper-credentials"</code></td></tr><tr><td><code>keeper.security/secret</code></td><td>A single record to inject, by <strong>title</strong></td><td><code>"my-secret"</code></td></tr><tr><td><code>keeper.security/secrets</code></td><td>Multiple records, comma-separated (by title)</td><td><code>"db-creds, api-keys"</code></td></tr><tr><td><code>keeper.security/secret-&#x3C;name></code></td><td>One record (by title or <code>keeper://</code> notation) to a chosen path</td><td><code>keeper.security/secret-db: "keeper://UID/field/password:/keeper/secrets/db.txt"</code></td></tr><tr><td><code>keeper.security/config</code></td><td>YAML block for formats, templates, and per-secret paths</td><td><em>(see above)</em></td></tr><tr><td><code>keeper.security/folder</code> / <code>keeper.security/folder-uid</code></td><td>Fetch every record in a folder (by path or UID)</td><td><code>"Production/Databases"</code></td></tr><tr><td><code>keeper.security/folder-path</code></td><td>Output directory for folder records</td><td><code>"/keeper/secrets/db"</code></td></tr><tr><td><code>keeper.security/file-&#x3C;name></code></td><td>Download a file attachment (<code>record:filename:/path</code>)</td><td><code>"tls:server.crt:/keeper/secrets/server.crt"</code></td></tr><tr><td><code>keeper.security/inject-env-vars</code></td><td>Inject fields as environment variables</td><td><code>"true"</code></td></tr><tr><td><code>keeper.security/env-prefix</code></td><td>Prefix for injected env var names</td><td><code>"APP_"</code></td></tr><tr><td><code>keeper.security/inject-as-k8s-secret</code></td><td>Create a native Kubernetes <code>Secret</code></td><td><code>"true"</code></td></tr><tr><td><code>keeper.security/k8s-secret-name</code></td><td>Name of the created <code>Secret</code></td><td><code>"app-secrets"</code></td></tr><tr><td><code>keeper.security/k8s-secret-mode</code></td><td>Conflict behavior: <code>overwrite</code>, <code>merge</code>, <code>skip-if-exists</code>, <code>fail</code></td><td><code>"merge"</code></td></tr><tr><td><code>keeper.security/k8s-secret-type</code></td><td><code>Secret</code> type</td><td><code>"kubernetes.io/tls"</code></td></tr><tr><td><code>keeper.security/refresh-interval</code></td><td>Sidecar re-fetch interval</td><td><code>"5m"</code></td></tr><tr><td><code>keeper.security/init-only</code></td><td>Fetch once at startup, no refresh sidecar</td><td><code>"true"</code></td></tr><tr><td><code>keeper.security/fail-on-error</code></td><td>Fail pod startup if a secret can't be fetched (default <code>"true"</code>)</td><td><code>"false"</code></td></tr><tr><td><code>keeper.security/ca-cert-secret</code> / <code>keeper.security/ca-cert-configmap</code></td><td>Custom CA for an SSL-inspecting proxy</td><td><code>"corporate-ca"</code></td></tr><tr><td><code>keeper.security/auth-method</code></td><td>Cloud auth provider (omit for K8s-Secret auth)</td><td><code>"aws-secrets-manager"</code></td></tr></tbody></table>

### Helm configuration

<table><thead><tr><th width="227.94921875">Parameter</th><th width="294.0859375">Description</th><th>Default</th></tr></thead><tbody><tr><td><code>replicaCount</code></td><td>Number of webhook replicas (HA)</td><td><code>2</code></td></tr><tr><td><code>image.repository</code> / <code>image.tag</code></td><td>Webhook image / tag</td><td><code>keeper/injector-webhook</code> / chart appVersion</td></tr><tr><td><code>sidecar.repository</code> / <code>sidecar.tag</code></td><td>Sidecar image / tag</td><td><code>keeper/injector-sidecar</code> / chart appVersion</td></tr><tr><td><code>metrics.enabled</code></td><td>Expose Prometheus metrics</td><td><code>true</code></td></tr><tr><td><code>tls.autoGenerate</code></td><td>Auto-generate the webhook's TLS certificate</td><td><code>true</code></td></tr><tr><td><code>tls.certManager.enabled</code></td><td>Use cert-manager to issue the webhook certificate</td><td><code>false</code></td></tr><tr><td><code>rbac.create</code></td><td>Create the controller's RBAC role and binding</td><td><code>true</code></td></tr></tbody></table>

See the chart's [`values.yaml`](https://github.com/Keeper-Security/helm-charts/blob/main/charts/keeper-injector/values.yaml) for the full set of options.

### Verifying the setup

Confirm the injector controller is running:

```bash
kubectl get pods -n keeper-security
```

Deploy an annotated workload, then confirm the sidecar was injected and the secret is present:

```bash
# the keeper-secrets-sidecar container should be listed
kubectl get pod my-app -o jsonpath='{.spec.containers[*].name}'

# read an injected file
kubectl exec my-app -c app -- cat /keeper/secrets/database-credentials.json
```

### Troubleshooting

| Symptom                                     | Check                                                                                                                                                              |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| No sidecar injected                         | The namespace isn't labeled `keeper.security/inject=disabled`; the controller pods in `keeper-security` are running; the pod has `keeper.security/inject: "true"`. |
| Pod won't start / secret missing            | Inspect the init container logs: `kubectl logs <pod> -c keeper-secrets-init`. Confirm the record title matches exactly and the auth Secret has a `config` key.     |
| Refresh issues at runtime                   | Inspect the sidecar logs: `kubectl logs <pod> -c keeper-secrets-sidecar`.                                                                                          |
| App can't read the file (permission denied) | The app container runs as a UID that isn't `root`/`nobody`; set a matching pod `securityContext.fsGroup`.                                                          |
| Want a soft failure                         | Set `keeper.security/fail-on-error: "false"` so the pod starts even if a secret can't be fetched.                                                                  |

### Uninstall

```bash
helm uninstall keeper-injector --namespace keeper-security
```

Removing the controller stops new injections; existing pods keep their injected sidecars until they are recreated. Files on `tmpfs` disappear with their pods, but any `Secret` objects created by the K8s-Secret mode persist - delete them explicitly.

### Example Configuration

A detailed example of using Kubernetes Secrets Injector can be found on the next page.

[Examples](/en/keeperpam/secrets-manager/integrations/kubernetes-secrets-injector/examples.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://newdocs.keeper.io/en/keeperpam/secrets-manager/integrations/kubernetes-secrets-injector.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
