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 docs for version 0.1.0. Use the version menu to switch to next or another archived 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.