Skip to main content
Version: 0.1.0-rc.5
Validated lane

This recipe follows the local ACME lifecycle covered by the in-repo ACME suite and the project validation environment. The tested path covers private ACME trust material, Transit auto-unseal, ACME readiness, and human admin JWT access.

Use the main docs for generic behavior

Use external access, network configuration, and TLS and workload identity for the product-wide explanation. This recipe captures the exact validated local lane.

Decision matrix

What this lane assumes

What this lane assumes.
AssumptionWhy it existsWhat breaks if it is wrong
The external trust-services endpoint exposes both Transit and an ACME directoryOne dependency owns both the seal root and the private certificate issuance path.You will not reproduce the lane if Transit and ACME trust material come from unrelated systems.
The external hostname resolves back to the passthrough edgetls-alpn-01 only works when the validator reaches the hostname that OpenBao serves.ACME will fail even though the cluster itself is healthy and the route object exists.
The ingress layer supports TLS passthrough on port 443OpenBao must terminate TLS itself in this lane.Any edge termination in front of OpenBao breaks the ACME contract immediately.

Reference table

Inputs to replace before apply

Inputs to replace before apply.
PlaceholderExamplePurpose
<namespace>openbaocluster-acmeTenant namespace for the cluster.
<cluster-name>openbaocluster-acmeOpenBaoCluster name.
<openbao-version>2.5.0OpenBao version.
<transit-address>https://trust-services.openbao-infra.svc:8200Transit provider URL.
<acme-directory-url>https://trust-services.openbao-infra.svc:8200/v1/pki/acme/directoryACME directory URL.
<transit-key>openbao-unsealTransit key name.
<external-host>bao-acme.example.comExternal hostname used for clients and ACME validation.
<operator-namespace>openbao-operator-systemNamespace that hosts the central OpenBaoTenant resource.

Step 1: Onboard the tenant namespace

Apply

Create the namespace, onboarding request, and admin ServiceAccount

yaml

apiVersion: v1
kind: Namespace
metadata:
name: <namespace>
labels:
openbao.org/tenant: "true"
---
apiVersion: openbao.org/v1alpha1
kind: OpenBaoTenant
metadata:
name: <cluster-name>-tenant
namespace: <operator-namespace>
spec:
targetNamespace: <namespace>
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: openbao-admin
namespace: <namespace>

Verify

Wait for tenant provisioning

bash

kubectl -n <operator-namespace> describe openbaotenant <cluster-name>-tenant

The steady-state expectation is Provisioned=True.

Step 2: Create the Transit and ACME trust Secret

Apply

Create the Secret used by Transit and the private ACME issuer

bash

kubectl -n <namespace> create secret generic trust-services-token \
--from-literal=token='<transit-token>' \
--from-file=ca.crt=/path/to/trust-services-ca.crt \
--from-file=pki-ca.crt=/path/to/acme-issuer-ca.crt

The validated path expects token, ca.crt, and pki-ca.crt in the same Secret so Transit and ACME trust material stay aligned.

Step 3: Expose the ACME challenge Service with passthrough

Apply

Create the user-managed passthrough route

yaml

apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: bao-acme
namespace: <namespace>
spec:
entryPoints:
- websecure
routes:
- match: HostSNI(`<external-host>`)
services:
- name: <cluster-name>-acme
port: 443
tls:
passthrough: true

tls.mode: ACME requires passthrough. If the edge terminates TLS first, OpenBao cannot complete ACME challenges.

Step 4: Apply the hardened ACME cluster

Apply

Apply the validated hardened ACME manifest

yaml

apiVersion: openbao.org/v1alpha1
kind: OpenBaoCluster
metadata:
name: <cluster-name>
namespace: <namespace>
spec:
profile: Hardened
replicas: 3
version: "<openbao-version>"

storage:
size: "10Gi"
deletionPolicy: Retain

tls:
enabled: true
mode: ACME
acme:
directoryURL: "<acme-directory-url>"
domains:
- "<cluster-name>-acme.<namespace>.svc"
- "<external-host>"
email: "admin@example.invalid"

configuration:
logLevel: "info"
ui: true
logging:
format: "json"
acmeCARoot: "/etc/bao/seal-creds/ca.crt"

unseal:
type: transit
credentialsSecretRef:
name: trust-services-token
transit:
address: "<transit-address>"
mountPath: "transit"
keyName: "<transit-key>"
tlsCACert: "/etc/bao/seal-creds/ca.crt"

selfInit:
enabled: true
oidc:
enabled: true
requests:
- name: enable-jwt-auth
operation: update
path: sys/auth/jwt
authMethod:
type: jwt
- name: create-admin-policy
operation: update
path: sys/policies/acl/admin
policy:
policy: |
path "*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
- name: create-admin-jwt-role
operation: update
path: auth/jwt/role/admin
data:
role_type: jwt
user_claim: sub
bound_audiences:
- openbao-internal
bound_subject: system:serviceaccount:<namespace>:openbao-admin
token_policies:
- admin
policies:
- admin
ttl: 1h

upgrade:
strategy: RollingUpdate
Internal `.svc` domain

The internal .svc hostname in spec.tls.acme.domains gives the local lane a stable service name for certificate issuance and cluster-internal joins while the external hostname stays present in the SAN set.

AppArmor on local clusters

If kubelet rejects the Pods because AppArmor is unavailable, add:

spec:
workloadHardening:
appArmorEnabled: false

Verify the lane

Verify

Check the cluster conditions

bash

kubectl -n <namespace> get openbaocluster <cluster-name> \
-o jsonpath='{range .status.conditions[*]}{.type}={.status}{" reason="}{.reason}{"\n"}{end}'

The steady-state expectation is Available=True, ACMEIntegrationReady=True, ACMECacheReady=True, UserAccessBootstrap=True, ProductionReady=True, OpenBaoInitialized=True, and OpenBaoSealed=False.

Verify

Verify that the ACME Service exists and no external TLS Secret is required

bash

kubectl -n <namespace> get svc <cluster-name>-acme
kubectl -n <namespace> get secret <cluster-name>-tls-server

The service should exist. The Secret lookup should return NotFound, because OpenBao manages the leaf certificate itself in this lane.

Verify

Verify JWT admin login

bash

kubectl -n <namespace> port-forward svc/<cluster-name> 8200:8200
export VAULT_ADDR="https://127.0.0.1:8200"
JWT="$(kubectl -n <namespace> create token openbao-admin --audience openbao-internal --duration=1h)"

curl -sS -k \
-H 'Content-Type: application/json' \
-d "{\"role\":\"admin\",\"jwt\":\"${JWT}\"}" \
${VAULT_ADDR%/}/v1/auth/jwt/login

Keep moving

Prerelease documentation

This version tracks a prerelease build. Features and behavior may change before the next stable release.

Was this page helpful?

Use Needs work to open a structured GitHub issue for this page. The Yes button only acknowledges the signal locally.