In this post, I will show you how to integrate Vault with Kubernetes secrets. There are other ways of integration, such as injecting secrets as a text file into the POD, but I will now show you how to create a Secret in Kubernetes linked to the Vault Secret.
A big advantage is that nothing changes in the use of secrets on the Kubernetes/Pods/Containers side, etc., reading and obtaining secrets is the same. The way to generate it is of course different, we can also configure how often the Vault operator in K8s does the secret check in the Vault and updates the K8s secret.
The files used in this post are on our GitHub. https://github.com/faustobranco/devops-db/tree/master/vault/k8s-secrets
Of course, I am using the structures created throughout this project and described on the website.
First point, install the HELM repository for operator use.
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
Some observations from this example.
vault auth enable -path auth-kubernetes kubernetes
Success! Enabled kubernetes auth method at: auth-kubernetes/
Now, in K8s, let’s create the file with the basic operator settings. There are several other configurations, including security ones, that can be made, one of them is the Audience parameter, but there is plenty of material on this on the Hashicorp website and specialized websites.
Note that the skipTLSVerify parameter is enabled since our certificate does not have a valid CA and the vault is on https.
operator-values.yaml
defaultVaultConnection:
enabled: true
address: "https://vault.devops-db.internal:8200"
skipTLSVerify: true
Install the operator using HELM with the values from the above file.
helm install vault-secrets-operator hashicorp/vault-secrets-operator --values operator-values.yaml
NAME: vault-secrets-operator
LAST DEPLOYED: Fri Apr 11 09:57:33 2025
NAMESPACE: default
STATUS: deployed
REVISION: 1
Validate that the POD has been created.
kubectl get pods
NAME READY STATUS RESTARTS AGE
vault-secrets-operator-controller-manager-5cd7c8bbd7-cxdjt 2/2 Running 0 2m53s
To continue we will need a ServiceAccount, you can create one but I will use the same ServiceAccount created by the operator.
kubectl get serviceaccounts
NAME SECRETS AGE
default 0 49d
vault-secrets-operator-controller-manager 0 5m27s
This ServiceAccount needs a Token to authenticate in K8s, for that, we will create a Secret and generate a token.
vault-secret.yaml
kubectl apply -f vault-secret.yaml
secret/vault-token-k8s created
We now have to recover this token for the next configurations.
kubectl describe secret vault-token-k8s
Name: vault-token-k8s
Namespace: default
Labels: <none>
Annotations: kubernetes.io/service-account.name: vault-secrets-operator-controller-manager
kubernetes.io/service-account.uid: 3e92198b-4bd6-4c50-9188-7d3e38ca99f3
Type: kubernetes.io/service-account-token
Data
====
ca.crt: 1123 bytes
namespace: 7 bytes
token: eyJhbGciOiJSUzI1NiIsImtpZCI6ImZsX21QdzhLbmVEYXFzbjJpNWIwNWp4M2s4T2x0WjljX1c2WWVEWHliTUEifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InZhdWx0LXRva2VuLWs4cyIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJ2YXVsdC1zZWNyZXRzLW9wZXJhdG9yLWNvbnRyb2xsZXItbWFuYWdlciIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjNlOTIxOThiLTRiZDYtNGM1MC05MTg4LTdkM2UzOGNhOTlmMyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OnZhdWx0LXNlY3JldHMtb3BlcmF0b3ItY29udHJvbGxlci1tYW5hZ2VyIn0.qH6RI2zoGzlSLpSzHeOLk2LrtDagr810S6SxodMG9AiY8cyAyQpKe1ylvOFLPvVYzLsPJhhoivV1EstbuQAM12IWDkbpecrBCzeMEXqddCL2T7ICKr-Z-0_9KF8AzJCCrkAuDSaaX_wsiEkbTLTR5xjwnkarYsuAzhvtjgns9bl3WO-tGklLQ5YBa1UTnpqm0Cnk6HgXDYsDX62VGGuHwnO2QHYE43xv63Q5Qw3aHpLUr5ciypXsrVh9g_nrAIEyQJBwm1vPuUz2mCBoeJt-ZkR4BisBjQW0EQb4Gw1WeId1Llj3X1LAXJ12k9wviKkRNdjvuSZzH7nv6kWK2dvwDQ
Once we have the token, we have to create a configuration in the Vault. This configuration needs some other parameters from the K8s cluster like URL, certificate (if any), etc.
URL:
kubectl config view --raw --minify --flatten --output='jsonpath={.clusters[].cluster.server}'
https://172.21.5.76:16443
Certificate:
kubectl config view --raw --minify --flatten --output='jsonpath={.clusters[].cluster.certificate-authority-data}' | base64 --decode > cert.crt
Now, let’s set up the authenticator configuration in Vault.
vault write auth/auth-kubernetes/config \
token_reviewer_jwt="eyJhbGciOiJSUzI1NiIsImtpZCI6ImZsX21QdzhLbmVEYXFzbjJpNWIwNWp4M2s4T2x0WjljX1c2WWVEWHliTUEifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InZhdWx0LXRva2VuLWs4cyIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJ2YXVsdC1zZWNyZXRzLW9wZXJhdG9yLWNvbnRyb2xsZXItbWFuYWdlciIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjNlOTIxOThiLTRiZDYtNGM1MC05MTg4LTdkM2UzOGNhOTlmMyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OnZhdWx0LXNlY3JldHMtb3BlcmF0b3ItY29udHJvbGxlci1tYW5hZ2VyIn0.qH6RI2zoGzlSLpSzHeOLk2LrtDagr810S6SxodMG9AiY8cyAyQpKe1ylvOFLPvVYzLsPJhhoivV1EstbuQAM12IWDkbpecrBCzeMEXqddCL2T7ICKr-Z-0_9KF8AzJCCrkAuDSaaX_wsiEkbTLTR5xjwnkarYsuAzhvtjgns9bl3WO-tGklLQ5YBa1UTnpqm0Cnk6HgXDYsDX62VGGuHwnO2QHYE43xv63Q5Qw3aHpLUr5ciypXsrVh9g_nrAIEyQJBwm1vPuUz2mCBoeJt-ZkR4BisBjQW0EQb4Gw1WeId1Llj3X1LAXJ12k9wviKkRNdjvuSZzH7nv6kWK2dvwDQ" \
kubernetes_host="https://172.21.5.76:16443" \
kubernetes_ca_cert=@cert.crt \
disable_iss_validation=true
Success! Data written to: auth/auth-kubernetes/config
We now need to create a Policy in the Vault to read the secrets. For the example, I’ll use the same secrets structure used in the other examples. But to make it easier, I’ll leave their commands here.
vault kv put secret/infrastructure/jenkins/test-secret01 username="usr-test01" pwd="1234qwer5678"
vault kv put secret/infrastructure/certificates/devops-db @ca.crt
So, for the Policy, I’ll leave read and list permissions for the infrastructure root: secret/data/infrastructure/*
vault policy write k8s_jenkins_policy - << EOF
path "secret/data/infrastructure/*" {
capabilities = ["read", "list"]
}
EOF
Success! Uploaded policy: k8s-jenkins-policy
After the Policy, we have to create a role for the authentication method we enabled. We will inform the ServiceAccount, the default namespace and the policies that this Role will use in the Vault.
vault write auth/auth-kubernetes/role/jenkins \
bound_service_account_names=vault-secrets-operator-controller-manager \
bound_service_account_namespaces=default \
policies=default,k8s_jenkins_policy \
ttl=24h
Success! Data written to: auth/auth-kubernetes/role/jenkins
Now let’s start configuring new objects, created in the K8s cluster with the Operator installation.
The first one is VaultAuth.
Here is one of the reasons why I didn’t create the authentication method in Vault like kubernetes, there would be confusion between method and mount.
vault-auth.yaml
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: static-auth-jenkins
namespace: default
spec:
vaultConnectionRef: default
method: kubernetes
mount: auth-kubernetes
kubernetes:
role: jenkins
serviceAccount: vault-secrets-operator-controller-manager
kubectl apply -f vault-auth.yaml
vaultauth.secrets.hashicorp.com/static-auth-jenkins configured
Now, it is worth validating to ensure that there is no authentication problem.
kubectl describe vaultauth/static-auth-jenkins
[...]
vents:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Accepted 31s (x2 over 29m) VaultAuth Successfully handled VaultAuth resource request
####
kubectl get vaultauth static-auth-jenkins -o jsonpath='{.status}'
{"specHash":"1126bafc436f606ad2914c8517f29e311c3be282c94b745b90f99cc404651d4d","valid":true}
If everything is ok, let’s also create a new VaultStaticSecret object, which is nothing more than a Secret, but with differences.
In this configuration, we will have informed the mount, type and path of the secret in the Vault that we are going to map, the name of how the secret will be in K8s and the refresh rate (10s).
And yes, unfortunately, it has to be a configuration like this per Vault secret.
kv-secret.yaml
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
namespace: default
name: vault-static-secret-jenkins
spec:
vaultAuthRef: static-auth-jenkins
mount: secret
type: kv-v2
path: infrastructure/jenkins/test-secret01
refreshAfter: 10s
destination:
create: true
name: jenkins-test-secret01
rolloutRestartTargets:
- kind: Deployment
name: vault_jenkins
kubectl apply -f kv-secret.yaml
vaultstaticsecret.secrets.hashicorp.com/vault-static-secret-jenkins configured
Validate that the configuration, authentication went well.
kubectl describe vaultstaticsecrets/vault-static-secret-jenkins
[..]
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SecretSynced 11s VaultStaticSecret Secret synced
Warning RolloutRestartFailed 11s VaultStaticSecret Rollout restart failed for target v1beta1.RolloutRestartTarget{Kind:"Deployment", Name:"vault_jenkins"}: err=failed to Get object for objKey default/vault_jenkins, err=Deployment.apps "vault_jenkins" not found
Normal SecretRotated 11s VaultStaticSecret Secret synced
Let’s create the second secret:
kv-secret-2.yaml
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
namespace: default
name: vault-static-secret-certificates-devops-db
spec:
vaultAuthRef: static-auth-jenkins
mount: secret
type: kv-v2
path: infrastructure/certificates/devops-db
refreshAfter: 10s
destination:
create: true
name: certificates-devops-db
rolloutRestartTargets:
- kind: Deployment
name: vault_certificates
kubectl apply -f kv-secret-2.yaml
vaultstaticsecret.secrets.hashicorp.com/vault-static-secret-certificates-devops-db created
Also validate the status.
kubectl describe vaultstaticsecrets/vault-static-secret-certificates-devops-db
[...]
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SecretSynced 84s VaultStaticSecret Secret synced
Warning RolloutRestartFailed 84s VaultStaticSecret Rollout restart failed for target v1beta1.RolloutRestartTarget{Kind:"Deployment", Name:"vault_certificates"}: err=failed to Get object for objKey default/vault_certificates, err=Deployment.apps "vault_certificates" not found
Normal SecretRotated 84s VaultStaticSecret Secret synced
Verify that they were created.
kubectl get secret
NAME TYPE DATA AGE
certificates-devops-db Opaque 2 2m18s
jenkins-test-secret01 Opaque 3 10m
And its contents.
kubectl get secret jenkins-test-secret01 -o json | jq '.data | map_values(@base64d)'
{
"_raw": "{\"data\":{\"pwd\":\"1234qwer5678\",\"username\":\"usr-test01\"},\"metadata\":{\"created_time\":\"2025-04-11T09:18:54.347282964Z\",\"custom_metadata\":null,\"deletion_time\":\"\",\"destroyed\":false,\"version\":3}}",
"pwd": "1234qwer5678",
"username": "usr-test01"
}
kubectl get secrets certificates-devops-db -o json | jq '.data | map_values(@base64d)'
{
"_raw": "{\"data\":{\"certificate\":\"----BEGIN CERTIFICATE-----\\nMIIFwjCCA6qgAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwgYQxCzAJBgNVBAYTAlBU\\nMQ4wDAYDVQQIDAVQb3J0bzEOMAwGA1UEBwwFUG9ydG8xEjAQBgNVBAoMCURldm9w\\ncy1EQjEcMBoGA1UEAwwTbGRhcC5kZXZvcHMtZGIuaW5mbzEjMCEGCSqGSIb3DQEJ\\nARYUYWRtaW5AZGV2b3BzLWRiLmluZm8wHhcNMjQwNDA4MTEzMzE0WhcNMjUwNDA4\\nMTEzMzE0WjB0MQswCQYDVQQGEwJQVDEOMAwGA1UECAwFUG9ydG8xEjAQBgNVBAoM\\nCURldm9wcy1EQjEcMBoGA1UEAwwTbGRhcC5kZXZvcHMtZGIuaW5mbzEjMCEGCSqG\\nSIb3DQEJARYUYWRtaW5AZGV2b3BzLWRiLmluZm8wggIiMA0GCSqGSIb3DQEBAQUA\\nA4ICDwAwggIKAoICAQCz6rcxPNUdN37jfFmMf/Mwdxmf6ST3hulCJAxNBBHzOSOW\\n+umvVhfqtw88X3dgq3CW7fQvGQ2izqzN/2MLSe7yV9I1LGqLmtwN2IHTTpC3NAeH\\n8qlfuwHi+IvO2eFr0EGaL4rT5O+h8qnthPWI1/Rh9xdkx9xvTvVTsElkNZJ+zAZs\\nE41XwWRsxwHUyDRxQIL2EGbUVyjbJgTzoxQQHdetIeYc6Vya7ABHa6iwfSMKR99A\\nNdOujpnNecGnGJSzQ0B6RXp2ae28LjjyVdMV1wo6PrvgsKqt5MO4GxyPKginVp0s\\nly2zVshCzREw6/4lxxJXqRHcmcS6mhm0U77QTG32A2bU+SJONqxF6GVil60sGm59\\nMxwDvkgQuE9QDQ4+txYh8K97PfxqVcPG92PS2sYOinc1EBsnD6TEsuvfc8m0Ev3m\\nnjZ+jgJA4Gf+e9AbfgQVJEbdQj7mM2gRFL7CWKAgzk2KRh9xWTlk8nCwqCwHx57d\\nhE+qfC/SkInZCQHGTSQ22+RidEXq7GxtY2KDNSFSTPeGjR8KA5+lqeuIf8ZeHc6s\\nHEtK12B8yvYDfRyCBnPugXxs77CiTcmUvWCfBP9GZv4icoCdDTzfnfTNaGO9X16+\\nXY5sC27o24WsxXLF9EDtp6FpnGX1moR6h89Jky+clCwpDUj4sucWz4WPtHpAnwID\\nAQABo00wSzAJBgNVHRMEAjAAMB0GA1UdDgQWBBTP5qF7BCcepDXMDBBq+Bbonlr2\\nFzAfBgNVHSMEGDAWgBRN6D0ZxNB2qFyd9JTxZ/oYSBfMTjANBgkqhkiG9w0BAQsF\\nAAOCAgEAjz2Hx2s8A+liNwaEGPS1B/TDHDUKgzsrjXPEVfni3vhlhqgczej/rDAX\\nd8pLJ4QawlFPbQVZqFFsyJjo6dAG2WfIJeA0c+7kw1pVcbVqQKymq7UtOaHiZF8j\\njkBuxKJH4P+lxBLSWCiktvqhTJ/1IgQ66R3wve8J0+KCHGsX2BFMA8Zqc48PTXnu\\nNGLUsIIYNwMlsCyfEYRY9s6Lg5q8Vcb1dN39uJwMe+Q8XKM0tPM+Dn1z4289q9CZ\\npoGfhonu3u/9e/LsrptE0ZURGqBFCXXU8gAq0Yg1ViGwcA8e6HC6iia6H0Y4vS6j\\nq33VIswn0In56zw4gOD/BnYHU5B/277hY8UIsU6vQaTZbpE/GpW8RT85V4MT6r5W\\nMI2WF8ZATyBbJ5PbXctYfmnydQwTXpoNnioGmxH4Qu0S+ydc+yneAIlZWURlq5gB\\nYuZrmI7xuGxs5UfzQBEFroRlI6QmW95ebkac5XwURTW1/bI5deJZnfTOezRJKr5n\\nL1yySSIs2rYXQkphDwV6HqSNg3eJSEGusCbV9Kgx52X5uorfot5i1bZUJcSKJJby\\n38UpFzQ4/SOBewNfWAq6Q1NBWUbfn8BARv96L7n3uK4JrxfkUrHGjzWGJmtlgx+b\\nshJhA/b/35i7KxmdI6eGy0I3vylbGjaH0WGS4+QRYeJKWXPm6kk=\\n-----END CERTIFICATE-----\\n\"},\"metadata\":{\"created_time\":\"2025-03-12T11:18:49.50067416Z\",\"custom_metadata\":null,\"deletion_time\":\"\",\"destroyed\":false,\"version\":4}}",
"certificate": "----BEGIN CERTIFICATE-----\nMIIFwjCCA6qgAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwgYQxCzAJBgNVBAYTAlBU\nMQ4wDAYDVQQIDAVQb3J0bzEOMAwGA1UEBwwFUG9ydG8xEjAQBgNVBAoMCURldm9w\ncy1EQjEcMBoGA1UEAwwTbGRhcC5kZXZvcHMtZGIuaW5mbzEjMCEGCSqGSIb3DQEJ\nARYUYWRtaW5AZGV2b3BzLWRiLmluZm8wHhcNMjQwNDA4MTEzMzE0WhcNMjUwNDA4\nMTEzMzE0WjB0MQswCQYDVQQGEwJQVDEOMAwGA1UECAwFUG9ydG8xEjAQBgNVBAoM\nCURldm9wcy1EQjEcMBoGA1UEAwwTbGRhcC5kZXZvcHMtZGIuaW5mbzEjMCEGCSqG\nSIb3DQEJARYUYWRtaW5AZGV2b3BzLWRiLmluZm8wggIiMA0GCSqGSIb3DQEBAQUA\nA4ICDwAwggIKAoICAQCz6rcxPNUdN37jfFmMf/Mwdxmf6ST3hulCJAxNBBHzOSOW\n+umvVhfqtw88X3dgq3CW7fQvGQ2izqzN/2MLSe7yV9I1LGqLmtwN2IHTTpC3NAeH\n8qlfuwHi+IvO2eFr0EGaL4rT5O+h8qnthPWI1/Rh9xdkx9xvTvVTsElkNZJ+zAZs\nE41XwWRsxwHUyDRxQIL2EGbUVyjbJgTzoxQQHdetIeYc6Vya7ABHa6iwfSMKR99A\nNdOujpnNecGnGJSzQ0B6RXp2ae28LjjyVdMV1wo6PrvgsKqt5MO4GxyPKginVp0s\nly2zVshCzREw6/4lxxJXqRHcmcS6mhm0U77QTG32A2bU+SJONqxF6GVil60sGm59\nMxwDvkgQuE9QDQ4+txYh8K97PfxqVcPG92PS2sYOinc1EBsnD6TEsuvfc8m0Ev3m\nnjZ+jgJA4Gf+e9AbfgQVJEbdQj7mM2gRFL7CWKAgzk2KRh9xWTlk8nCwqCwHx57d\nhE+qfC/SkInZCQHGTSQ22+RidEXq7GxtY2KDNSFSTPeGjR8KA5+lqeuIf8ZeHc6s\nHEtK12B8yvYDfRyCBnPugXxs77CiTcmUvWCfBP9GZv4icoCdDTzfnfTNaGO9X16+\nXY5sC27o24WsxXLF9EDtp6FpnGX1moR6h89Jky+clCwpDUj4sucWz4WPtHpAnwID\nAQABo00wSzAJBgNVHRMEAjAAMB0GA1UdDgQWBBTP5qF7BCcepDXMDBBq+Bbonlr2\nFzAfBgNVHSMEGDAWgBRN6D0ZxNB2qFyd9JTxZ/oYSBfMTjANBgkqhkiG9w0BAQsF\nAAOCAgEAjz2Hx2s8A+liNwaEGPS1B/TDHDUKgzsrjXPEVfni3vhlhqgczej/rDAX\nd8pLJ4QawlFPbQVZqFFsyJjo6dAG2WfIJeA0c+7kw1pVcbVqQKymq7UtOaHiZF8j\njkBuxKJH4P+lxBLSWCiktvqhTJ/1IgQ66R3wve8J0+KCHGsX2BFMA8Zqc48PTXnu\nNGLUsIIYNwMlsCyfEYRY9s6Lg5q8Vcb1dN39uJwMe+Q8XKM0tPM+Dn1z4289q9CZ\npoGfhonu3u/9e/LsrptE0ZURGqBFCXXU8gAq0Yg1ViGwcA8e6HC6iia6H0Y4vS6j\nq33VIswn0In56zw4gOD/BnYHU5B/277hY8UIsU6vQaTZbpE/GpW8RT85V4MT6r5W\nMI2WF8ZATyBbJ5PbXctYfmnydQwTXpoNnioGmxH4Qu0S+ydc+yneAIlZWURlq5gB\nYuZrmI7xuGxs5UfzQBEFroRlI6QmW95ebkac5XwURTW1/bI5deJZnfTOezRJKr5n\nL1yySSIs2rYXQkphDwV6HqSNg3eJSEGusCbV9Kgx52X5uorfot5i1bZUJcSKJJby\n38UpFzQ4/SOBewNfWAq6Q1NBWUbfn8BARv96L7n3uK4JrxfkUrHGjzWGJmtlgx+b\nshJhA/b/35i7KxmdI6eGy0I3vylbGjaH0WGS4+QRYeJKWXPm6kk=\n-----END CERTIFICATE-----\n"
}