OpenBao¶
Environment Variables¶
To use OpenBao, you need to set the following environment variables:
| Key | Value |
|---|---|
SECRET_PROVIDER | openbao |
SECRET_PROVIDER_SITE_URL | The URL of the OpenBao instance |
SECRET_PROVIDER_ACCESS_TOKEN | Access token for authenticating with the secret provider |
SECRET_PROVIDER_ACCESS_TOKEN_FILE | Path to a file containing the access token inside the container |
Set SECRET_PROVIDER_ACCESS_TOKEN or SECRET_PROVIDER_ACCESS_TOKEN_FILE, not both.
For the environment variables that control automatic certificate rotation, see below.
Access token rotation¶
With SECRET_PROVIDER_ACCESS_TOKEN_FILE, Doco-CD reads the file again every time it fetches something from OpenBao. If the token in the file changes during runtime, the next request simply uses the new one and you don't have to restart anything. A token set with SECRET_PROVIDER_ACCESS_TOKEN is only read at startup. SOPS-encrypted token files work as well.
Doco-CD does not renew the token itself
Let something else handle that, for example OpenBao Agent, and have it write each new token to the file.
If the file is missing, empty or can't be decrypted, the request fails with an error. Doco-CD never falls back to the previous token. As soon as the token file is valid again, the next request works.
Mount the directory, not the file
Write the new token to a temporary file and move it over the old one, so Doco-CD never reads a half-written token. Then mount the directory that contains the token file. A single bind-mounted file keeps pointing at the old file after it is replaced on the host.
Deployment configuration¶
Add a mapping/reference between the environment variable you want to set in the docker compose project/stack and the reference to the key-value secret in OpenBao.
By default, the root namespace is used (root or /), but you can specify a different namespace by adding it as the first part of the reference.
- A valid key-value secret reference should use the syntax:
- A valid PKI certificate reference (read-only, fetches an already-issued certificate) should use the syntax:
- A valid PKI role reference (issues a new certificate on every deployment/rotation, see Automatic Certificate Rotation) should use the syntax:
Examples of valid references:
kv:prod-secrets:db-prod:username→ Fetches theusernamekey from thedb-prodkey-value secret in theprod-secretssecret engine in therootnamespace.kv:root:prod-secrets:db-prod:username→ Same as above, explicitly specifying therootnamespace.kv:my-namespace:secret:api-keys:stripe→ Fetches thestripekey from theapi-keyssecret in thesecretkey-value secret engine in themy-namespacenamespace.pki:certs:myapp.example.com→ Fetches the certificate for the common namemyapp.example.comfrom thecertspki secret engine in therootnamespace.pki:my-namespace:certs:myapp.example.com→ Fetches the certificate for the common namemyapp.example.comfrom thecertspki secret engine in themy-namespacenamespace.pki-role:certs:myapp-role:myapp.example.com→ Issues a new certificate for the common namemyapp.example.comusing themyapp-rolePKI role in thecertssecret engine in therootnamespace.pki-role:my-namespace:certs:myapp-role:myapp.example.com→ Same as above, in themy-namespacenamespace.
Example¶
For example in your .doco-cd.yml:
name: myapp
external_secrets:
DB_USERNAME: kv:secret:db-prod:username
DB_PASSWORD: kv:secret:db-prod:password
CERT: pki:pki:myapp.example.com
To use the certificate in your compose file, you can pass the value to a compose config:
configs:
myapp-example-com.crt:
#environment: CERT # Either pass the variable via the environment like this (without a $ sign)
content: $CERT # Or use the content field to directly inject the variable value to the config content
services:
app:
image: myapp:latest
environment:
DB_USERNAME: $DB_USERNAME
DB_PASSWORD: $DB_PASSWORD
configs:
- source: myapp-example-com.crt
target: /etc/ssl/certs/example.crt
Full certificate chain¶
A pki: or pki-role: reference resolves to the leaf certificate only. Many TLS servers (nginx, HAProxy, Traefik, Postgres, …) instead expect a fullchain bundle: the leaf certificate followed by the intermediate and root CA certificates that signed it, so clients can build a complete trust path.
Every certificate reference therefore automatically exposes that bundle as an additional environment variable, named after the certificate's variable with a _FULL suffix — the same mechanism as the _KEY suffix used for private keys. An external secret named CERT resolves to:
CERT→ the leaf certificate PEMCERT_FULL→ the leaf certificate followed by its issuing CA chainCERT_KEY→ the matching private key PEM (pki-role:references only)
configs:
myapp-fullchain.crt:
environment: CERT_FULL
myapp-cert.key:
environment: CERT_KEY
services:
app:
image: myapp:latest
configs:
- source: myapp-fullchain.crt
target: /etc/ssl/certs/myapp-fullchain.crt
- source: myapp-cert.key
target: /etc/ssl/private/myapp.key
The _FULL value is a single PEM bundle, with the leaf certificate always first:
-----BEGIN CERTIFICATE----- <- leaf certificate (the one issued for your common name)
-----BEGIN CERTIFICATE----- <- intermediate CA (if any)
-----BEGIN CERTIFICATE----- <- root CA
How the chain is determined depends on the reference type:
pki-role:references use theca_chainfield of the issue response, falling back toissuing_cawhen the mount does not return a chain.pki:references use the chain of the issuer that actually signed the certificate, so certificates from a non-default issuer on a multi-issuer mount get the correct chain. The issuers are listed via<secretEngine>/issuersand read via<secretEngine>/issuer/<id>/json, both unauthenticated endpoints, so no additional policy is required. When several issuers share the signing key (e.g. a reissued CA), the currently valid one is preferred. The mount's default chain (<secretEngine>/cert/ca_chain) is only used when all issuers could be read and none of them signed the certificate. If some issuers could not be read and none of the readable ones matches, resolving the reference fails instead of returning a possibly wrong chain.
When no chain is available at all, _FULL holds the leaf certificate on its own.
Note
Use CERT where a leaf-only certificate is expected and CERT_FULL where a bundle is; both are always set, so you can switch without touching the reference. Automatic certificate rotation covers all three entries: expiry and serial tracking always read the leaf from CERT, and services consuming CERT_FULL are recreated on rotation just like those consuming CERT or CERT_KEY.
Warning
Because the _FULL and _KEY entries are generated, an external secret of your own may not use those names. Mapping both CERT and CERT_FULL in external_secrets is rejected with a conflict error.
Automatic Certificate Rotation¶
Certificates issued via a pki-role: reference (see above) are eligible for automatic rotation: doco-cd runs a background watcher that tracks the expiry of every deployment whose certificate(s) were all issued through pki-role: refs, and automatically reissues the certificate(s) and redeploys the project before they expire.
Note
Deployments that use the read-only pki: reference are not eligible for automatic rotation, since that reference only reads an already-issued certificate and cannot reissue a new one. Use pki-role: if you want doco-cd to keep your certificates renewed automatically.
Enabling automatic rotation¶
Automatic certificate rotation is currently only supported for OpenBao PKI roles:
| Key | Type | Description | Default |
|---|---|---|---|
CERT_ROTATION_ENABLED | boolean | Enables the built-in automatic certificate rotation watcher. | false |
CERT_ROTATION_THRESHOLD | duration | How far ahead of a certificate's expiry doco-cd triggers automatic rotation. Accepts a Go duration (e.g. 72h). | 72h |
CERT_ROTATION_CHECK_INTERVAL | duration | How often the certificate rotation watcher checks deployed certificates for upcoming expiry. Accepts a Go duration. | 1h |
How it works¶
- When a deployment resolves a
pki-roleexternal secret, doco-cd stamps three labels on the deployed container(s)/service(s):cd.doco.deployment.cert.expiry: the earliest expiry (RFC3339) among all certificates in the deployment.cd.doco.deployment.cert.rotatable:trueonly if every certificate-bearing external secret in the deployment used apki-rolereference.cd.doco.deployment.cert.state: the reference and serial number of every deployedpki-rolecertificate, used to detect revocation.
- The watcher periodically (every
CERT_ROTATION_CHECK_INTERVAL) lists all deployments labeledcert.rotatable=trueand rotates a deployment when either is true:- its recorded expiry is within
CERT_ROTATION_THRESHOLD(or already past), - or any certificate in its
cert.statelabel has since been revoked in OpenBao.
- its recorded expiry is within
-
When a deployment is due, doco-cd reloads its deploy config and re-resolves all external secrets. It issues brand-new certificates and keys for every
pki-rolereference, then redeploys to pick up the fresh values:Only services that actually consume a rotated certificate or private key (via direct environment variables or
configs/secretsentries) get recreated; unrelated services in the same project are left untouched.
Private key access¶
A pki-role: reference automatically exposes the certificate's matching private key as a second environment variable, named after the certificate's variable with a _KEY suffix. For example, an external secret named CERT referencing pki-role:... resolves to both:
CERT→ the leaf certificate PEMCERT_KEY→ the matching private key PEM
The full certificate chain is exposed alongside them as CERT_FULL.
configs:
myapp-cert.crt:
environment: CERT
myapp-cert.key:
environment: CERT_KEY
services:
app:
image: myapp:latest
configs:
- source: myapp-cert.crt
target: /etc/ssl/certs/myapp.crt
- source: myapp-cert.key
target: /etc/ssl/private/myapp.key
Examples¶
mTLS¶
To secure service-to-service communication with mutual TLS (mTLS), issue a client certificate for your application through a PKI role dedicated to mTLS clients, and let doco-cd keep it renewed automatically:
configs:
myapp-mtls.crt:
content: $MTLS_CERT
myapp-mtls.key:
content: $MTLS_CERT_KEY
services:
app:
image: myapp:latest
configs:
- source: myapp-mtls.crt
target: /etc/ssl/certs/mtls-client.crt
- source: myapp-mtls.key
target: /etc/ssl/private/mtls-client.key
Enable CERT_ROTATION_ENABLED=true on the doco-cd instance, and the client certificate will be reissued and redeployed automatically before it expires, so mTLS handshakes never fail due to an expired client certificate.
Private PKI¶
For internal services that need a server certificate signed by your own private Certificate Authority, define a PKI role that constrains what common names/domains it may issue for (e.g. allowed_domains: internal.example.com), then reference it the same way:
name: internal-service
external_secrets:
SERVER_CERT: pki-role:pki:internal-services-role:api.internal.example.com
configs:
server.crt:
content: $SERVER_CERT
server.key:
content: $SERVER_CERT_KEY
services:
app:
image: internal-service:latest
configs:
- source: server.crt
target: /etc/ssl/certs/server.crt
- source: server.key
target: /etc/ssl/private/server.key
With CERT_ROTATION_ENABLED=true, doco-cd continuously monitors the certificate's expiry and automatically reissues + redeploys the service well ahead of expiry, so your internal PKI-secured services stay trusted without any manual certificate management.