Skip to main content
Version: next

This recipe should leave you with

  • an onboarded tenant namespace and admin ServiceAccount
  • a Hardened-profile cluster that unseals with AWS KMS and serves the public hostname through passthrough
  • public ACME issuance completed by OpenBao itself with a shared ACME cache
  • manual and scheduled S3 backups using a backup identity distinct from the main workload identity
Validated lane

This recipe matches the hardened Amazon EKS lane validated in the project cloud environment. The tested path covered KMS auto-unseal, public ACME issuance, dedicated passthrough ingress, JWT bootstrap, and successful S3 backups.

Public reachability is a hard requirement

A public ACME CA such as Let's Encrypt must reach the hardened hostname on port 443. Do not source-restrict the hardened passthrough hostname to a single client IP and still expect this lane to work.

Decision matrix

What this lane assumes

What this lane assumes.
AssumptionWhy it existsWhat breaks if it is wrong
A dedicated public passthrough Gateway exists for the hardened hostnameThe public OpenBao hostname must stay separate from the terminating admin edge.Using a shared terminating edge or the wrong listener changes the lane's TLS contract completely.
The Gateway controller supports TLSRoute and public passthroughOpenBao has to remain the TLS endpoint while ACME validation reaches it on 443.The route can look syntactically correct while the public edge never forwards TLS correctly.
RWX storage is available for the shared ACME cacheHA ACME depends on multi-replica access to shared certificate state.ACME readiness will fail or remain unstable if the cache path is not truly shared.

Reference table

Inputs to replace before apply

Inputs to replace before apply.
PlaceholderExamplePurpose
<namespace>openbaocluster-hardenedTenant namespace for the cluster.
<cluster-name>openbaocluster-hardenedOpenBaoCluster name.
<openbao-version>2.5.1OpenBao version.
<aws-region>eu-central-1AWS region for KMS and S3.
<kms-key-arn>arn:aws:kms:...KMS key ARN for auto-unseal.
<main-role-arn>arn:aws:iam::...:role/openbao-unsealIRSA role for the main OpenBao Pods.
<backup-role-arn>arn:aws:iam::...:role/openbao-backupIRSA role for backup Jobs.
<backup-bucket>openbao-backupsS3 bucket for snapshots.
<external-host>bao.example.comPublic hostname for the hardened cluster.
<gateway-name>openbao-hardened-gatewayDedicated passthrough Gateway.
<gateway-namespace>defaultNamespace of the Gateway.
<gateway-class-name>traefik-passthroughGatewayClass used by the dedicated passthrough edge.
<acme-cache-storage-class>efs-acmeRWX StorageClass for the shared ACME cache.
<operator-namespace>openbao-operator-systemNamespace that hosts the central OpenBaoTenant resource.

Step 1: Create the dedicated public passthrough Gateway

Apply

Expose the hardened hostname through a dedicated TLS passthrough Gateway

yaml

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: <gateway-name>
namespace: <gateway-namespace>
spec:
gatewayClassName: <gateway-class-name>
listeners:
- name: websecure-passthrough
hostname: <external-host>
port: 443
protocol: TLS
tls:
mode: Passthrough
allowedRoutes:
namespaces:
from: All
Validated edge shape

The validated EKS design used a dedicated Traefik release for the hardened hostname with a public LoadBalancer, only port 443 exposed, externalTrafficPolicy: Local, and TLSRoute support enabled.

Step 2: 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>

Step 3: Apply the validated hardened EKS cluster

Apply

Apply the Hardened-profile EKS manifest

yaml

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

configuration:
logLevel: "info"
ui: true
logging:
format: "json"
defaultLeaseTTL: "720h"
maxLeaseTTL: "8760h"
cacheSize: 134217728
disableCache: false
raft:
performanceMultiplier: 2

imageVerification:
enabled: true
failurePolicy: Block
operatorImageVerification:
enabled: true
failurePolicy: Block

tls:
enabled: true
mode: ACME
acme:
directoryURL: "https://acme-v02.api.letsencrypt.org/directory"
domains:
- "<external-host>"
email: "platform@example.com"
sharedCache:
mode: ManagedPVC
size: "1Gi"
storageClassName: <acme-cache-storage-class>

storage:
size: "10Gi"
storageClassName: gp3
deletionPolicy: DeleteAll

serviceAccount:
annotations:
eks.amazonaws.com/role-arn: "<main-role-arn>"

unseal:
type: awskms
awskms:
region: "<aws-region>"
kmsKeyID: "<kms-key-arn>"

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

gateway:
enabled: true
listenerName: websecure-passthrough
gatewayRef:
name: <gateway-name>
namespace: <gateway-namespace>
hostname: "<external-host>"
tlsPassthrough: true

backup:
schedule: "0 */6 * * *"
target:
provider: s3
endpoint: "https://s3.<aws-region>.amazonaws.com"
bucket: "<backup-bucket>"
pathPrefix: "clusters/<cluster-name>"
region: "<aws-region>"
roleArn: "<backup-role-arn>"
usePathStyle: false
retention:
maxCount: 7
maxAge: "168h"

upgrade:
preUpgradeSnapshot: true
strategy: RollingUpdate

network:
egressRules:
- to:
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
port: 443
Helper image defaults

For released operator builds, prefer the default operator-managed helper images or explicitly pin official signed helper images that match your operator release.

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, CloudUnsealIdentityReady=True, BackupConfigurationReady=True, ProductionReady=True, OpenBaoInitialized=True, and OpenBaoSealed=False.

Verify

Verify Gateway programming and the public certificate

bash

kubectl -n <gateway-namespace> get gateway <gateway-name> -o yaml
curl -I https://<external-host>

The Gateway should report Accepted=True and Programmed=True. The public endpoint should present a valid certificate and an OpenBao response code such as 307, 429, or another application-level reply.

Verify

Verify JWT admin login and trigger a manual backup

bash

JWT="$(kubectl -n <namespace> create token openbao-admin --audience openbao-internal --duration=1h)"

curl -sS \
-H 'Content-Type: application/json' \
-d "{\"role\":\"admin\",\"jwt\":\"${JWT}\"}" \
"https://<external-host>/v1/auth/jwt/login"

kubectl -n <namespace> annotate openbaocluster <cluster-name> \
openbao.org/trigger-backup="$(date -u +%Y-%m-%dT%H:%M:%SZ)" --overwrite

kubectl -n <namespace> get openbaocluster <cluster-name> \
-o jsonpath='{.status.backup.lastBackupName}{"\n"}{.status.backup.lastBackupTime}{"\n"}{.status.backup.lastFailureReason}{"\n"}{.status.backup.lastFailureMessage}{"\n"}'

Keep moving

Next release documentation

You are reading the unreleased main docs. Use the version menu for the newest published release, or check the release notes for what is already out.

Was this page helpful?

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