Table of Contents

Today, we are setting up OpenBao (the open-source fork of HashiCorp Vault) using rootless Podman on a Hetzner VPS (running openSUSE). This setup ensures your secrets are encrypted at rest, isolated by policy, and running under a non-privileged user.


0. The Infrastructure Script (Setting the Stage)

Before launching the container, we must prepare the host directory. In a rootless environment, UID mapping is tricky. OpenBao runs internally as UID 100, so we use podman unshare to grant that specific “ghost” user permission to our host folder.

First create a directory for your podman compose file and directory creation script.

mkdir openbao-secrets-manager

Save this as dir.sh:

#!/bin/bash
INFRA_PATH="/srv/infra/openbao"

echo "Creating OpenBao infrastructure directories..."
sudo mkdir -p $INFRA_PATH/{data,config}
sudo chown -R $USER:$USER /srv/infra

echo "Applying SELinux contexts for openSUSE..."
sudo semanage fcontext -a -t container_file_t "$INFRA_PATH(/.*)?"
sudo restorecon -Rv -F $INFRA_PATH

echo "Applying Podman UID mappings for OpenBao (UID 100)..."
podman unshare chown -R 100:100 $INFRA_PATH/data
chmod 755 $INFRA_PATH

echo "OpenBao is ready at $INFRA_PATH."

Save this as openbao.yml:

name: openbao

networks:
  openbao_external:
    external: true

services:
  openbao:
    image: openbao/openbao:2.5.2
    container_name: openbao-server
    restart: always
    cap_add:
      - IPC_LOCK
    environment:
      BAO_API_ADDR: "http://0.0.0.0:8200"
      BAO_ADVERTISE_ADDR: "http://openbao-server:8200"
      BAO_CLUSTER_ADDR: "http://openbao-server:8201"
      BAO_LOCAL_CONFIG: |
        {
          "storage": {
            "raft": {
              "path": "/openbao/data",
              "node_id": "node1"
            }
          },
          "listener": [{
            "tcp": {
              "address": "0.0.0.0:8200",
              "cluster_address": "0.0.0.0:8201",
              "tls_disable": true
            }
          }],
          "ui": true
        }
    ports:
      - "8200:8200"
      - "8201:8201"
    volumes:
      - /srv/infra/openbao/data:/openbao/data:Z
    command: server
    networks:
      - openbao_external

run ./dir.shthis will create the initial directory structure.


1. Initialization: The “Birth” of the Vault

This command generates your master encryption keys. Copy the output of this command immediately into a secure text file or password manager.

podman exec -it -e BAO_ADDR='http://127.0.0.1:8200' openbao-server bao operator init

⚠️ IMPORTANT: You will see 5 “Unseal Keys” and 1 “Initial Root Token.” Save them now. If you lose them, your data is mathematically unrecoverable.


2. Unsealing: Opening the Door

By design, OpenBao starts “Sealed” (locked). You need to provide 3 out of your 5 keys to load the master decryption key into memory. Run this command 3 separate times, pasting a different key each time:

podman exec -it -e BAO_ADDR='http://127.0.0.1:8200' openbao-server bao operator unseal

(After the 3rd key, the output will show Sealed: false.)


3. Authentication: Becoming Admin

Now, log in as the administrator using that Initial Root Token:

podman exec -it -e BAO_ADDR='http://127.0.0.1:8200' openbao-server bao login
# Paste the Root Token when prompted

4. Enable the Storage Engine

Tell OpenBao to activate the “Key-Value” (KV) engine where your app secrets will live:

podman exec -it -e BAO_ADDR='http://127.0.0.1:8200' openbao-server bao secrets enable -path=secret kv-v2

5. Upload Your Policies (Least Privilege)

We want our apps to be isolated. The API shouldn’t see Web secrets, and vice versa. So first create your policy file for example like this:

path "secret/data/ulyaoth/api" {
  capabilities = ["read"]
}

you save this as ulyaoth-api-policy.hcland then continue with importing it.

# Upload API Policy
cat ulyaoth-api-policy.hcl | podman exec -i -e BAO_ADDR='http://127.0.0.1:8200' openbao-server bao policy write ulyaoth-api -

# Upload Web Policy
cat ulyaoth-web-policy.hcl | podman exec -i -e BAO_ADDR='http://127.0.0.1:8200' openbao-server bao policy write ulyaoth-web -

6. Generate Restricted Tokens

Finally, generate the tokens your apps will actually use. Unlike the Root Token, these tokens are strictly limited to the paths defined in the policies above.

# Generate API Token
podman exec -it -e BAO_ADDR='http://127.0.0.1:8200' openbao-server bao token create -policy="ulyaoth-api"

# Generate Web Token
podman exec -it -e BAO_ADDR='http://127.0.0.1:8200' openbao-server bao token create -policy="ulyaoth-web"

Save these strings! These go into your production environment variables as BAO_TOKEN.


7. Storing the Secrets

Now, move those hardcoded secrets into the secure vault:

# API Secrets
podman exec -it -e BAO_ADDR='http://127.0.0.1:8200' openbao-server bao kv put secret/ulyaoth/api \
    INTERNAL_API_SECRET="your-real-secret" \
    JWT_SECRET="secure-jwt-key"

# Web Secrets
podman exec -it -e BAO_ADDR='http://127.0.0.1:8200' openbao-server bao kv put secret/ulyaoth/web \
    OAUTH_CLIENT_ID="your-id"

Conclusion

You are now 100% set up! Your secrets are stored in a memory-locked vault, your apps are cryptographically isolated.


Troubleshooting

If you’ve tried to run OpenBao and hit “Permission Denied” loops or locking errors, it’s best to wipe the state and ensure the plumbing is perfect.

Run these to reset your environment:

# Bring down existing containers
podman compose openbao.yml down

# Clean the database folder (Using unshare to handle the mapped permissions)
podman unshare rm -rf /srv/infra/openbao/data/*

# Start it back up
podman compose openbao.yml up -d

Categorized in:

Tutorials,