DEV Community

Cover image for True Secrets Auto Rotation with ESO and Vault
Lucas Severo Alves
Lucas Severo Alves

Posted on • Updated on

True Secrets Auto Rotation with ESO and Vault

Translation to pt-br at https://knela.dev/blog/auto-rotao-de-segredos-de-verdade-com-eso-e-vault

Requirements

  • A Kubernetes cluster that you can use (kind, minikube, something managed) and kubectl to connect to it
  • Vault CLI
  • External Secrets Operator (ESO) installed.
  • Vault installed through the helm chart

What we want to achieve

Image description

This guide aims to establish an automatic hourly rotation of a database connection secret. Following these steps, an administrator sets up the process once, ensuring that the secret refreshes/changes every hour. Simultaneously, the application will always maintain new valid credentials for seamless database interactions.

Image description

ESO secret Generators

⚠️ As of this writing this feature is in alpha state, and we want more people to help test it, so we can make improvements, and eventually promote to stable.

Documentation around it is a bit limited, that's why I am getting this guide out in my blog, while we figure better ways to bring these into our documentation.

Image description

Getting Started

Let's make sure we start with the same setup locally:

  • I have installed Vault in a namespace named vault.
    • You can use helm install vault hashicorp/vault -n vault --create-namespace command instead of the one provided in the guide.
    • Follow all steps in there to init Vault, unseal, and get the cluster-keys.json with the token.
    • You can skip other steps.
  • I have installed ESO in the default namespace.

In this guide we are going to use Vault token authentication just for the sake of simplicity. However, please never use this in real setups. Prefer service account auth.

After properly starting Vault and unsealing it, take note of your auth token. Let's do a port forward and authenticate in our work desktop so we don't have to exec into Vault every time we need to run commands.

In a new terminal (this terminal will be blocked)

kubectl -n vault port-forward service/vault 8200:8200
Enter fullscreen mode Exit fullscreen mode

In another terminal you can run.

export VAULT_ADDR=http://127.0.0.1:8200
vault login
## type your auth token
Enter fullscreen mode Exit fullscreen mode

Image description

Simple Deployment of PostgreSQL

To have an interesting example, let's deploy psql and configure it so we can let Vault and other workloads connect to it.

Let's first create a configmap with an admin user and password for this psql instance (just for simplicity and to get to the other part of the guide quickly).

cat <<EOF > postgres-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-config
  labels:
    app: postgres
data:
  POSTGRES_DB: postgresdb
  POSTGRES_USER: admin
  POSTGRES_PASSWORD: psltest
EOF
Enter fullscreen mode Exit fullscreen mode

Apply it.

kubectl apply -f postgres-config.yaml
Enter fullscreen mode Exit fullscreen mode

Now create the postgres-deployment.yaml.

cat <<EOF > postgres-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres  # Sets Deployment name
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:10.1 # Sets Image
          imagePullPolicy: "IfNotPresent"
          ports:
            - containerPort: 5432  # Exposes container port
          envFrom:
            - configMapRef:
                name: postgres-config

EOF
Enter fullscreen mode Exit fullscreen mode

Apply it.

kubectl apply -f postgres-deployment.yaml
Enter fullscreen mode Exit fullscreen mode

And finally, let's create a service, so other workloads can access it:

cat <<EOF > postgres-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: postgres # Sets service name
  labels:
    app: postgres # Labels and Selectors
spec:
  type: NodePort # Sets service type
  ports:
    - port: 5432 # Sets port to run the postgres application
  selector:
    app: postgres

EOF
Enter fullscreen mode Exit fullscreen mode

Apply it.

kubectl apply -f postgres-service.yaml
Enter fullscreen mode Exit fullscreen mode

Preparing DB with new readonly role

Exec into the psql pod.

kubectl get pods # get pod name
kubectl exec -it <postgres-pod-name> -- bash
Enter fullscreen mode Exit fullscreen mode

Change into postgres user, and run commands to create the new role.

su postgres
psql -c "CREATE ROLE \"ro\" NOINHERIT;"
psql -c "GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"ro\";"
Enter fullscreen mode Exit fullscreen mode

We are going to use this role when configuring Vault to use Dynamic Secrets with psql plugin.

Image description

Vault Dynamic Secrets

Vault Dynamic secrets are in fact meant to be used as a way to get short lived credentials. However, there is nothing stopping us from using them in our auto-rotation process. There are various other plugins that integrate with other systems, like AWS credentials, or certificate issuing systems. Most of these are also interesting in the context of ESO, but I wanted a self-contained example with no need to create external accounts for you to try it out.

Lets first enable the database engine.

vault secrets enable database
Enter fullscreen mode Exit fullscreen mode

After that, let's configure PostgreSQL secrets engine, with the admin creds we had before (we are passing credentials into the connection url here, never do that outside of test labs).

## POSTGRES_URL with name of the service and namespace
export POSTGRES_URL=postgres.default.svc.cluster.local:5432

vault write database/config/postgresql \
     plugin_name=postgresql-database-plugin \
connection_url="postgresql://admin:psltest@$POSTGRES_URL/postgres?sslmode=disable" \
     allowed_roles=readonly \
     username="root" \
     password="rootpassword"
