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.

I will not create any namespaces, I will use all of them in the default, it is up to you to create or use your namespaces. I’ll leave it like this, to make the example simpler.
Normally I would create the Kubernetes authentication method, but to make it easier to understand, as this parameter may be confused with others that should be Kubernetes, I will start as auth-kubernetes.

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"
}