Reproduce the validated hardened EKS lane without diluting the public-edge and ACME assumptions it depends on.
This recipe applies the hardened cloud baseline with KMS auto-unseal, a dedicated public passthrough Gateway, OpenBao-managed ACME, JWT bootstrap, and S3 backups. Use it when you want the production-style EKS path the project has actually validated.
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
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.
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
| Assumption | Why it exists | What breaks if it is wrong |
|---|---|---|
| EKS has IRSA or an equivalent workload identity path enabled | Both KMS auto-unseal and S3 backups depend on cloud workload identity behavior. | The cluster can fail KMS or backup auth long before any OpenBao-specific logic becomes relevant. |
| A dedicated public passthrough Gateway exists for the hardened hostname | The 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 passthrough | OpenBao 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 cache | HA 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
| Placeholder | Example | Purpose |
|---|---|---|
<namespace> | openbaocluster-hardened | Tenant namespace for the cluster. |
<cluster-name> | openbaocluster-hardened | OpenBaoCluster name. |
<openbao-version> | 2.5.1 | OpenBao version. |
<aws-region> | eu-central-1 | AWS 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-unseal | IRSA role for the main OpenBao Pods. |
<backup-role-arn> | arn:aws:iam::...:role/openbao-backup | IRSA role for backup Jobs. |
<backup-bucket> | openbao-backups | S3 bucket for snapshots. |
<external-host> | bao.example.com | Public hostname for the hardened cluster. |
<gateway-name> | openbao-hardened-gateway | Dedicated passthrough Gateway. |
<gateway-namespace> | default | Namespace of the Gateway. |
<gateway-class-name> | traefik-passthrough | GatewayClass used by the dedicated passthrough edge. |
<acme-cache-storage-class> | efs-acme | RWX StorageClass for the shared ACME cache. |
<operator-namespace> | openbao-operator-system | Namespace 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
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
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
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
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
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
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
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
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
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.