OpenBao - With Forgejo and Ansible Automation Platform

OpenBao System Architecture and Operations Reference

1. Core Architecture

OpenBao functions as an API-based request router. All interactions are HTTP API calls to specific paths.

  • Prefix Routing: The first path segment defines the handling engine. Unmounted prefixes drop requests (404).
  • KV-v2 Data Split: Applications access secrets at secret/data/foo. Administrators manage configuration/versions at secret/metadata/foo.
  • Engines & Mounts: Engines are pre-compiled plugins (kv, database, approle). Mounts are active deployments of engines to API paths.
  • Built-in Paths: * sys/: Runtime configuration.
    • auth/: Authentication endpoints.
    • identity/: Entity mapping.
    • cubbyhole/: Ephemeral, token-scoped storage.

Setting up OpenBao (the community-maintained fork of HashiCorp Vault) is very similar to the original Vault setup, but with a few key “Baoish” quirks regarding binary names and environment variables.

Here is a guide to getting a basic instance running.

Installation

1. Package installation (CentOS/RHEL)

# Add EPEL for dependencies
sudo dnf install -y epel-release
# Install OpenBao (replace with actual repo/package name)
sudo dnf install -y openbao

2. Setup (Linux)

For a persistent installation, you’ll want to use a configuration file (.hcl) and a storage backend (we use the local filesystem for simplicity). The service will run as openbao user.

The Configuration (/etc/openbao.d/openbao.hcl)

# /etc/openbao.d/openbao.hcl 
# Full configuration options can be found at https://openbao.org/docs/configuration

ui = true

# use the local host filesystem as its database backend, rather than a distributed system like Raft, Consul, or PostgreSQL
# no HA needed for my setup
storage "file" {
  path = "/var/lib/openbao/data"
}

# enable auditing
audit "file" "file" {
  description = "Local file audit log"
  options = {
    file_path = "/var/log/openbao_audit.log"
  }
}

# HTTPS listener
# self-signed
# don't forget to use export BAO_SKIP_VERIFY=true 
listener "tcp" {
  address       = "0.0.0.0:8200"
  tls_cert_file = "/etc/openbao.d/tls/tls.crt"
  tls_key_file  = "/etc/openbao.d/tls/tls.key"
}

3. Initialization & Unsealing

Once the service is running, OpenBao starts in a Sealed state (encrypted). You must initialize it once to get your “Unseal Keys.”

  1. Initialize:

    export BAO_ADDR='http://127.0.0.1:8200'
    bao operator init
    

    Crucial: Save the Unseal Keys and the Initial Root Token somewhere extremely secure. If you lose these, your data is gone forever.

  2. Unseal: Run this command x (defined by the initial_shares value from the initialization output) times (using x different keys from the list you just generated):

    bao operator unseal
    
  3. Login:

    bao login [YOUR_ROOT_TOKEN]
    


Key Differences from Vault

  • The Command: Use bao instead of vault. (Though many distributions include a vault alias for compatibility).
  • The Environment Variables: Usually prefixed with BAO_ (e.g., BAO_ADDR, BAO_TOKEN), though it often respects VAULT_ vars for migration ease.
  • Open Source: Unlike Vault, features like Namespaces (multi-tenancy) are included for free in OpenBao.

OpenBao Configuration & Usage

1. System Configuration & Audit

Here we set up a file-based audit log and verify that API calls are being logged correctly.

# Prepare log file
sudo touch /var/log/openbao_audit.log
sudo chown openbao:openbao /var/log/openbao_audit.log

# Append to /etc/openbao.d/openbao.hcl
audit "file" "file" {
  options = { file_path = "/var/log/openbao_audit.log" }
}

# Apply and unseal (I have 2 initial_shares, so I run this twice)
sudo systemctl restart openbao
bao operator unseal
bao operator unseal

# Verify audit log
bao token lookup
tail -n 1 /var/log/openbao_audit.log | jq

2. Access Control (Policies)

We create an admin user because we don’t work with root all the time. Policies map capabilities to paths. Default is deny.

# Create admin policy
cat <<EOF > foo-admin.hcl
path "auth/*" { capabilities = ["create", "read", "update", "delete", "list", "sudo"] }
path "sys/auth/*" { capabilities = ["create", "update", "delete", "sudo"] }
path "sys/auth" { capabilities = ["read"] }
path "sys/policies/acl/*" { capabilities = ["create", "read", "update", "delete", "list", "sudo"] }
path "sys/policies/acl" { capabilities = ["list"] }
path "sys/mounts/*" { capabilities = ["create", "read", "update", "delete", "list", "sudo"] }
path "sys/mounts" { capabilities = ["read"] }
path "secret/*" { capabilities = ["create", "read", "update", "delete", "list", "sudo"] }
EOF

bao policy write foo-admin foo-admin.hcl

# Create machine least-privilege policy (KV-v2 data path)
cat <<EOF > foo-app.hcl
path "secret/data/foo/*" { capabilities = ["read"] }
EOF
bao policy write foo-app foo-app.hcl

3. Authentication (Human & Machine)

Human (Userpass):

bao auth enable userpass
bao write auth/userpass/users/example-user password="bar-password" policies="foo-admin"
bao login -method=userpass username=example-user

Machine (AppRole):

bao auth enable approle

# Create role mapped to policy
bao write auth/approle/role/foo-role token_policies="foo-app" token_ttl=1h token_max_ttl=4h

# Retrieve credentials for CI/CD injection
bao read auth/approle/role/foo-role/role-id
bao write -f auth/approle/role/foo-role/secret-id

4. Secrets Management (KV-v2)

Write to standard path, read from /data/ path.

bao kv put secret/foo/bar db_user="example-admin" db_pass="bar-password"

5. Ansible Integration

Local Requirements & Execution:

sudo dnf install python3-hvac
ansible-galaxy collection install community.hashi_vault

export VAULT_ADDR="https://127.0.0.1:8200"
export VAULT_AUTH_METHOD="approle"
export VAULT_ROLE_ID="<foo-role-id>"
export VAULT_SECRET_ID="<foo-secret-id>"

ansible-playbook foo-playbook.yml

Ansible Automation Platform (AAP) Configuration: Create a Custom Credential Type in the AAP UI.

Input Configuration (YAML):

fields:
  - id: bao_url
    type: string
    label: OpenBao Server URL
  - id: role_id
    type: string
    label: AppRole Role ID
  - id: secret_id
    type: string
    label: AppRole Secret ID
    secret: true

Injector Configuration (YAML):

env:
  VAULT_ADDR: '{{ bao_url }}'
  VAULT_AUTH_METHOD: approle
  VAULT_ROLE_ID: '{{ role_id }}'
  VAULT_SECRET_ID: '{{ secret_id }}'

Playbook Syntax:

tasks:
  - name: Fetch secret
    ansible.builtin.set_fact:
      db_pass: "{{ lookup('community.hashi_vault.hashi_vault', 'secret=secret/data/foo/bar:db_pass') }}"

6. Forgejo / GitHub Actions Integration

Use repository secrets for VAULT_ROLE_ID and VAULT_SECRET_ID. If the runner executes in a Docker container, the Vault URL must point to the host via the Docker bridge gateway (172.17.0.1).

steps:
  - name: Fetch Secrets
    uses: hashicorp/vault-action@v3
    with:
      url: https://127.0.0.1:8200 
      method: approle
      roleId: ${{ secrets.VAULT_ROLE_ID }}
      secretId: ${{ secrets.VAULT_SECRET_ID }}
      secrets: |
        secret/data/foo/bar db_pass | DB_PASS_VAR ;