Enter fullscreen mode Exit fullscreen mode

Create an SQL file container the templated command that will be used by Vault when dynamically creating roles.

tee readonly.sql <<EOF
CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}' INHERIT;
GRANT ro TO "{{name}}";
EOF
Enter fullscreen mode Exit fullscreen mode

Write that into Vault and configure default expiration of new requested roles and other fields (this will fail if you did not create the ROLE 'ro' correctly while setting up psql).

vault write database/roles/readonly \
      db_name=postgresql \
      creation_statements=@readonly.sql \
      default_ttl=1h \
      max_ttl=24h
Enter fullscreen mode Exit fullscreen mode

You can already check within Vault if you can get the temporary credentials before setting up other steps.

vault read database/creds/readonly
## response
Key                Value
---                -----
lease_id           database/creds/readonly/CPqcUrG55f8qfrA9QKMV3peO
lease_duration     1h
lease_renewable    true
password           5p-xDWSC5Iu9z-hlZPrs
username           v-root-readonly-SQjhNhGxxmKx9QaRKsxM-1690473242
Enter fullscreen mode Exit fullscreen mode

Image description

ESO Generator and ExternalSecret

Before next steps we are going to base64 encode the token so we can apply it with a secret. Grab you Vault token and echo it into base64.

echo "somethinsomething" | base64
Enter fullscreen mode Exit fullscreen mode

Now we can use the new External Secrets Operator CRD, the Generator. Use the value outputted above for the auth token secret (vault-token).

cat <<EOF > vaultDynamicSecret.yaml
apiVersion: generators.external-secrets.io/v1alpha1
kind: VaultDynamicSecret
metadata:
  name: "psql-example"
spec:
  path: "/database/creds/readonly" ## this is how you choose which vault dynamic path to use
  method: "GET" ## this path will only work with GETs
  # parameters: ## no needed parameters 
  # ...
  provider:
    server: "http://vault.vault.svc.cluster.local:8200" ## vault url. In this case vault service on the vault namespace
    auth:
      # points to a secret that contains a vault token
      # https://www.vaultproject.io/docs/auth/token
      tokenSecretRef: ## reference to the secret holding the Vault auth token
        name: "vault-token"
        key: "token"
---
apiVersion: v1
kind: Secret
metadata:
  name: vault-token
data:
  token: aHZzLkM4M0o2UWNQSW1YQkRJVU96aWNNNzVHdwo= ## token base64 encoded
EOF
Enter fullscreen mode Exit fullscreen mode

Apply this file.

kubectl apply -f vaultDynamicSecret.yaml
Enter fullscreen mode Exit fullscreen mode

And finally we can now create our ExternalSecret that in the end will let the operator create the final Kubernetes Secret.

cat <<EOF > vaultDynamicSecret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: "psql-example-es"
spec:
  refreshInterval: "1h" ## the same as the expiry time on the dynamic config of Vault, or lower, so apps have always new valid credentials
  target:
    name: psql-example-for-use ## the final name of the kubernetes secret created in your cluster
  dataFrom:
  - sourceRef:
      generatorRef:
        apiVersion: generators.external-secrets.io/v1alpha1
        kind: VaultDynamicSecret
        name: "psql-example" ## reference to the generator
EOF
Enter fullscreen mode Exit fullscreen mode

Apply this and check if the status of the ExternalSecret is ok.

kubectl get externalsecret
## response
NAME              STORE   REFRESH INTERVAL   STATUS         READY
psql-example-es           1h                 SecretSynced   True
Enter fullscreen mode Exit fullscreen mode

If you get errors here, verify that you used the right path in the Generator. Also check that you created the right roles inside psql and you can ping vault from a pod in the ESO namespace.

Checking the final secret

You should get a secret containing new users and passwords with read-only access to the database every hour.

kubectl get secrets psql-example-for-use -o jsonpath="{.data}"
## response
{"password":"V2lSWUlqZzdvQS1yOTFaV2N1SWE=","username":"di1yb290LXJlYWRvbmx5LVlXQ3kzZ01hbkhSbGtuY3FqTUg2LTE2OTA0NzIwMzc="}
Enter fullscreen mode Exit fullscreen mode

To check one of the values you can get it and base64 decode it.

kubectl get secrets psql-example-for-use -o jsonpath="{.data.password}" | base64 -d
Enter fullscreen mode Exit fullscreen mode

Now your application can use this secret, it will be automatically auto-rotated, and still be a valid credential to the database.

Caveats

If you use secrets as Environment Variables you will need to use something to make workloads get the new credentials, if they just loose connection. You can use the Reloader project for that.

If you use secrets as volumes, pods will get that update automatically, and you won't have problems connecting, as long as your application can get the new values.

Conclusion

Image description

That's it! We've set up an auto-rotating secret for a database connection using ESO and Vault. The magic is, as we said, you can set it once and forget. Your secret refreshes every hour and your app stays connected to the database with new valid credentials. It is secure, you follow best practices with regard to rotation, and you avoid manual intervention if that is not needed.

Top comments (0)