The module lifecycle stage: General Availability
The module has requirements for installation
This section contains usage examples for the secrets-store-integration module.
CLI tool d8 for Stronghold commands
Deckhouse CLI (d8) is a universal tool required to run commands such as d8 stronghold in the terminal.
To install d8, use one of the methods described in the CLI tool documentation.
Configuring the module to work with Deckhouse Stronghold
-
Enable the
strongholdmodule by following the instructions. -
To enable the
secrets-store-integrationmodule, apply the following resource:apiVersion: deckhouse.io/v1alpha1 kind: ModuleConfig metadata: name: secrets-store-integration spec: enabled: true version: 1You do not have to set the
connectionConfigurationparameter, becauseDiscoverLocalStrongholdis used by default.
Configuring the module to work with an external store
The module requires a preconfigured secret store compatible with HashiCorp Vault. An authentication path must already be configured in the store. An example of store configuration is shown below.
To ensure that each API request is encrypted, sent, and processed by the correct recipient, you need a valid public Certificate Authority certificate used by the secret store. This CA public certificate in PEM format must be used as the caCert variable in the module configuration.
Example module configuration for using a Vault-compatible secret store running at secretstoreexample.com on the default TLS port (443):
apiVersion: deckhouse.io/v1alpha1
kind: ModuleConfig
metadata:
name: secrets-store-integration
spec:
version: 1
enabled: true
settings:
connection:
url: "https://secretstoreexample.com"
authPath: "main-kube"
caCert: |
-----BEGIN CERTIFICATE-----
MIIFoTCCA4mgAwIBAgIUX9kFz7OxlBlALMEj8WsegZloXTowDQYJKoZIhvcNAQEL
................................................................
WoR9b11eYfyrnKCYoSqBoi2dwkCkV1a0GN9vStwiBnKnAmV3B8B5yMnSjmp+42gt
o2SYzqM=
-----END CERTIFICATE-----
connectionConfiguration: ManualSetting caCert is recommended. If it is not set, the module uses the system ca-certificates bundle.
Preparing a test environment
To run the commands below, you need the Stronghold address and a token with root privileges.
You can get such a token when initializing a new secret store.
The examples below assume these settings are defined in environment variables:
export VAULT_TOKEN=xxxxxxxxxxx
export VAULT_ADDR=https://secretstoreexample.comThis section contains two variants of example commands:
- Commands using the
d8CLI tool - Commands using
curlto make direct requests to the secret store API
Before injecting secrets, prepare a test environment.
-
Create a
kv2secret in Stronghold atdemo-kv/myapp-secretand put theDB_USERandDB_PASSvalues there.-
Enable and create the Key-Value store:
d8 stronghold secrets enable -path=demo-kv -version=2 kvAlternative using
curl:curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ --request POST \ --data '{"type":"kv","options":{"version":"2"}}' \ ${VAULT_ADDR}/v1/sys/mounts/demo-kv -
Set the database username and password as the secret value:
d8 stronghold kv put demo-kv/myapp-secret DB_USER="username" DB_PASS="secret-password"Alternative using
curl:curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ --request PUT \ --data '{"data":{"DB_USER":"username","DB_PASS":"secret-password"}}' \ ${VAULT_ADDR}/v1/demo-kv/data/myapp-secret -
Verify the stored secret:
d8 stronghold kv get demo-kv/myapp-secretAlternative verification using
curl:curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ ${VAULT_ADDR}/v1/demo-kv/data/myapp-secret
-
-
If necessary, add an authentication path (
authPath) for authentication and authorization in Stronghold using the Kubernetes API of a remote cluster.-
By default, Stronghold enables and configures the Kubernetes authentication method under the name
kubernetes_localfor the cluster where Stronghold itself is running. If you need to configure access through remote clusters, set the authentication path (authPath) and enable authentication and authorization in Stronghold through the Kubernetes API for each cluster:d8 stronghold auth enable -path=remote-kube-1 kubernetesAlternative using
curl:curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ --request POST \ --data '{"type":"kubernetes"}' \ ${VAULT_ADDR}/v1/sys/auth/remote-kube-1 -
Set the Kubernetes API address for each cluster:
d8 stronghold write auth/remote-kube-1/config \ kubernetes_host="https://api.kube.my-deckhouse.com"Alternative using
curl:curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ --request PUT \ --data '{"kubernetes_host":"https://api.kube.my-deckhouse.com"}' \ ${VAULT_ADDR}/v1/auth/remote-kube-1/config
-
-
Create a
myapp-ro-policypolicy in Stronghold that allows reading secrets fromdemo-kv/data/myapp-secret:d8 stronghold policy write myapp-ro-policy - <<EOF path "demo-kv/data/myapp-secret" { capabilities = ["read"] } EOFAlternative using
curl:curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ --request PUT \ --data '{"policy":"path \"demo-kv/data/myapp-secret\" {\n capabilities = [\"read\"]\n}\n"}' \ ${VAULT_ADDR}/v1/sys/policies/acl/myapp-ro-policy -
Create a role in Stronghold for the
myapp-saservice account in themyapp-namespacenamespace and bind the policy created earlier to it.In addition to the Stronghold-side configuration, you must configure authorization permissions for the ServiceAccount objects used in the Kubernetes cluster.
See the required settings in the next section.
-
Create a role consisting of the namespace and policy name. Bind it to the
myapp-saServiceAccount in themyapp-namespacenamespace and to themyapp-ro-policypolicy:The recommended TTL value for the Kubernetes token is
10m.d8 stronghold write auth/kubernetes_local/role/myapp-role \ bound_service_account_names=myapp-sa \ bound_service_account_namespaces=myapp-namespace \ policies=myapp-ro-policy \ ttl=10mAlternative using
curl:curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ --request PUT \ --data '{"bound_service_account_names":"myapp-sa","bound_service_account_namespaces":"myapp-namespace","policies":"myapp-ro-policy","ttl":"10m"}' \ ${VAULT_ADDR}/v1/auth/kubernetes_local/role/myapp-role -
Repeat the same for remote clusters, specifying a different authentication path:
d8 stronghold write auth/remote-kube-1/role/myapp-role \ bound_service_account_names=myapp-sa \ bound_service_account_namespaces=myapp-namespace \ policies=myapp-ro-policy \ ttl=10mAlternative using
curl:curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ --request PUT \ --data '{"bound_service_account_names":"myapp-sa","bound_service_account_namespaces":"myapp-namespace","policies":"myapp-ro-policy","ttl":"10m"}' \ ${VAULT_ADDR}/v1/auth/remote-kube-1/role/myapp-role
These settings allow any pod in the
myapp-namespacenamespace in both Kubernetes clusters that uses themyapp-saServiceAccount to authenticate and authorize in Stronghold to read secrets according to themyapp-ro-policypolicy. -
-
Create the
myapp-namespacenamespace in the cluster:d8 k create namespace myapp-namespace -
Create the
myapp-saservice account in that namespace:d8 k -n myapp-namespace create serviceaccount myapp-sa
How to allow a ServiceAccount to authenticate in Stronghold
To authenticate in Stronghold, a pod uses the token generated for its ServiceAccount. For Stronghold to validate the provided ServiceAccount data, the Stronghold service must have get, list, and watch permissions for the tokenreviews.authentication.k8s.io and subjectaccessreviews.authorization.k8s.io endpoints. You can also use the system:auth-delegator ClusterRole for this.
Stronghold can use different credentials to send requests to the Kubernetes API:
- A token of the application that is trying to authenticate in Stronghold. In this case, every service authenticating in Stronghold requires the
system:auth-delegatorClusterRole or the API permissions listed above on the ServiceAccount it uses. See the example in the Stronghold documentation. - A static token of a ServiceAccount created specifically for Stronghold and granted the necessary permissions. Configuring Stronghold for this case is described in detail in the Stronghold documentation.
Injecting environment variables
How injection works
When the module is enabled, a mutating-webhook appears in the cluster. If a pod has the secrets-store.deckhouse.io/role annotation, the webhook modifies the pod manifest by adding the injector.
In the modified pod:
- An init container is added.
- The init container copies a statically linked injector binary from the service image into a temporary directory shared by all containers in the pod.
- In the remaining containers, the original startup commands are replaced with a command that launches the injector binary.
- The injector retrieves the required data from a Vault-compatible store using the application’s service account.
- It puts these variables into the process
ENV. - It performs the
execvesystem call and starts the original command.
If a container does not define a startup command in the pod manifest, the image manifest is fetched from the registry and the command is taken from it.
Credentials from imagePullSecrets specified in the pod manifest are used to retrieve the manifest from a private image registry.
Injector annotations
The following annotations are available to modify injector behavior:
| Annotation | Default value | Description |
|---|---|---|
secrets-store.deckhouse.io/addr |
From module | Secret store address in the format https://stronghold.mycompany.tld:8200 |
secrets-store.deckhouse.io/tls-secret |
From module | Name of the Secret object in Kubernetes containing the ca.crt key with the CA certificate value in PEM format |
secrets-store.deckhouse.io/tls-skip-verify |
false |
Disables verification of the server TLS certificate |
secrets-store.deckhouse.io/auth-path |
From module | Path to use for authentication |
secrets-store.deckhouse.io/namespace |
From module | Namespace that will be used to connect to the store |
secrets-store.deckhouse.io/role |
Role used to connect to the secret store | |
secrets-store.deckhouse.io/env-from-path |
Comma-separated list of secret paths in the store from which all keys will be extracted and placed into the environment. Keys from paths closer to the end of the list take precedence | |
secrets-store.deckhouse.io/ignore-missing-secrets |
false |
Starts the original application if retrieving a secret from the store fails |
secrets-store.deckhouse.io/client-timeout |
10s |
Timeout for secret retrieval |
secrets-store.deckhouse.io/mutate-probes |
false |
Injects environment variables into probes |
secrets-store.deckhouse.io/log-level |
info |
Logging level |
secrets-store.deckhouse.io/enable-json-log |
false |
Enables JSON log output |
secrets-store.deckhouse.io/skip-mutate-containers |
Space-separated list of container names that will not be mutated |
Using the injector, you can specify templates in pod manifests instead of actual env values. They are replaced with values from the store at container startup time.
Importing variables from a store path has higher priority than explicitly defined variables from the store. This means that if you use the secrets-store.deckhouse.io/env-from-path annotation with a path to a secret containing, for example, the MY_SECRET key, and also define an environment variable with the same name in the manifest:
env:
- name: MY_SECRET
value: secrets-store:demo-kv/data/myapp-secret#passwordthe MY_SECRET environment variable inside the container will be set to the secret value from the annotation.
Example of retrieving the DB_PASS key from a kv2 secret at demo-kv/myapp-secret from a Vault-compatible store:
env:
- name: PASSWORD
value: secrets-store:demo-kv/data/myapp-secret#DB_PASSExample of retrieving version 4 of the DB_PASS key from a kv2 secret at demo-kv/myapp-secret:
env:
- name: PASSWORD
value: secrets-store:demo-kv/data/myapp-secret#DB_PASS#4The template can also be stored in a ConfigMap or a Secret and connected using envFrom:
envFrom:
- secretRef:
name: app-secret-env
- configMapRef:
name: app-envActual secrets from the Vault-compatible store are injected only at application startup. The Secret and ConfigMap objects contain templates.
Importing variables from a store path
In this scenario, all keys from a single secret are imported.
-
Create a pod named
myapp1that imports all variables from the store atdemo-kv/data/myapp-secret:kind: Pod apiVersion: v1 metadata: name: myapp1 namespace: myapp-namespace annotations: secrets-store.deckhouse.io/role: "myapp-role" secrets-store.deckhouse.io/env-from-path: demo-kv/data/common-secret,demo-kv/data/myapp-secret spec: serviceAccountName: myapp-sa containers: - image: alpine:3.20 name: myapp command: - sh - -c - while printenv; do sleep 5; done -
Apply the manifest:
d8 k create --filename myapp1.yaml -
Check the pod logs after startup. The output should include all variables from
demo-kv/data/myapp-secret:d8 k -n myapp-namespace logs myapp1 -
Delete the pod:
d8 k -n myapp-namespace delete pod myapp1 --force
Importing explicitly defined variables from the store
-
Create a test pod named
myapp2that imports the required variables from the store using templates:kind: Pod apiVersion: v1 metadata: name: myapp2 namespace: myapp-namespace annotations: secrets-store.deckhouse.io/role: "myapp-role" spec: serviceAccountName: myapp-sa containers: - image: alpine:3.20 env: - name: DB_USER value: secrets-store:demo-kv/data/myapp-secret#DB_USER - name: DB_PASS value: secrets-store:demo-kv/data/myapp-secret#DB_PASS name: myapp command: - sh - -c - while printenv; do sleep 5; done -
Apply the configuration:
d8 k create --filename myapp2.yaml -
Check the pod logs after startup. The output should include variables from
demo-kv/data/myapp-secret:d8 k -n myapp-namespace logs myapp2 -
Delete the pod:
d8 k -n myapp-namespace delete pod myapp2 --force
Mounting a secret from the store as a file into a container
Use the SecretsStoreImport custom resource to deliver secrets to the application.
This example uses the myapp-sa service account and the myapp-namespace namespace created during test environment preparation.
-
Create a SecretsStoreImport custom resource named
myapp-ssiin the cluster:apiVersion: deckhouse.io/v1alpha1 kind: SecretsStoreImport metadata: name: myapp-ssi namespace: myapp-namespace spec: type: CSI role: myapp-role files: - name: "db-password" source: path: "demo-kv/data/myapp-secret" key: "DB_PASS" -
Create a test pod named
myapp3in the cluster that mounts the secret from the store as a file:kind: Pod apiVersion: v1 metadata: name: myapp3 namespace: myapp-namespace spec: serviceAccountName: myapp-sa containers: - image: alpine:3.20 name: backend command: - sh - -c - while cat /mnt/secrets/db-password; do echo; sleep 5; done volumeMounts: - name: secrets mountPath: "/mnt/secrets" volumes: - name: secrets csi: driver: secrets-store.csi.deckhouse.io volumeAttributes: secretsStoreImport: "myapp-ssi"After these resources are applied, a pod is created with a
backendcontainer. Inside the container filesystem, the/mnt/secretsdirectory contains the mountedsecretsvolume. This directory contains thedb-passwordfile with the database password (DB_PASS) from the Stronghold Key-Value store. -
Check the pod logs after startup. The output should contain the contents of
/mnt/secrets/db-password:d8 k -n myapp-namespace logs myapp3 -
Delete the pod:
d8 k -n myapp-namespace delete pod myapp3 --force
Delivering binary files into a container
In some cases, you may need to deliver a binary file into a container, for example:
- A JKS keystore
- A
keytabfor Kerberos authentication
In this case, you can encode the binary file as Base64 and place it into the secret store. When retrieved, the CSI driver decodes the data and places the binary file into the container. To do this, set decodeBase64 to true for the corresponding file.
If decoding fails, for example because the store contains invalid Base64 data, the container will not be created.
Example:
-
Encode the file as Base64 and place it into the store:
d8 stronghold kv put demo-kv/myapp-secret keytab=$(cat /path/to/keytab_file | base64 -w0) -
Create a SecretsStoreImport manifest with the decoding parameter set:
apiVersion: deckhouse.io/v1alpha1 kind: SecretsStoreImport metadata: name: myapp-ssi namespace: myapp-namespace spec: type: CSI role: myapp-role files: - name: "keytab" decodeBase64: true source: path: "demo-kv/data/myapp-secret" key: "keytab" -
A binary file named
keytabwill be created in the container.
Autorotation feature
The autorotation feature in the secrets-store-integration module is enabled by default. Every two minutes, the module polls Stronghold and synchronizes secrets in the mounted file if they have changed.
There are two ways to track changes to the secret file in a pod:
- watch the modification time of the mounted file and react when it changes;
- use the
inotifyAPI, which provides a file system event subscription mechanism.
Inotify is part of the Linux kernel. Once a change is detected, there are many possible responses depending on the application architecture and programming language. The simplest option is to make Kubernetes restart the pod by failing the livenessProbe.
Example of using inotify in a Python application:
#!/usr/bin/python3
import inotify.adapters
def _main():
i = inotify.adapters.Inotify()
i.add_watch('/mnt/secrets-store/db-password')
for event in i.event_gen(yield_nones=False):
(_, type_names, path, filename) = event
if 'IN_MODIFY' in type_names:
print("file modified")
if __name__ == '__main__':
_main()Example of using inotify in a Go application:
watcher, err := inotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
err = watcher.Watch("/mnt/secrets-store/db-password")
if err != nil {
log.Fatal(err)
}
for {
select {
case ev := <-watcher.Event:
if ev == 'InModify' {
log.Println("file modified")
}
case err := <-watcher.Error:
log.Println("error:", err)
}
}Limitations when updating secrets
Files with secrets are not updated if subPath is used.
volumeMounts:
- mountPath: /app/settings.ini
name: app-config
subPath: settings.ini
...
volumes:
- name: app-config
csi:
driver: secrets-store.csi.deckhouse.io
volumeAttributes:
secretsStoreImport: "python-backend"