From d5a5c7ec888bf9366c00da39c10b5c791b202c14 Mon Sep 17 00:00:00 2001 From: Shrinidhi Prabhu Date: Mon, 13 Oct 2025 13:07:07 +0530 Subject: [PATCH 1/7] added openbao module --- intents/openbao/facets.yaml | 5 + modules/openbao/default/1.0/README.md | 281 +++++++++++ modules/openbao/default/1.0/facets.yaml | 130 +++++ modules/openbao/default/1.0/main.tf | 596 +++++++++++++++++++++++ modules/openbao/default/1.0/outputs.tf | 20 + modules/openbao/default/1.0/variables.tf | 76 +++ outputs/openbao/output.facets.yaml | 55 +++ 7 files changed, 1163 insertions(+) create mode 100644 intents/openbao/facets.yaml create mode 100644 modules/openbao/default/1.0/README.md create mode 100644 modules/openbao/default/1.0/facets.yaml create mode 100644 modules/openbao/default/1.0/main.tf create mode 100644 modules/openbao/default/1.0/outputs.tf create mode 100644 modules/openbao/default/1.0/variables.tf create mode 100644 outputs/openbao/output.facets.yaml diff --git a/intents/openbao/facets.yaml b/intents/openbao/facets.yaml new file mode 100644 index 00000000..eb5a5290 --- /dev/null +++ b/intents/openbao/facets.yaml @@ -0,0 +1,5 @@ +name: openbao +type: Secrets +displayName: OpenBao +description: OpenBao is an open-source fork of HashiCorp Vault providing secure secrets management and data protection for cloud-native applications +iconUrl: https://openbao.org/assets/img/logo.svg diff --git a/modules/openbao/default/1.0/README.md b/modules/openbao/default/1.0/README.md new file mode 100644 index 00000000..62c49948 --- /dev/null +++ b/modules/openbao/default/1.0/README.md @@ -0,0 +1,281 @@ +# Auto-Unsealing OpenBao Cluster + +This Facets module deploys OpenBao with automatic initialization and unsealing capabilities using configurable key shares and threshold. The module uses Helm to deploy OpenBao and implements auto-unseal functionality through Terraform null resources that execute API calls against the OpenBao cluster. + +## Features + +- **Helm-based Deployment**: Uses the official Vault Helm chart (compatible with OpenBao) +- **Automatic Initialization**: Initializes OpenBao with configurable key shares and threshold +- **Auto-Unseal**: Automatically unseals OpenBao using stored unseal keys +- **Secure Key Storage**: Stores unseal keys and root token in Kubernetes secrets +- **Flexible Storage Backends**: Supports file, Raft, and Azure storage backends +- **High Availability**: Configurable replicas with Raft consensus +- **TLS Support**: Optional TLS configuration with multiple certificate sources +- **UI Access**: Optional web UI with ingress support +- **Resource Management**: Configurable CPU/memory requests and limits + +## Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Helm Release │ │ Null Resource │ │ Null Resource │ +│ (OpenBao) │───▶│ (Initialize) │───▶│ (Auto-Unseal) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ OpenBao Pods │ │ K8s Secrets │ │ Unsealed │ +│ │ │ - Unseal Keys │ │ OpenBao │ +│ │ │ - Root Token │ │ Ready for Use │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +## Usage + +### Basic Configuration + +```yaml +kind: openbao-cluster +flavor: auto-unseal +version: "1.0" +spec: + namespace: "openbao" + release_name: "my-openbao" + key_shares: 5 + key_threshold: 3 + storage_backend: + type: "raft" +``` + +### Advanced Configuration + +```yaml +kind: openbao-cluster +flavor: auto-unseal +version: "1.0" +spec: + namespace: "openbao-prod" + release_name: "openbao-ha" + key_shares: 7 + key_threshold: 4 + + # High availability setup + server_replicas: 3 + storage_backend: + type: "raft" + config: + retry_join: + - "openbao-ha-0.openbao-ha-internal:8201" + - "openbao-ha-1.openbao-ha-internal:8201" + - "openbao-ha-2.openbao-ha-internal:8201" + + # Resource configuration + server_resources: + requests: + cpu: "1000m" + memory: "512Mi" + limits: + cpu: "2000m" + memory: "1Gi" + + # TLS and UI + tls_config: + enabled: true + cert_source: "cert-manager" + ui_enabled: true + ingress_enabled: true + + # Custom timeout + init_timeout: "600s" +``` + +## Configuration Reference + +### Required Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `key_shares` | integer | Number of key shares to generate (1-255) | +| `key_threshold` | integer | Number of shares required to unseal (1-255, ≤ key_shares) | +| `namespace` | string | Kubernetes namespace (created automatically) | +| `release_name` | string | Helm release name | +| `storage_backend` | object | Storage backend configuration | + +### Optional Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `helm_chart_version` | string | "0.28.1" | Vault Helm chart version | +| `helm_repository` | string | "https://helm.releases.hashicorp.com" | Helm repository URL | +| `secret_name_prefix` | string | "openbao" | Prefix for secrets | +| `server_replicas` | integer | 1 | Number of OpenBao replicas | +| `server_resources` | object | See defaults | CPU/memory requests and limits | +| `ui_enabled` | boolean | true | Enable OpenBao web UI | +| `ingress_enabled` | boolean | false | Create ingress for UI | +| `tls_config` | object | See TLS section | TLS configuration | +| `init_timeout` | string | "300s" | Initialization timeout | + +### Storage Backend Options + +#### Raft (Recommended for HA) +```yaml +storage_backend: + type: "raft" + config: + retry_join: + - "pod-0.service:8201" + - "pod-1.service:8201" +``` + +#### File (Single Instance) +```yaml +storage_backend: + type: "file" + config: + path: "/openbao/data" +``` + +#### Azure Key Vault +```yaml +storage_backend: + type: "azure" + config: + tenant_id: "your-tenant-id" + client_id: "your-client-id" + client_secret: "your-secret" + vault_name: "your-keyvault" +``` + +### TLS Configuration + +```yaml +tls_config: + enabled: true + cert_source: "self-signed" # or "cert-manager" or "manual" +``` + +## Outputs + +The module provides the following outputs: + +### Default Output +- **Connection details**: Service URLs, namespace, release name +- **UI access**: UI URL if enabled +- **Ingress details**: Ingress host if enabled +- **Secrets**: Names of Kubernetes secrets containing keys and tokens +- **Configuration**: Deployment configuration details +- **Health check**: Health check endpoint URL + +### Additional Outputs +- `root_token_secret_name`: Name of secret containing root token +- `unseal_keys_secret_name`: Name of secret containing unseal keys +- `namespace`: Deployment namespace +- `service_details`: Service connection details for other apps +- `helm_release`: Helm release information + +## Security Considerations + +### Unseal Keys Storage +- Unseal keys are stored in Kubernetes secrets with base64 encoding +- Secrets are labeled for easy identification and management +- Consider implementing additional encryption for secrets at rest + +### Root Token Protection +- Root token is stored in a separate Kubernetes secret +- Rotate the root token regularly using OpenBao's token rotation features +- Limit access to the root token secret using RBAC + +### Network Security +- Use TLS for all communications when possible +- Configure network policies to restrict pod-to-pod communication +- Use ingress with proper authentication for UI access + +## Operations + +### Accessing OpenBao +```bash +# Get the root token +kubectl get secret openbao-root-token -n openbao -o jsonpath='{.data.root-token}' | base64 -d + +# Port forward to access UI +kubectl port-forward svc/openbao-vault -n openbao 8200:8200 + +# Access via browser +open http://localhost:8200/ui +``` + +### Manual Unseal (if needed) +```bash +# Get unseal keys +kubectl get secret openbao-init-keys -n openbao -o jsonpath='{.data.unseal-keys}' | base64 -d | base64 -d + +# Unseal manually (if auto-unseal fails) +kubectl exec -n openbao openbao-vault-0 -- openbao operator unseal +``` + +### Backup and Recovery +- Regularly backup the Kubernetes secrets containing unseal keys +- Consider using Velero or similar tools for cluster-level backups +- Test recovery procedures in non-production environments + +## Troubleshooting + +### Common Issues + +1. **Initialization Timeout** + - Increase `init_timeout` value + - Check pod logs: `kubectl logs -n ` + - Verify network connectivity + +2. **Unseal Failures** + - Check if secrets exist and contain valid keys + - Verify key threshold is not greater than available keys + - Check OpenBao pod status and logs + +3. **Helm Deployment Issues** + - Verify Helm repository is accessible + - Check chart version compatibility + - Review Helm release status: `helm status -n ` + +### Debugging Commands +```bash +# Check OpenBao status +kubectl exec -n -- openbao status + +# View initialization logs +kubectl logs -n | grep init + +# Check secrets +kubectl get secrets -n | grep openbao + +# Describe Helm release +helm get values -n +``` + +## Prerequisites + +- Kubernetes cluster with RBAC enabled +- Helm 3.x installed and configured +- `kubectl` access to the target cluster +- `jq` utility for JSON processing (required for auto-unseal scripts) + +## Limitations + +- Currently supports single-cluster deployments only +- Auto-unseal requires `kubectl` and `jq` to be available in Terraform execution environment +- Raft storage backend requires persistent volumes +- TLS certificate management is basic (consider cert-manager for production) + +## Contributing + +This module follows Facets module development standards. When contributing: + +1. Maintain backward compatibility +2. Update documentation for any new features +3. Add appropriate validation rules +4. Test with different storage backends +5. Follow Terraform best practices + +## License + +This module is part of the Facets platform and follows the same licensing terms. \ No newline at end of file diff --git a/modules/openbao/default/1.0/facets.yaml b/modules/openbao/default/1.0/facets.yaml new file mode 100644 index 00000000..39c889af --- /dev/null +++ b/modules/openbao/default/1.0/facets.yaml @@ -0,0 +1,130 @@ +intent: openbao +flavor: default +version: '1.0' +clouds: +- azure +description: Deploys OpenBao with static seal auto-unseal for Kubernetes +metadata: + title: Metadata of service + type: object + properties: + namespace: + type: string + title: Kubernetes Namespace + description: Kubernetes namespace to deploy OpenBao (will be created if it doesn't + exist) + default: default + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ +spec: + title: Auto-Unsealing OpenBao Cluster + description: Deploys OpenBao with static seal auto-unseal - pods auto-unseal on + restart + type: object + properties: + release_name: + type: string + title: Helm Release Name + description: Helm release name for the OpenBao deployment + default: openbao + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + chart_version: + type: string + title: Helm Chart Version + description: Version of the OpenBao Helm chart to deploy + default: 0.18.4 + server_replicas: + type: integer + title: Server Replicas + description: Number of OpenBao server replicas + default: 1 + minimum: 1 + maximum: 10 + server_resources: + type: object + title: Server Resources + description: CPU and memory resource requests/limits for OpenBao servers + properties: + requests: + type: object + properties: + cpu: + type: string + default: 500m + memory: + type: string + default: 256Mi + limits: + type: object + properties: + cpu: + type: string + default: 1000m + memory: + type: string + default: 512Mi + ui_enabled: + type: boolean + title: Enable UI + description: Whether to enable OpenBao web UI + default: true + storage_type: + type: string + title: Storage Backend + description: Storage backend for OpenBao (raft for HA, file for standalone) + default: raft + enum: + - file + - raft + storage_size: + type: string + title: PVC Storage Size + description: Size of persistent volume for each OpenBao server + default: 10Gi + pvc_labels: + type: object + title: PVC Labels + description: Additional labels to apply to PersistentVolumeClaims + default: {} + openbao: + type: object + title: Advanced OpenBao Configuration + description: Advanced configuration options for OpenBao Helm chart + properties: + values: + type: object + title: Custom Helm Values + description: Additional Helm values to merge with constructed values + default: {} + x-ui-editor: true + required: + - namespace + - release_name + - storage_type +inputs: + kubernetes_cluster: + type: '@outputs/kubernetes' + optional: false + displayName: Kubernetes Cluster + description: Kubernetes cluster to deploy OpenBao + providers: + - helm + - kubernetes +outputs: + default: + type: '@outputs/openbao_cluster' + title: OpenBao Cluster + description: OpenBao cluster connection information +iac: + validated_files: + - main.tf + - variables.tf + - outputs.tf +sample: + kind: openbao + flavor: default + version: '1.0' + disabled: true + spec: + namespace: default + release_name: openbao + storage_type: raft diff --git a/modules/openbao/default/1.0/main.tf b/modules/openbao/default/1.0/main.tf new file mode 100644 index 00000000..64e57bca --- /dev/null +++ b/modules/openbao/default/1.0/main.tf @@ -0,0 +1,596 @@ +terraform { + required_providers { + helm = { + source = "hashicorp/helm" + version = "~> 2.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.0" + } + } +} + +# Local values for common configurations +locals { + metadata = lookup(var.instance, "metadata", {}) + spec = lookup(var.instance, "spec", {}) + openbao = lookup(local.spec, "openbao", {}) + namespace = lookup(local.metadata, "namespace", "default") + release_name = lookup(local.spec, "release_name", "openbao") + + # Hardcoded chart details + chart_repo = "https://openbao.github.io/openbao-helm" + chart_name = "openbao" + chart_version = lookup(local.spec, "chart_version", "0.18.4") + + # Generate unique resource names + unseal_secret_name = "${local.release_name}-unseal-key" + + # Common labels + labels = merge(var.environment.cloud_tags, { + "app.kubernetes.io/name" = "openbao" + "app.kubernetes.io/instance" = var.instance_name + "app.kubernetes.io/managed-by" = "facets" + }) + + # Determine HA mode based on storage type + is_ha_mode = lookup(local.spec, "storage_type", "raft") == "raft" + + # OpenBao configuration with static seal for HA mode + openbao_config_ha = <<-EOF + disable_mlock = true + + ui = ${lookup(local.spec, "ui_enabled", true)} + + seal "static" { + current_key_id = "openbao-unseal-key-v1" + current_key = "env://OPENBAO_UNSEAL_KEY" + } + + listener "tcp" { + tls_disable = true + address = "[::]:8200" + cluster_address = "[::]:8201" + } + + storage "raft" { + path = "/openbao/data" + } + + cluster_addr = "http://POD_IP:8201" + api_addr = "http://POD_IP:8200" + + log_level = "info" + EOF + + # OpenBao configuration with static seal for standalone mode + openbao_config_standalone = <<-EOF + disable_mlock = true + + ui = ${lookup(local.spec, "ui_enabled", true)} + + seal "static" { + current_key_id = "openbao-unseal-key-v1" + current_key = "env://OPENBAO_UNSEAL_KEY" + } + + listener "tcp" { + tls_disable = true + address = "[::]:8200" + } + + storage "file" { + path = "/openbao/data" + } + + log_level = "info" + EOF + + # Helm values configuration + constructed_helm_values = { + global = { + enabled = true + } + + server = { + enabled = true + + replicas = lookup(local.spec, "server_replicas", 1) + + resources = { + requests = lookup(lookup(local.spec, "server_resources", {}), "requests", {}) + limits = lookup(lookup(local.spec, "server_resources", {}), "limits", {}) + } + + tolerations = lookup(var.environment, "default_tolerations", [{ + key = "kubernetes.azure.com/scalesetpriority" + value = "spot" + operator = "Equal" + effect = "NoSchedule" + }]) + + dataStorage = { + enabled = true + } + + auditStorage = { + enabled = false + } + + extraSecretEnvironmentVars = [ + { + envName = "OPENBAO_UNSEAL_KEY" + secretName = local.unseal_secret_name + secretKey = "unseal-key" + } + ] + + standalone = { + enabled = !local.is_ha_mode + config = local.openbao_config_standalone + } + + ha = { + enabled = local.is_ha_mode + replicas = lookup(local.spec, "server_replicas", 1) + raft = { + enabled = local.is_ha_mode + setNodeId = local.is_ha_mode + config = local.openbao_config_ha + } + } + + service = { + enabled = true + type = "ClusterIP" + port = 8200 + } + + serviceAccount = { + create = true + name = "${local.release_name}-sa" + } + + readinessProbe = { + enabled = true + path = "/v1/sys/health?standbyok=true&sealedcode=204&uninitcode=204" + } + + livenessProbe = { + enabled = true + path = "/v1/sys/health?standbyok=true" + initialDelaySeconds = 60 + } + } + + ui = { + enabled = lookup(local.spec, "ui_enabled", true) + serviceType = "ClusterIP" + } + + injector = { + enabled = false + tolerations = lookup(var.environment, "default_tolerations", [{ + key = "kubernetes.azure.com/scalesetpriority" + value = "spot" + operator = "Equal" + effect = "NoSchedule" + }]) + } + } + user_defined_helm_values = lookup(local.openbao, "values", {}) + + # Hardcoded service account names for OpenBao policies + control_plane_sa_name = "control-plane-service-sa" + facets_release_sa_name = "facets-release-pod" +} + +# Generate a random 32-byte key for static seal +resource "random_id" "unseal_key" { + byte_length = 32 + + lifecycle { + ignore_changes = [byte_length] + } +} + +# Create Kubernetes secret to store the unseal key +resource "kubernetes_secret" "unseal_key" { + metadata { + name = local.unseal_secret_name + namespace = local.namespace + labels = local.labels + } + + data = { + unseal-key = random_id.unseal_key.b64_std + } + + type = "Opaque" + + lifecycle { + prevent_destroy = true + } +} + +# Create PVC for server +module "openbao_pvc" { + count = lookup(local.spec, "server_replicas", 1) + source = "github.com/Facets-cloud/facets-utility-modules//pvc" + name = "data-${local.release_name}-${count.index}" + namespace = local.namespace + provisioned_for = "${local.release_name}-${count.index}" + instance_name = local.release_name + volume_size = lookup(local.spec, "storage_size", "10Gi") + access_modes = ["ReadWriteOnce"] + kind = "openbao" + additional_labels = lookup(local.spec, "pvc_lables", {}) +} + +# Deploy OpenBao using Helm +resource "helm_release" "openbao" { + name = local.release_name + repository = local.chart_repo + chart = local.chart_name + version = local.chart_version + namespace = local.namespace + + create_namespace = true + + values = [yamlencode(local.constructed_helm_values), yamlencode(local.user_defined_helm_values)] + + timeout = 600 + + depends_on = [kubernetes_secret.unseal_key, module.openbao_pvc] + + lifecycle { + create_before_destroy = true + } +} + +# Create ServiceAccount for the init job +resource "kubernetes_service_account" "openbao_init" { + metadata { + name = "${local.release_name}-init-sa" + namespace = local.namespace + labels = local.labels + } + + depends_on = [helm_release.openbao] +} + +# Create Role with permissions to exec into pods and manage secrets +resource "kubernetes_role" "openbao_init" { + metadata { + name = "${local.release_name}-init-role" + namespace = local.namespace + labels = local.labels + } + + rule { + api_groups = [""] + resources = ["pods", "pods/exec"] + verbs = ["get", "list", "create"] + } + + rule { + api_groups = [""] + resources = ["secrets"] + verbs = ["create", "get", "update", "patch"] + } + + rule { + api_groups = ["apps"] + resources = ["statefulsets"] + verbs = ["get", "list"] + } + + depends_on = [helm_release.openbao] +} + +# Bind the role to the service account +resource "kubernetes_role_binding" "openbao_init" { + metadata { + name = "${local.release_name}-init-binding" + namespace = local.namespace + labels = local.labels + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "Role" + name = kubernetes_role.openbao_init.metadata[0].name + } + + subject { + kind = "ServiceAccount" + name = kubernetes_service_account.openbao_init.metadata[0].name + namespace = local.namespace + } +} + +# Kubernetes Job to initialize and maintain OpenBao cluster +# Job is recreated whenever replicas change to join new nodes +resource "kubernetes_job_v1" "openbao_init" { + metadata { + name = "${local.release_name}-init-${md5("${lookup(local.spec, "server_replicas", 1)}")}" + namespace = local.namespace + labels = local.labels + } + + spec { + template { + metadata { + labels = merge(local.labels, { + "app" = "${local.release_name}-init" + }) + } + + spec { + service_account_name = kubernetes_service_account.openbao_init.metadata[0].name + restart_policy = "Never" + + dynamic "toleration" { + for_each = lookup(var.environment, "default_tolerations", [{ + key = "kubernetes.azure.com/scalesetpriority" + value = "spot" + operator = "Equal" + effect = "NoSchedule" + }]) + + content { + key = toleration.value.key + operator = toleration.value.operator + value = toleration.value.value + effect = toleration.value.effect + } + } + + container { + name = "openbao-init" + image = "alpine/k8s:1.28.3" + + command = ["/bin/bash", "-c"] + args = [<<-EOF + set -e + + echo "=== OpenBao Auto-Init and Raft Join Script ===" + echo "Checking for OpenBao pods..." + + # Wait for at least one OpenBao pod + kubectl wait --for=condition=ready pod/${local.release_name}-0 -n ${local.namespace} --timeout=300s || { + echo "ERROR: openbao-0 pod not ready" + exit 1 + } + + POD_NAME="${local.release_name}-0" + echo "Found leader pod: $POD_NAME" + + # Check if already initialized + echo "Checking if OpenBao leader is initialized..." + if kubectl exec -n ${local.namespace} $POD_NAME -- bao status 2>&1 | grep -q "Initialized.*true"; then + echo "OpenBao is already initialized." + + # Retrieve root token from existing secret + if kubectl get secret ${local.release_name}-init-keys -n ${local.namespace} &>/dev/null; then + echo "Retrieving root token from existing secret..." + ROOT_TOKEN=$(kubectl get secret ${local.release_name}-init-keys -n ${local.namespace} -o jsonpath='{.data.root-token}' | base64 -d) + else + echo "ERROR: OpenBao is initialized but root token secret not found!" + exit 1 + fi + else + echo "Initializing OpenBao with auto-unseal (static seal)..." + INIT_OUTPUT=$(kubectl exec -n ${local.namespace} $POD_NAME -- bao operator init -format=json) + + echo "OpenBao initialized successfully!" + + # Extract recovery keys and root token using jq + ROOT_TOKEN=$(echo "$INIT_OUTPUT" | jq -r '.root_token') + RECOVERY_KEYS=$(echo "$INIT_OUTPUT" | jq -r '.recovery_keys_b64 | @json') + + # Store in Kubernetes Secret + kubectl create secret generic ${local.release_name}-init-keys -n ${local.namespace} \ + --from-literal=root-token="$ROOT_TOKEN" \ + --from-literal=recovery-keys="$RECOVERY_KEYS" \ + --dry-run=client -o yaml | kubectl apply -f - + + echo "Recovery keys and root token stored in secret '${local.release_name}-init-keys'" + echo "" + echo "=== IMPORTANT: Save these credentials securely ===" + echo "Root Token: $ROOT_TOKEN" + echo "Recovery Keys: $RECOVERY_KEYS" + echo "==================================================" + echo "" + echo "OpenBao is now initialized and auto-unsealed via static seal!" + fi + + # Join additional Raft nodes if running in HA mode + echo "" + echo "=== Raft Cluster Setup ===" + REPLICA_COUNT=$(kubectl get statefulset ${local.release_name} -n ${local.namespace} -o jsonpath='{.spec.replicas}' 2>/dev/null || echo "1") + echo "Detected replica count: $REPLICA_COUNT" + + if [ "$REPLICA_COUNT" -gt 1 ]; then + echo "HA mode enabled. Processing additional nodes..." + + # Join all non-leader nodes to the cluster + for i in $(seq 1 $(($REPLICA_COUNT - 1))); do + POD="${local.release_name}-$i" + echo "" + echo "Processing $POD..." + + # Wait for pod to be running (allow time for node provisioning if needed) + echo "Waiting for $POD to be ready (timeout: 5 minutes)..." + kubectl wait --for=condition=ready pod/$POD -n ${local.namespace} --timeout=300s || { + echo "Warning: $POD not ready after 5 minutes, will retry on next run" + continue + } + + # Check current status + POD_STATUS=$(kubectl exec -n ${local.namespace} $POD -- bao status 2>&1 || true) + + if echo "$POD_STATUS" | grep -q "Initialized.*true"; then + echo "$POD is already initialized and part of cluster" + continue + fi + + # Node is uninitialized, join it to the cluster + echo "$POD is uninitialized. Joining to Raft cluster..." + if kubectl exec -n ${local.namespace} $POD -- bao operator raft join \ + http://${local.release_name}-0.${local.release_name}-internal.${local.namespace}.svc.cluster.local:8200; then + echo "Successfully joined $POD to cluster" + + # Give it a moment to auto-unseal with static seal + sleep 3 + + # Verify it unsealed + NEW_STATUS=$(kubectl exec -n ${local.namespace} $POD -- bao status 2>&1 || true) + if echo "$NEW_STATUS" | grep -q "Sealed.*false"; then + echo "$POD is now unsealed and operational" + else + echo "Warning: $POD joined but may need time to unseal" + fi + else + echo "Failed to join $POD - will retry on next run" + fi + done + + echo "" + echo "Raft cluster setup complete!" + else + echo "Single replica deployment - no additional nodes to join" + fi + + # Configure OpenBao policies and auth + echo "" + echo "Configuring OpenBao policies and Kubernetes auth..." + + # Enable Kubernetes auth method + echo "Enabling Kubernetes auth method..." + kubectl exec -n ${local.namespace} $POD_NAME -- env BAO_TOKEN="$ROOT_TOKEN" bao auth enable kubernetes || echo "Kubernetes auth already enabled" + + # Configure Kubernetes auth backend + echo "Configuring Kubernetes auth backend..." + kubectl exec -n ${local.namespace} $POD_NAME -- sh -c " + export BAO_TOKEN='$ROOT_TOKEN' + bao write auth/kubernetes/config \ + kubernetes_host='https://kubernetes.default.svc:443' \ + kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \ + token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token + " + + # Create read-write policy for control-plane-service-sa + echo "Creating read-write policy for control-plane-service-sa..." + kubectl exec -n ${local.namespace} $POD_NAME -- sh -c " + export BAO_TOKEN='$ROOT_TOKEN' + bao policy write control-plane-rw - <<'POLICY' +# Full read-write access to all secrets +path \"secret/*\" { + capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\"] +} + +path \"secret/data/*\" { + capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\"] +} + +path \"secret/metadata/*\" { + capabilities = [\"list\", \"read\", \"delete\"] +} + +# System health check +path \"sys/health\" { + capabilities = [\"read\"] +} +POLICY + " + + # Create read-only policy for facets-release-pod + echo "Creating read-only policy for facets-release-pod..." + kubectl exec -n ${local.namespace} $POD_NAME -- sh -c " + export BAO_TOKEN='$ROOT_TOKEN' + bao policy write facets-release-readonly - <<'POLICY' +# Read-only access to all secrets +path \"secret/*\" { + capabilities = [\"read\", \"list\"] +} + +path \"secret/data/*\" { + capabilities = [\"read\", \"list\"] +} + +path \"secret/metadata/*\" { + capabilities = [\"read\", \"list\"] +} + +# System health check +path \"sys/health\" { + capabilities = [\"read\"] +} +POLICY + " + + # Create auth role for control-plane-service-sa + echo "Creating auth role for control-plane-service-sa..." + kubectl exec -n ${local.namespace} $POD_NAME -- env BAO_TOKEN="$ROOT_TOKEN" bao write auth/kubernetes/role/control-plane-role \ + bound_service_account_names=${local.control_plane_sa_name} \ + bound_service_account_namespaces='*' \ + policies=control-plane-rw \ + ttl=72h + + # Create auth role for facets-release-pod + echo "Creating auth role for facets-release-pod..." + kubectl exec -n ${local.namespace} $POD_NAME -- env BAO_TOKEN="$ROOT_TOKEN" bao write auth/kubernetes/role/facets-release-role \ + bound_service_account_names=${local.facets_release_sa_name} \ + bound_service_account_namespaces='*' \ + policies=facets-release-readonly \ + ttl=72h + + echo "" + echo "OpenBao configuration complete!" + echo "- Policies created: control-plane-rw, facets-release-readonly" + echo "- Auth roles created: control-plane-role, facets-release-role" + EOF + ] + + env { + name = "BAO_ADDR" + value = "http://${local.release_name}-openbao.${local.namespace}.svc.cluster.local:8200" + } + } + } + } + + backoff_limit = 3 + } + + wait_for_completion = false + + timeouts { + create = "10m" + } + + depends_on = [ + helm_release.openbao, + kubernetes_role_binding.openbao_init + ] + + lifecycle { + replace_triggered_by = [helm_release.openbao] + } +} + +# Data source to read the init keys secret (for outputs) +data "kubernetes_secret_v1" "openbao_init_keys" { + metadata { + name = "${local.release_name}-init-keys" + namespace = local.namespace + } + + depends_on = [kubernetes_job_v1.openbao_init] +} diff --git a/modules/openbao/default/1.0/outputs.tf b/modules/openbao/default/1.0/outputs.tf new file mode 100644 index 00000000..523423b7 --- /dev/null +++ b/modules/openbao/default/1.0/outputs.tf @@ -0,0 +1,20 @@ +locals { + output_attributes = { + namespace = lookup(local.metadata, "namespace", "default") + release_name = lookup(local.spec, "release_name", "openbao") + service_name = "${lookup(local.spec, "release_name", "openbao")}-openbao" + service_url = "http://${lookup(local.spec, "release_name", "openbao")}-openbao.${lookup(local.metadata, "namespace", "default")}.svc.cluster.local:8200" + ui_enabled = lookup(local.spec, "ui_enabled", true) + ui_url = lookup(local.spec, "ui_enabled", true) ? "http://${lookup(local.spec, "release_name", "openbao")}-openbao.${lookup(local.metadata, "namespace", "default")}.svc.cluster.local:8200/ui" : null + health_check_url = "http://${lookup(local.spec, "release_name", "openbao")}-openbao.${lookup(local.metadata, "namespace", "default")}.svc.cluster.local:8200/v1/sys/health" + unseal_secret_name = "${lookup(local.spec, "release_name", "openbao")}-unseal-key" + init_keys_secret_name = "${lookup(local.spec, "release_name", "openbao")}-init-keys" + storage_type = lookup(local.spec, "storage_type", "raft") + server_replicas = lookup(local.spec, "server_replicas", 1) + auto_unseal_enabled = true + root_token = sensitive(try(data.kubernetes_secret_v1.openbao_init_keys.data["root-token"], "")) + recovery_keys = sensitive(try(data.kubernetes_secret_v1.openbao_init_keys.data["recovery-keys"], "")) + secrets = ["root_token", "recovery_keys"] + } + output_interfaces = {} +} \ No newline at end of file diff --git a/modules/openbao/default/1.0/variables.tf b/modules/openbao/default/1.0/variables.tf new file mode 100644 index 00000000..93640b55 --- /dev/null +++ b/modules/openbao/default/1.0/variables.tf @@ -0,0 +1,76 @@ +# Standard Facets module variables +variable "instance_name" { + description = "The unique name for this module instance" + type = string +} + +variable "instance" { + description = "The instance configuration" + type = object({ + kind = string + flavor = string + version = string + metadata = optional(object({ + namespace = optional(string, "default") + }), {}) + spec = object({ + namespace = optional(string, "default") + release_name = optional(string, "openbao") + chart_version = optional(string, "0.18.4") + server_replicas = optional(number, 1) + server_resources = optional(object({ + requests = optional(object({ + cpu = optional(string, "500m") + memory = optional(string, "256Mi") + }), {}) + limits = optional(object({ + cpu = optional(string, "1000m") + memory = optional(string, "512Mi") + }), {}) + }), {}) + ui_enabled = optional(bool, true) + storage_type = optional(string, "raft") + storage_size = optional(string, "10Gi") + pvc_labels = optional(map(string), {}) + controlplane_sa_name = optional(string, "control-plane-service-sa") + facets_release_sa_name = optional(string, "facets-release-pod") + openbao = optional(object({ + values = optional(map(any), {}) + }), {}) + }) + }) + + validation { + condition = var.instance.spec.server_replicas >= 1 && var.instance.spec.server_replicas <= 10 + error_message = "Server replicas must be between 1 and 10." + } + + validation { + condition = contains(["file", "raft"], var.instance.spec.storage_type) + error_message = "Storage type must be one of: file, raft." + } + + validation { + condition = can(regex("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", var.instance.spec.namespace)) + error_message = "Namespace must match pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + } + + validation { + condition = can(regex("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", var.instance.spec.release_name)) + error_message = "Release name must match pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + } +} + +variable "environment" { + description = "Environment configuration" + type = object({ + unique_name = string + cloud_tags = map(string) + }) +} + +variable "inputs" { + description = "Inputs from other modules" + type = map(any) + default = {} +} diff --git a/outputs/openbao/output.facets.yaml b/outputs/openbao/output.facets.yaml new file mode 100644 index 00000000..6fe029e0 --- /dev/null +++ b/outputs/openbao/output.facets.yaml @@ -0,0 +1,55 @@ +name: openbao +out: + type: object + title: OpenBao Cluster Outputs + description: Outputs from the OpenBao cluster module including connection details and credentials + properties: + attributes: + type: object + description: Attributes of the OpenBao cluster outputs + properties: + namespace: + type: string + description: Kubernetes namespace where OpenBao is deployed + release_name: + type: string + description: Helm release name for the OpenBao deployment + service_name: + type: string + description: Kubernetes service name for OpenBao + service_url: + type: string + description: Internal service URL for OpenBao API access + ui_enabled: + type: boolean + description: Whether the OpenBao web UI is enabled + ui_url: + type: string + description: URL for accessing the OpenBao web UI (null if UI disabled) + health_check_url: + type: string + description: Health check endpoint URL for OpenBao + unseal_secret_name: + type: string + description: Name of the Kubernetes secret containing the unseal key + init_keys_secret_name: + type: string + description: Name of the Kubernetes secret containing initialization keys and root token + storage_type: + type: string + description: Storage backend type (raft or file) + server_replicas: + type: integer + description: Number of OpenBao server replicas + root_token: + type: string + description: Root token for OpenBao (sensitive) + recovery_keys: + type: string + description: Recovery keys for OpenBao (sensitive) + secrets: + type: array + description: List of sensitive field names + items: + type: string + interfaces: {} From 6be7b5638c7c69bccaa62ad345481d7efdd6bd55 Mon Sep 17 00:00:00 2001 From: Shrinidhi-59 Date: Mon, 13 Oct 2025 13:51:15 +0530 Subject: [PATCH 2/7] removed tokens from init job logs --- modules/openbao/default/1.0/main.tf | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/modules/openbao/default/1.0/main.tf b/modules/openbao/default/1.0/main.tf index 64e57bca..f0f86e4c 100644 --- a/modules/openbao/default/1.0/main.tf +++ b/modules/openbao/default/1.0/main.tf @@ -401,12 +401,7 @@ resource "kubernetes_job_v1" "openbao_init" { --dry-run=client -o yaml | kubectl apply -f - echo "Recovery keys and root token stored in secret '${local.release_name}-init-keys'" - echo "" - echo "=== IMPORTANT: Save these credentials securely ===" - echo "Root Token: $ROOT_TOKEN" - echo "Recovery Keys: $RECOVERY_KEYS" - echo "==================================================" - echo "" + echo "OpenBao is now initialized and auto-unsealed via static seal!" fi From ccacc8b60339714f988efaecd79a90c34f225503 Mon Sep 17 00:00:00 2001 From: Shrinidhi-59 Date: Mon, 13 Oct 2025 17:45:21 +0530 Subject: [PATCH 3/7] updated openbao module to enable secret engine --- modules/openbao/default/1.0/facets.yaml | 2 +- modules/openbao/default/1.0/main.tf | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/modules/openbao/default/1.0/facets.yaml b/modules/openbao/default/1.0/facets.yaml index 39c889af..54e12a4e 100644 --- a/modules/openbao/default/1.0/facets.yaml +++ b/modules/openbao/default/1.0/facets.yaml @@ -111,7 +111,7 @@ inputs: - kubernetes outputs: default: - type: '@outputs/openbao_cluster' + type: '@outputs/openbao' title: OpenBao Cluster description: OpenBao cluster connection information iac: diff --git a/modules/openbao/default/1.0/main.tf b/modules/openbao/default/1.0/main.tf index f0f86e4c..7dad4202 100644 --- a/modules/openbao/default/1.0/main.tf +++ b/modules/openbao/default/1.0/main.tf @@ -62,8 +62,8 @@ locals { path = "/openbao/data" } - cluster_addr = "http://POD_IP:8201" - api_addr = "http://POD_IP:8200" + cluster_addr = "http://$(POD_IP):8201" + api_addr = "http://$(POD_IP):8200" log_level = "info" EOF @@ -229,7 +229,7 @@ module "openbao_pvc" { volume_size = lookup(local.spec, "storage_size", "10Gi") access_modes = ["ReadWriteOnce"] kind = "openbao" - additional_labels = lookup(local.spec, "pvc_lables", {}) + additional_labels = lookup(local.spec, "pvc_labels", {}) } # Deploy OpenBao using Helm @@ -480,6 +480,10 @@ resource "kubernetes_job_v1" "openbao_init" { token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token " + # Enable KV v2 secrets engine at 'secret/' path + echo "Enabling KV v2 secrets engine at secret/ path..." + kubectl exec -n ${local.namespace} $POD_NAME -- env BAO_TOKEN="$ROOT_TOKEN" bao secrets enable -path=secret -version=2 kv || echo "KV v2 secrets engine already enabled at secret/" + # Create read-write policy for control-plane-service-sa echo "Creating read-write policy for control-plane-service-sa..." kubectl exec -n ${local.namespace} $POD_NAME -- sh -c " @@ -564,10 +568,10 @@ POLICY backoff_limit = 3 } - wait_for_completion = false + wait_for_completion = true timeouts { - create = "10m" + create = "15m" } depends_on = [ From 4379f6bc3c5011babfccc10bd88f18b3241448ab Mon Sep 17 00:00:00 2001 From: Shrinidhi-59 Date: Wed, 15 Oct 2025 22:22:34 +0530 Subject: [PATCH 4/7] fixed init job to add issuer --- modules/openbao/default/1.0/main.tf | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/modules/openbao/default/1.0/main.tf b/modules/openbao/default/1.0/main.tf index 7dad4202..9861243c 100644 --- a/modules/openbao/default/1.0/main.tf +++ b/modules/openbao/default/1.0/main.tf @@ -470,14 +470,39 @@ resource "kubernetes_job_v1" "openbao_init" { echo "Enabling Kubernetes auth method..." kubectl exec -n ${local.namespace} $POD_NAME -- env BAO_TOKEN="$ROOT_TOKEN" bao auth enable kubernetes || echo "Kubernetes auth already enabled" - # Configure Kubernetes auth backend + # Detect Kubernetes issuer from JWT token + echo "Detecting Kubernetes issuer from JWT token..." + TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) + PAYLOAD=$(echo "$TOKEN" | cut -d. -f2) + + # Add padding for base64 decoding if needed + MOD=$(($(echo -n "$PAYLOAD" | wc -c) % 4)) + if [ $MOD -eq 2 ]; then + PAYLOAD="$${PAYLOAD}==" + elif [ $MOD -eq 3 ]; then + PAYLOAD="$${PAYLOAD}=" + fi + + # Extract issuer from JWT payload + ISSUER=$(echo "$PAYLOAD" | base64 -d 2>/dev/null | grep -o '"iss":"[^"]*"' | cut -d'"' -f4) + + if [ -z "$ISSUER" ]; then + echo "Warning: Could not detect issuer from JWT token, using default configuration" + ISSUER_PARAM="" + else + echo "Detected issuer: $ISSUER" + ISSUER_PARAM="issuer='$ISSUER'" + fi + + # Configure Kubernetes auth backend with auto-detected issuer echo "Configuring Kubernetes auth backend..." kubectl exec -n ${local.namespace} $POD_NAME -- sh -c " export BAO_TOKEN='$ROOT_TOKEN' bao write auth/kubernetes/config \ kubernetes_host='https://kubernetes.default.svc:443' \ kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \ - token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token + token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token \ + $ISSUER_PARAM " # Enable KV v2 secrets engine at 'secret/' path From 8e8afb2c18b6bc99afc9220cdbc55f25e4185099 Mon Sep 17 00:00:00 2001 From: Shrinidhi-59 Date: Fri, 24 Oct 2025 20:32:38 +0530 Subject: [PATCH 5/7] updated openbao module to make policies, serviceaccount and role configurable --- modules/openbao/default/1.0/facets.yaml | 73 ++++++++++++++++++ modules/openbao/default/1.0/main.tf | 98 +++++++----------------- modules/openbao/default/1.0/outputs.tf | 8 +- modules/openbao/default/1.0/variables.tf | 18 +++-- 4 files changed, 115 insertions(+), 82 deletions(-) diff --git a/modules/openbao/default/1.0/facets.yaml b/modules/openbao/default/1.0/facets.yaml index 54e12a4e..19bcc76a 100644 --- a/modules/openbao/default/1.0/facets.yaml +++ b/modules/openbao/default/1.0/facets.yaml @@ -43,24 +43,45 @@ spec: type: object title: Server Resources description: CPU and memory resource requests/limits for OpenBao servers + default: + requests: + cpu: 500m + memory: 256Mi + limits: + cpu: 1000m + memory: 512Mi properties: requests: type: object + title: Resource Requests + description: Minimum resources required + default: + cpu: 500m + memory: 256Mi properties: cpu: type: string + title: CPU Request default: 500m memory: type: string + title: Memory Request default: 256Mi limits: type: object + title: Resource Limits + description: Maximum resources allowed + default: + cpu: 1000m + memory: 512Mi properties: cpu: type: string + title: CPU Limit default: 1000m memory: type: string + title: Memory Limit default: 512Mi ui_enabled: type: boolean @@ -85,11 +106,44 @@ spec: title: PVC Labels description: Additional labels to apply to PersistentVolumeClaims default: {} + unseal_secret_name: + type: string + title: Unseal Secret Name + description: Name of the Kubernetes secret storing the unseal key (defaults + to instance name + '-unseal-key' for uniqueness) + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ openbao: type: object title: Advanced OpenBao Configuration description: Advanced configuration options for OpenBao Helm chart properties: + policies: + type: object + title: OpenBao Policies + description: Define custom policies, roles, and service account bindings + for OpenBao + default: {} + patternProperties: + .*: + type: object + properties: + service_account_name: + type: string + title: Service Account Name + description: Kubernetes service account name to bind to this policy + role_name: + type: string + title: Role Name + description: OpenBao Kubernetes auth role name + policy: + type: string + title: Policy HCL + description: OpenBao policy in HCL format + x-ui-editor: true + required: + - service_account_name + - role_name + - policy values: type: object title: Custom Helm Values @@ -128,3 +182,22 @@ sample: namespace: default release_name: openbao storage_type: raft + openbao: + policies: + control-plane-rw: + service_account_name: control-plane-service-sa + role_name: control-plane-role + policy: "# Full read-write access to all secrets\npath \"cp-secrets/*\"\ + \ {\n capabilities = [\"create\", \"read\", \"update\", \"delete\", \"\ + list\"]\n}\n\npath \"cp-secrets/data/*\" {\n capabilities = [\"create\"\ + , \"read\", \"update\", \"delete\", \"list\"]\n}\n\npath \"cp-secrets/metadata/*\"\ + \ {\n capabilities = [\"list\", \"read\", \"delete\"]\n}\n\n# System\ + \ health check\npath \"sys/health\" {\n capabilities = [\"read\"]\n}\n" + facets-release-readonly: + service_account_name: facets-release-pod + role_name: facets-release-role + policy: "# Read-only access to all secrets\npath \"cp-secrets/*\" {\n capabilities\ + \ = [\"read\", \"list\"]\n}\n\npath \"cp-secrets/data/*\" {\n capabilities\ + \ = [\"read\", \"list\"]\n}\n\npath \"cp-secrets/metadata/*\" {\n capabilities\ + \ = [\"read\", \"list\"]\n}\n\n# System health check\npath \"sys/health\"\ + \ {\n capabilities = [\"read\"]\n}\n" diff --git a/modules/openbao/default/1.0/main.tf b/modules/openbao/default/1.0/main.tf index 9861243c..470bb02a 100644 --- a/modules/openbao/default/1.0/main.tf +++ b/modules/openbao/default/1.0/main.tf @@ -28,8 +28,8 @@ locals { chart_name = "openbao" chart_version = lookup(local.spec, "chart_version", "0.18.4") - # Generate unique resource names - unseal_secret_name = "${local.release_name}-unseal-key" + # Generate unique resource names - use instance_name to ensure uniqueness across multiple instances + unseal_secret_name = lookup(local.spec, "unseal_secret_name", "${var.instance_name}-unseal-key") # Common labels labels = merge(var.environment.cloud_tags, { @@ -183,11 +183,12 @@ locals { }]) } } - user_defined_helm_values = lookup(local.openbao, "values", {}) + # Handle values field - can be either YAML string or object + user_defined_helm_values_raw = lookup(local.openbao, "values", {}) + user_defined_helm_values = try(yamldecode(local.user_defined_helm_values_raw), local.user_defined_helm_values_raw) - # Hardcoded service account names for OpenBao policies - control_plane_sa_name = "control-plane-service-sa" - facets_release_sa_name = "facets-release-pod" + # Get configurable policies from spec + policies = lookup(local.openbao, "policies", {}) } # Generate a random 32-byte key for static seal @@ -505,80 +506,35 @@ resource "kubernetes_job_v1" "openbao_init" { $ISSUER_PARAM " - # Enable KV v2 secrets engine at 'secret/' path - echo "Enabling KV v2 secrets engine at secret/ path..." - kubectl exec -n ${local.namespace} $POD_NAME -- env BAO_TOKEN="$ROOT_TOKEN" bao secrets enable -path=secret -version=2 kv || echo "KV v2 secrets engine already enabled at secret/" + # Enable KV v2 secrets engine at 'cp-secrets/' path + echo "Enabling KV v2 secrets engine at cp-secrets/ path..." + kubectl exec -n ${local.namespace} $POD_NAME -- env BAO_TOKEN="$ROOT_TOKEN" bao secrets enable -path=cp-secrets -version=2 kv || echo "KV v2 secrets engine already enabled at cp-secrets/" - # Create read-write policy for control-plane-service-sa - echo "Creating read-write policy for control-plane-service-sa..." - kubectl exec -n ${local.namespace} $POD_NAME -- sh -c " - export BAO_TOKEN='$ROOT_TOKEN' - bao policy write control-plane-rw - <<'POLICY' -# Full read-write access to all secrets -path \"secret/*\" { - capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\"] -} - -path \"secret/data/*\" { - capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\"] -} - -path \"secret/metadata/*\" { - capabilities = [\"list\", \"read\", \"delete\"] -} - -# System health check -path \"sys/health\" { - capabilities = [\"read\"] -} -POLICY - " - - # Create read-only policy for facets-release-pod - echo "Creating read-only policy for facets-release-pod..." - kubectl exec -n ${local.namespace} $POD_NAME -- sh -c " - export BAO_TOKEN='$ROOT_TOKEN' - bao policy write facets-release-readonly - <<'POLICY' -# Read-only access to all secrets -path \"secret/*\" { - capabilities = [\"read\", \"list\"] -} - -path \"secret/data/*\" { - capabilities = [\"read\", \"list\"] -} - -path \"secret/metadata/*\" { - capabilities = [\"read\", \"list\"] -} + # Create dynamic policies and roles + echo "" + echo "=== Creating Custom Policies and Roles ===" + %{for policy_key, policy_config in local.policies~} -# System health check -path \"sys/health\" { - capabilities = [\"read\"] -} -POLICY - " + # Create policy: ${policy_key} + echo "Creating policy '${policy_key}' for service account '${policy_config.service_account_name}'..." + cat <<'POLICY_EOF' | kubectl exec -i -n ${local.namespace} $POD_NAME -- env BAO_TOKEN="$ROOT_TOKEN" bao policy write ${policy_key} - +${policy_config.policy} - # Create auth role for control-plane-service-sa - echo "Creating auth role for control-plane-service-sa..." - kubectl exec -n ${local.namespace} $POD_NAME -- env BAO_TOKEN="$ROOT_TOKEN" bao write auth/kubernetes/role/control-plane-role \ - bound_service_account_names=${local.control_plane_sa_name} \ - bound_service_account_namespaces='*' \ - policies=control-plane-rw \ - ttl=72h +POLICY_EOF - # Create auth role for facets-release-pod - echo "Creating auth role for facets-release-pod..." - kubectl exec -n ${local.namespace} $POD_NAME -- env BAO_TOKEN="$ROOT_TOKEN" bao write auth/kubernetes/role/facets-release-role \ - bound_service_account_names=${local.facets_release_sa_name} \ + # Create auth role: ${policy_config.role_name} + echo "Creating auth role '${policy_config.role_name}'..." + kubectl exec -n ${local.namespace} $POD_NAME -- env BAO_TOKEN="$ROOT_TOKEN" bao write auth/kubernetes/role/${policy_config.role_name} \ + bound_service_account_names=${policy_config.service_account_name} \ bound_service_account_namespaces='*' \ - policies=facets-release-readonly \ + policies=${policy_key} \ ttl=72h + %{endfor~} echo "" echo "OpenBao configuration complete!" - echo "- Policies created: control-plane-rw, facets-release-readonly" - echo "- Auth roles created: control-plane-role, facets-release-role" + echo "- Policies created: ${join(", ", keys(local.policies))}" + echo "- Roles created: ${join(", ", [for p in local.policies : p.role_name])}" EOF ] diff --git a/modules/openbao/default/1.0/outputs.tf b/modules/openbao/default/1.0/outputs.tf index 523423b7..dc18ba51 100644 --- a/modules/openbao/default/1.0/outputs.tf +++ b/modules/openbao/default/1.0/outputs.tf @@ -2,11 +2,11 @@ locals { output_attributes = { namespace = lookup(local.metadata, "namespace", "default") release_name = lookup(local.spec, "release_name", "openbao") - service_name = "${lookup(local.spec, "release_name", "openbao")}-openbao" - service_url = "http://${lookup(local.spec, "release_name", "openbao")}-openbao.${lookup(local.metadata, "namespace", "default")}.svc.cluster.local:8200" + service_name = "${lookup(local.spec, "release_name", "openbao")}" + service_url = "http://${lookup(local.spec, "release_name", "openbao")}.${lookup(local.metadata, "namespace", "default")}.svc.cluster.local:8200" ui_enabled = lookup(local.spec, "ui_enabled", true) - ui_url = lookup(local.spec, "ui_enabled", true) ? "http://${lookup(local.spec, "release_name", "openbao")}-openbao.${lookup(local.metadata, "namespace", "default")}.svc.cluster.local:8200/ui" : null - health_check_url = "http://${lookup(local.spec, "release_name", "openbao")}-openbao.${lookup(local.metadata, "namespace", "default")}.svc.cluster.local:8200/v1/sys/health" + ui_url = lookup(local.spec, "ui_enabled", true) ? "http://${lookup(local.spec, "release_name", "openbao")}.${lookup(local.metadata, "namespace", "default")}.svc.cluster.local:8200/ui" : null + health_check_url = "http://${lookup(local.spec, "release_name", "openbao")}.${lookup(local.metadata, "namespace", "default")}.svc.cluster.local:8200/v1/sys/health" unseal_secret_name = "${lookup(local.spec, "release_name", "openbao")}-unseal-key" init_keys_secret_name = "${lookup(local.spec, "release_name", "openbao")}-init-keys" storage_type = lookup(local.spec, "storage_type", "raft") diff --git a/modules/openbao/default/1.0/variables.tf b/modules/openbao/default/1.0/variables.tf index 93640b55..6acca48a 100644 --- a/modules/openbao/default/1.0/variables.tf +++ b/modules/openbao/default/1.0/variables.tf @@ -28,14 +28,18 @@ variable "instance" { memory = optional(string, "512Mi") }), {}) }), {}) - ui_enabled = optional(bool, true) - storage_type = optional(string, "raft") - storage_size = optional(string, "10Gi") - pvc_labels = optional(map(string), {}) - controlplane_sa_name = optional(string, "control-plane-service-sa") - facets_release_sa_name = optional(string, "facets-release-pod") + ui_enabled = optional(bool, true) + storage_type = optional(string, "raft") + storage_size = optional(string, "10Gi") + pvc_labels = optional(map(string), {}) + unseal_secret_name = optional(string) openbao = optional(object({ - values = optional(map(any), {}) + policies = optional(map(object({ + service_account_name = string + role_name = string + policy = string + })), {}) + values = optional(any, {}) }), {}) }) }) From 3812a8a7b37271fdc5c7ab17a3ead6407a8a3a11 Mon Sep 17 00:00:00 2001 From: Shrinidhi-59 Date: Mon, 27 Oct 2025 12:30:04 +0530 Subject: [PATCH 6/7] updated readme.md --- modules/openbao/default/1.0/README.md | 438 +++++++++++++++--------- modules/openbao/default/1.0/facets.yaml | 1 + 2 files changed, 270 insertions(+), 169 deletions(-) diff --git a/modules/openbao/default/1.0/README.md b/modules/openbao/default/1.0/README.md index 62c49948..1f43cc95 100644 --- a/modules/openbao/default/1.0/README.md +++ b/modules/openbao/default/1.0/README.md @@ -1,75 +1,103 @@ -# Auto-Unsealing OpenBao Cluster +# OpenBao Cluster with Static Seal Auto-Unseal -This Facets module deploys OpenBao with automatic initialization and unsealing capabilities using configurable key shares and threshold. The module uses Helm to deploy OpenBao and implements auto-unseal functionality through Terraform null resources that execute API calls against the OpenBao cluster. +This Facets module deploys OpenBao with automatic unsealing using a static seal mechanism. The module uses Helm to deploy OpenBao and a Kubernetes Job to handle initialization, Raft cluster setup, and policy configuration. ## Features -- **Helm-based Deployment**: Uses the official Vault Helm chart (compatible with OpenBao) -- **Automatic Initialization**: Initializes OpenBao with configurable key shares and threshold -- **Auto-Unseal**: Automatically unseals OpenBao using stored unseal keys -- **Secure Key Storage**: Stores unseal keys and root token in Kubernetes secrets -- **Flexible Storage Backends**: Supports file, Raft, and Azure storage backends -- **High Availability**: Configurable replicas with Raft consensus -- **TLS Support**: Optional TLS configuration with multiple certificate sources -- **UI Access**: Optional web UI with ingress support +- **Static Seal Auto-Unseal**: Pods automatically unseal on restart without manual intervention +- **Helm-based Deployment**: Uses the official OpenBao Helm chart +- **Kubernetes Job Initialization**: Automated initialization and cluster setup +- **High Availability**: Raft consensus with configurable replicas +- **Kubernetes Auth Integration**: Pre-configured Kubernetes authentication backend +- **Custom Policy Support**: Define policies, roles, and service account bindings +- **KV v2 Secrets Engine**: Pre-enabled at `cp-secrets/` path - **Resource Management**: Configurable CPU/memory requests and limits +- **Persistent Storage**: Automatic PVC creation for each replica ## Architecture ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Helm Release │ │ Null Resource │ │ Null Resource │ -│ (OpenBao) │───▶│ (Initialize) │───▶│ (Auto-Unseal) │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ - │ │ │ - ▼ ▼ ▼ -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ OpenBao Pods │ │ K8s Secrets │ │ Unsealed │ -│ │ │ - Unseal Keys │ │ OpenBao │ -│ │ │ - Root Token │ │ Ready for Use │ +│ Random Key │───▶│ K8s Secret │───▶│ Helm Release │ +│ Generation │ │ (Unseal Key) │ │ (OpenBao Pods) │ └─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ▼ + ┌─────────────────────────────────────────┐ + │ Kubernetes Init Job │ + │ 1. Initialize leader (openbao-0) │ + │ 2. Store root token & recovery keys │ + │ 3. Join Raft nodes (HA mode) │ + │ 4. Configure Kubernetes auth │ + │ 5. Enable KV v2 secrets engine │ + │ 6. Create custom policies & roles │ + └─────────────────────────────────────────┘ + │ + ▼ + ┌─────────────────────────────────────────┐ + │ Auto-Unsealed OpenBao Cluster │ + │ - Static seal with env var key │ + │ - Pods auto-unseal on restart │ + │ - Raft HA (if replicas > 1) │ + │ - Ready for secret management │ + └─────────────────────────────────────────┘ ``` +## How Static Seal Works + +Unlike traditional Shamir seal (which requires manual unseal keys), static seal: +- Uses a pre-generated encryption key stored in a Kubernetes secret +- Injects the key into pods via environment variable (`OPENBAO_UNSEAL_KEY`) +- OpenBao automatically unseals on startup using the static key +- Pods can restart without manual intervention +- Recovery keys (not unseal keys) are generated during initialization for emergency access + +## Environment as Dimension + +The module is environment-aware through `var.environment`: +- **Namespace**: Deployed in the namespace specified in metadata +- **Tolerations**: Uses `var.environment.default_tolerations` for spot instance support +- **Tags**: Applies `var.environment.cloud_tags` to all resources +- **Storage**: PVCs are created per-environment, allowing different storage configurations + +## Resources Created + +- **Helm Release**: OpenBao cluster with static seal configuration +- **Random ID**: 32-byte encryption key for static seal +- **Kubernetes Secret**: Stores the static unseal key +- **PersistentVolumeClaims**: One per replica for data storage +- **ServiceAccount + RBAC**: For the initialization job +- **Kubernetes Job**: Handles initialization and cluster setup +- **Secret (created by job)**: Stores root token and recovery keys + ## Usage -### Basic Configuration +### Basic Standalone Deployment ```yaml kind: openbao-cluster -flavor: auto-unseal +flavor: default version: "1.0" spec: namespace: "openbao" - release_name: "my-openbao" - key_shares: 5 - key_threshold: 3 - storage_backend: - type: "raft" + release_name: "openbao" + storage_type: "file" + server_replicas: 1 ``` -### Advanced Configuration +### High Availability Deployment ```yaml kind: openbao-cluster -flavor: auto-unseal +flavor: default version: "1.0" spec: namespace: "openbao-prod" release_name: "openbao-ha" - key_shares: 7 - key_threshold: 4 - - # High availability setup + storage_type: "raft" server_replicas: 3 - storage_backend: - type: "raft" - config: - retry_join: - - "openbao-ha-0.openbao-ha-internal:8201" - - "openbao-ha-1.openbao-ha-internal:8201" - - "openbao-ha-2.openbao-ha-internal:8201" - - # Resource configuration + storage_size: "20Gi" + server_resources: requests: cpu: "1000m" @@ -77,16 +105,39 @@ spec: limits: cpu: "2000m" memory: "1Gi" - - # TLS and UI - tls_config: - enabled: true - cert_source: "cert-manager" - ui_enabled: true - ingress_enabled: true - - # Custom timeout - init_timeout: "600s" +``` + +### With Custom Policies + +```yaml +kind: openbao-cluster +flavor: default +version: "1.0" +spec: + namespace: "openbao" + release_name: "openbao" + storage_type: "raft" + server_replicas: 3 + + openbao: + policies: + app-readonly: + service_account_name: "my-app-sa" + role_name: "app-role" + policy: | + # Read-only access to app secrets + path "cp-secrets/data/app/*" { + capabilities = ["read", "list"] + } + + admin-full: + service_account_name: "admin-sa" + role_name: "admin-role" + policy: | + # Full access to all secrets + path "cp-secrets/*" { + capabilities = ["create", "read", "update", "delete", "list"] + } ``` ## Configuration Reference @@ -95,187 +146,236 @@ spec: | Parameter | Type | Description | |-----------|------|-------------| -| `key_shares` | integer | Number of key shares to generate (1-255) | -| `key_threshold` | integer | Number of shares required to unseal (1-255, ≤ key_shares) | -| `namespace` | string | Kubernetes namespace (created automatically) | +| `namespace` | string | Kubernetes namespace for deployment | | `release_name` | string | Helm release name | -| `storage_backend` | object | Storage backend configuration | +| `storage_type` | string | Storage backend: `raft` (HA) or `file` (standalone) | ### Optional Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `helm_chart_version` | string | "0.28.1" | Vault Helm chart version | -| `helm_repository` | string | "https://helm.releases.hashicorp.com" | Helm repository URL | -| `secret_name_prefix` | string | "openbao" | Prefix for secrets | -| `server_replicas` | integer | 1 | Number of OpenBao replicas | +| `chart_version` | string | "0.18.4" | OpenBao Helm chart version | +| `server_replicas` | integer | 1 | Number of OpenBao replicas (1-10) | | `server_resources` | object | See defaults | CPU/memory requests and limits | | `ui_enabled` | boolean | true | Enable OpenBao web UI | -| `ingress_enabled` | boolean | false | Create ingress for UI | -| `tls_config` | object | See TLS section | TLS configuration | -| `init_timeout` | string | "300s" | Initialization timeout | +| `storage_size` | string | "10Gi" | PVC size per replica | +| `pvc_labels` | object | {} | Additional labels for PVCs | +| `unseal_secret_name` | string | `{instance_name}-unseal-key` | Name of unseal key secret | +| `openbao.policies` | object | {} | Custom policies and roles | +| `openbao.values` | object | {} | Additional Helm values | -### Storage Backend Options - -#### Raft (Recommended for HA) -```yaml -storage_backend: - type: "raft" - config: - retry_join: - - "pod-0.service:8201" - - "pod-1.service:8201" -``` +### Policy Configuration -#### File (Single Instance) -```yaml -storage_backend: - type: "file" - config: - path: "/openbao/data" -``` +Define custom policies with Kubernetes auth integration: -#### Azure Key Vault ```yaml -storage_backend: - type: "azure" - config: - tenant_id: "your-tenant-id" - client_id: "your-client-id" - client_secret: "your-secret" - vault_name: "your-keyvault" +openbao: + policies: + {policy-name}: + service_account_name: "k8s-service-account" + role_name: "openbao-auth-role" + policy: | + path "cp-secrets/data/my-app/*" { + capabilities = ["read"] + } ``` -### TLS Configuration - -```yaml -tls_config: - enabled: true - cert_source: "self-signed" # or "cert-manager" or "manual" -``` +Each policy creates: +1. An OpenBao policy with the specified HCL +2. A Kubernetes auth role bound to the service account +3. Automatic binding allowing the SA to authenticate and assume the policy ## Outputs -The module provides the following outputs: +| Output | Description | +|--------|-------------| +| `namespace` | Deployment namespace | +| `release_name` | Helm release name | +| `service_name` | Kubernetes service name | +| `service_url` | Internal service URL | +| `ui_enabled` | Whether UI is enabled | +| `ui_url` | UI access URL (if enabled) | +| `health_check_url` | Health check endpoint | +| `unseal_secret_name` | Name of unseal key secret | +| `init_keys_secret_name` | Name of init keys secret (root token & recovery keys) | +| `storage_type` | Storage backend type | +| `server_replicas` | Number of replicas | +| `root_token` | Root token (sensitive) | +| `recovery_keys` | Recovery keys (sensitive) | -### Default Output -- **Connection details**: Service URLs, namespace, release name -- **UI access**: UI URL if enabled -- **Ingress details**: Ingress host if enabled -- **Secrets**: Names of Kubernetes secrets containing keys and tokens -- **Configuration**: Deployment configuration details -- **Health check**: Health check endpoint URL +## Security Considerations -### Additional Outputs -- `root_token_secret_name`: Name of secret containing root token -- `unseal_keys_secret_name`: Name of secret containing unseal keys -- `namespace`: Deployment namespace -- `service_details`: Service connection details for other apps -- `helm_release`: Helm release information +### Static Unseal Key -## Security Considerations +- The 32-byte unseal key is generated using Terraform's `random_id` resource +- Stored in a Kubernetes secret with `prevent_destroy = true` lifecycle +- Injected into pods via environment variable +- Key is never exposed in logs or Terraform state (base64 encoded) +- **Important**: Backup this secret - losing it means losing access to sealed data -### Unseal Keys Storage -- Unseal keys are stored in Kubernetes secrets with base64 encoding -- Secrets are labeled for easy identification and management -- Consider implementing additional encryption for secrets at rest +### Root Token & Recovery Keys -### Root Token Protection -- Root token is stored in a separate Kubernetes secret -- Rotate the root token regularly using OpenBao's token rotation features -- Limit access to the root token secret using RBAC +- Root token is stored in `{release_name}-init-keys` secret +- Recovery keys are for emergency access if static seal fails +- Rotate the root token regularly using OpenBao's token rotation +- Limit RBAC access to these secrets ### Network Security -- Use TLS for all communications when possible -- Configure network policies to restrict pod-to-pod communication -- Use ingress with proper authentication for UI access + +- Service uses ClusterIP type (internal only) +- TLS is disabled by default (enable for production) +- Use Kubernetes network policies to restrict access +- Consider using a service mesh for mTLS + +### RBAC + +- Init job uses a dedicated ServiceAccount with minimal permissions +- Only has access to: pods, pods/exec, secrets, statefulsets +- Permissions scoped to the deployment namespace only ## Operations ### Accessing OpenBao + ```bash # Get the root token -kubectl get secret openbao-root-token -n openbao -o jsonpath='{.data.root-token}' | base64 -d +kubectl get secret openbao-init-keys -n openbao -o jsonpath='{.data.root-token}' | base64 -d # Port forward to access UI -kubectl port-forward svc/openbao-vault -n openbao 8200:8200 +kubectl port-forward svc/openbao -n openbao 8200:8200 # Access via browser open http://localhost:8200/ui ``` -### Manual Unseal (if needed) +### Using Kubernetes Auth (from a Pod) + ```bash -# Get unseal keys -kubectl get secret openbao-init-keys -n openbao -o jsonpath='{.data.unseal-keys}' | base64 -d | base64 -d +# Inside a pod with the configured service account +export VAULT_ADDR="http://openbao.openbao.svc.cluster.local:8200" +export SA_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) + +# Login +vault write auth/kubernetes/login role=app-role jwt=$SA_TOKEN -# Unseal manually (if auto-unseal fails) -kubectl exec -n openbao openbao-vault-0 -- openbao operator unseal +# Use the token to access secrets +vault kv get cp-secrets/my-app/config ``` +### Scaling the Cluster + +To add more replicas: +1. Update `server_replicas` in your configuration +2. Apply the changes +3. The init job will automatically join new nodes to the Raft cluster + ### Backup and Recovery -- Regularly backup the Kubernetes secrets containing unseal keys -- Consider using Velero or similar tools for cluster-level backups -- Test recovery procedures in non-production environments + +```bash +# Backup critical secrets +kubectl get secret openbao-unseal-key -n openbao -o yaml > unseal-key-backup.yaml +kubectl get secret openbao-init-keys -n openbao -o yaml > init-keys-backup.yaml + +# Take Raft snapshots (using root token) +export VAULT_TOKEN="" +vault operator raft snapshot save backup.snap +``` ## Troubleshooting -### Common Issues +### Init Job Fails -1. **Initialization Timeout** - - Increase `init_timeout` value - - Check pod logs: `kubectl logs -n ` - - Verify network connectivity +```bash +# Check job logs +kubectl logs -n openbao job/openbao-init-xxxxx -2. **Unseal Failures** - - Check if secrets exist and contain valid keys - - Verify key threshold is not greater than available keys - - Check OpenBao pod status and logs +# Common issues: +# - Pod not ready: Increase timeout or check pod status +# - RBAC issues: Verify ServiceAccount permissions +# - Network issues: Check service connectivity +``` -3. **Helm Deployment Issues** - - Verify Helm repository is accessible - - Check chart version compatibility - - Review Helm release status: `helm status -n ` +### Pods Not Auto-Unsealing -### Debugging Commands ```bash -# Check OpenBao status -kubectl exec -n -- openbao status +# Check if unseal key secret exists +kubectl get secret openbao-unseal-key -n openbao -# View initialization logs -kubectl logs -n | grep init +# Verify environment variable is injected +kubectl exec -n openbao openbao-0 -- env | grep OPENBAO_UNSEAL_KEY -# Check secrets -kubectl get secrets -n | grep openbao +# Check pod logs for seal errors +kubectl logs -n openbao openbao-0 +``` -# Describe Helm release -helm get values -n +### Raft Node Not Joining + +```bash +# Check node status +kubectl exec -n openbao openbao-1 -- bao status + +# Manually join if needed (using root token) +kubectl exec -n openbao openbao-1 -- bao operator raft join \ + http://openbao-0.openbao-internal.openbao.svc.cluster.local:8200 +``` + +### Check Cluster Status + +```bash +# View Raft peers +export VAULT_TOKEN="" +vault operator raft list-peers + +# Check seal status +vault status ``` ## Prerequisites -- Kubernetes cluster with RBAC enabled -- Helm 3.x installed and configured -- `kubectl` access to the target cluster -- `jq` utility for JSON processing (required for auto-unseal scripts) +- Kubernetes cluster (1.19+) +- Helm 3.x +- Persistent volume provisioner (for PVCs) +- RBAC enabled +- Kubernetes provider configured via inputs ## Limitations -- Currently supports single-cluster deployments only -- Auto-unseal requires `kubectl` and `jq` to be available in Terraform execution environment -- Raft storage backend requires persistent volumes -- TLS certificate management is basic (consider cert-manager for production) +- Only supports Azure cloud (as configured) +- TLS is not enabled by default +- Static seal key rotation requires manual process +- Single Kubernetes cluster deployments only +- Raft storage requires persistent volumes + +## Advanced Configuration + +### Custom Helm Values + +Override any Helm chart value: + +```yaml +openbao: + values: + server: + extraEnvironmentVars: + FOO: "bar" + annotations: + custom-annotation: "value" +``` -## Contributing +### Storage Backend Selection -This module follows Facets module development standards. When contributing: +**File Storage** (Standalone): +- Single replica only +- Data stored in PVC +- No HA capabilities +- Simpler setup -1. Maintain backward compatibility -2. Update documentation for any new features -3. Add appropriate validation rules -4. Test with different storage backends -5. Follow Terraform best practices +**Raft Storage** (HA): +- Multiple replicas supported +- Distributed consensus +- Automatic failover +- Requires internal service for inter-pod communication ## License -This module is part of the Facets platform and follows the same licensing terms. \ No newline at end of file +This module is part of the Facets platform and follows the same licensing terms. diff --git a/modules/openbao/default/1.0/facets.yaml b/modules/openbao/default/1.0/facets.yaml index 19bcc76a..60b69ada 100644 --- a/modules/openbao/default/1.0/facets.yaml +++ b/modules/openbao/default/1.0/facets.yaml @@ -140,6 +140,7 @@ spec: title: Policy HCL description: OpenBao policy in HCL format x-ui-editor: true + x-ui-editor-language: hcl required: - service_account_name - role_name From 476638dd99a58f41da3dc3048c55677b9b1b8589 Mon Sep 17 00:00:00 2001 From: Shrinidhi-59 Date: Mon, 27 Oct 2025 15:13:06 +0530 Subject: [PATCH 7/7] updated the log line for init job --- modules/openbao/default/1.0/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openbao/default/1.0/main.tf b/modules/openbao/default/1.0/main.tf index 470bb02a..8289fa48 100644 --- a/modules/openbao/default/1.0/main.tf +++ b/modules/openbao/default/1.0/main.tf @@ -365,7 +365,7 @@ resource "kubernetes_job_v1" "openbao_init" { # Wait for at least one OpenBao pod kubectl wait --for=condition=ready pod/${local.release_name}-0 -n ${local.namespace} --timeout=300s || { - echo "ERROR: openbao-0 pod not ready" + echo "ERROR: openbao server pod not ready" exit 1 }