Secrets Management
    Self-Hosted

    Deploy OpenBao on a VPS

    Self-host OpenBao — the Linux Foundation fork of HashiCorp Vault — on a RamNode VPS with integrated Raft storage, systemd, and TLS.

    A production deployment guide for self-hosted secrets management using OpenBao, the Linux Foundation fork of HashiCorp Vault.

    Overview

    OpenBao is an open-source, community-driven fork of HashiCorp Vault, governed by the Linux Foundation and the OpenSSF. It provides identity-based secrets management, dynamic secrets, and encryption as a service under the MPL 2.0 license, with no per-secret or per-request pricing. When you self-host it on a RamNode VPS you get full control over your secrets infrastructure at a flat monthly cost.

    This guide covers a single-node production install using the native binary, systemd, integrated Raft storage, and TLS. Raft removes the need for an external database, which keeps the deployment self-contained and easy to back up.

    The current stable series is OpenBao 2.x. The bao CLI is the primary binary and is a drop-in analogue of the vault command.

    Prerequisites

    • A RamNode VPS running Ubuntu 24.04 LTS (Debian 12 works with the same steps). A 2 GB RAM plan is comfortable for most single-node use cases; 1 GB is workable for light workloads.
    • A domain or subdomain pointing at your VPS public IP (for example bao.example.com) if you want browser-trusted TLS.
    • Root or sudo access.
    • Basic familiarity with Linux service administration.

    1. Initial server hardening

    Create a non-root administrative user and apply baseline hardening before installing anything.

    shell
    # As root
    adduser deploy
    usermod -aG sudo deploy
    
    # Copy your SSH key to the new user
    rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy

    Harden SSH by editing /etc/ssh/sshd_config:

    shell
    PermitRootLogin no
    PasswordAuthentication no

    Then reload:

    shell
    systemctl reload ssh

    Configure the firewall. OpenBao listens on 8200 (API/UI) and 8201 (cluster). For a single node you only need 8200 exposed, and ideally only behind TLS.

    shell
    sudo ufw default deny incoming
    sudo ufw default allow outgoing
    sudo ufw allow OpenSSH
    sudo ufw allow 8200/tcp
    sudo ufw enable

    2. Install the OpenBao binary

    Fetch the latest release binary directly from GitHub. This snippet resolves the newest version automatically so it stays current.

    shell
    sudo apt update && sudo apt install -y curl jq unzip
    
    VERSION=$(curl -s https://api.github.com/repos/openbao/openbao/releases/latest \
      | jq -r '.tag_name' | cut -d'v' -f2-)
    
    ARCH=$(dpkg --print-architecture)   # amd64 or arm64
    
    curl -Lo bao.tar.gz \
      "https://github.com/openbao/openbao/releases/download/v${VERSION}/bao_${VERSION}_linux_${ARCH}.tar.gz"
    
    tar -xzf bao.tar.gz bao
    sudo install -o root -g root -m 0755 bao /usr/local/bin/bao
    rm -f bao bao.tar.gz
    
    bao version

    OpenBao also ships native packages for Debian, Ubuntu, RHEL, and Fedora on the downloads page if you prefer apt-managed upgrades. The binary approach shown here keeps the install self-contained and predictable.

    3. Create the service account and directories

    shell
    sudo useradd --system --home /etc/openbao --shell /bin/false openbao
    
    sudo mkdir -p /etc/openbao /opt/openbao/data /opt/openbao/tls
    sudo chown -R openbao:openbao /opt/openbao
    sudo chmod 700 /opt/openbao/data

    4. Write the server configuration

    Create /etc/openbao/openbao.hcl:

    shell
    cluster_name = "ramnode-openbao"
    ui           = true
    
    storage "raft" {
      path    = "/opt/openbao/data"
      node_id = "node1"
    }
    
    listener "tcp" {
      address       = "0.0.0.0:8200"
      tls_cert_file = "/opt/openbao/tls/fullchain.pem"
      tls_key_file  = "/opt/openbao/tls/privkey.pem"
    }
    
    api_addr     = "https://bao.example.com:8200"
    cluster_addr = "https://127.0.0.1:8201"
    
    default_lease_ttl = "168h"
    max_lease_ttl     = "720h"
    
    disable_mlock = false

    Replace bao.example.com with your domain. Notes on the choices:

    • disable_mlock = false keeps secret material out of swap. Because OpenBao locks memory, the systemd unit below grants the IPC_LOCK capability. As a defense in depth measure you should also disable or encrypt swap on the host.
    • TLS is terminated by OpenBao itself rather than a proxy. For a secrets manager this is the more defensible design because plaintext secrets never traverse a local socket in the clear.

    Disable or encrypt swap

    shell
    # Confirm current swap
    sudo swapon --show
    
    # To disable swap entirely (simplest for a dedicated secrets host)
    sudo swapoff -a
    sudo sed -i '/\sswap\s/d' /etc/fstab

    5. Provision TLS certificates

    Option A: Let's Encrypt with Caddy in front (simplest)

    If you would rather not manage certificate renewals by hand, put Caddy in front and disable native TLS in the listener (set the listener to 127.0.0.1:8200 and tls_disable = true, then point api_addr at the HTTPS Caddy address). A minimal Caddyfile:

    shell
    bao.example.com {
        reverse_proxy 127.0.0.1:8200
    }

    Caddy obtains and renews the certificate automatically. With this model, only 80 and 443 need to be open in UFW, and 8200 stays bound to localhost.

    Option B: Native TLS with certbot (hardened)

    Obtain a certificate and copy it into place for OpenBao to read directly:

    shell
    sudo apt install -y certbot
    sudo certbot certonly --standalone -d bao.example.com
    
    sudo cp /etc/letsencrypt/live/bao.example.com/fullchain.pem /opt/openbao/tls/
    sudo cp /etc/letsencrypt/live/bao.example.com/privkey.pem   /opt/openbao/tls/
    sudo chown openbao:openbao /opt/openbao/tls/*.pem
    sudo chmod 600 /opt/openbao/tls/*.pem

    Add a renewal hook so OpenBao picks up rotated certs. Create /etc/letsencrypt/renewal-hooks/deploy/openbao.sh:

    shell
    #!/bin/bash
    cp /etc/letsencrypt/live/bao.example.com/fullchain.pem /opt/openbao/tls/
    cp /etc/letsencrypt/live/bao.example.com/privkey.pem   /opt/openbao/tls/
    chown openbao:openbao /opt/openbao/tls/*.pem
    chmod 600 /opt/openbao/tls/*.pem
    systemctl reload openbao
    shell
    sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/openbao.sh

    6. Create the systemd unit

    Create /etc/systemd/system/openbao.service:

    shell
    [Unit]
    Description=OpenBao secrets management
    Documentation=https://openbao.org/docs/
    Requires=network-online.target
    After=network-online.target
    ConditionFileNotEmpty=/etc/openbao/openbao.hcl
    
    [Service]
    User=openbao
    Group=openbao
    ProtectSystem=full
    ProtectHome=read-only
    PrivateTmp=yes
    PrivateDevices=yes
    SecureBits=keep-caps
    AmbientCapabilities=CAP_IPC_LOCK
    CapabilityBoundingSet=CAP_SYSLOG CAP_IPC_LOCK
    NoNewPrivileges=yes
    ExecStart=/usr/local/bin/bao server -config=/etc/openbao/openbao.hcl
    ExecReload=/bin/kill --signal HUP $MAINPID
    KillMode=process
    KillSignal=SIGINT
    Restart=on-failure
    RestartSec=5
    TimeoutStopSec=30
    LimitNOFILE=65536
    LimitMEMLOCK=infinity
    
    [Install]
    WantedBy=multi-user.target

    Enable and start:

    shell
    sudo systemctl daemon-reload
    sudo systemctl enable --now openbao
    sudo systemctl status openbao

    7. Initialize and unseal

    Point the CLI at your server and initialize. This produces unseal keys and the initial root token. Store these somewhere safe and offline; losing them means losing access to your data.

    shell
    export BAO_ADDR="https://bao.example.com:8200"
    
    bao operator init -key-shares=5 -key-threshold=3

    Unseal by supplying three of the five keys (run the command three times with different keys):

    shell
    bao operator unseal

    Log in with the root token:

    shell
    bao login <root-token>
    
    bao status

    A healthy node reports Sealed false and Initialized true.

    Auto-unseal (recommended for production)

    Manual unsealing on every restart is operationally painful. OpenBao supports auto-unseal via cloud KMS or a Transit seal from another OpenBao instance. If you run a second small VPS, a Transit auto-unseal removes the manual step while keeping the root of trust off the primary host. Configure a seal "transit" stanza pointing at the second instance once you are ready to scale beyond a single node.

    8. First secret and the UI

    Enable the KV v2 secrets engine and write a test secret:

    shell
    bao secrets enable -version=2 kv
    bao kv put kv/test username=admin password=s3cr3t
    bao kv get kv/test

    The web UI is available at https://bao.example.com:8200/ui (or your Caddy address). Log in with any valid token.

    For remote administration without exposing the UI publicly, an SSH tunnel is a clean option:

    shell
    ssh -L 8200:127.0.0.1:8200 deploy@your-ramnode-ip

    9. Backups

    Raft storage supports consistent point-in-time snapshots. Automate them.

    shell
    # Manual snapshot
    bao operator raft snapshot save /opt/openbao/backups/bao-$(date +%F).snap

    A simple daily cron under root, writing to a directory the openbao user owns and shipping off-box:

    shell
    0 3 * * * BAO_ADDR="https://bao.example.com:8200" BAO_TOKEN="<backup-token>" \
      /usr/local/bin/bao operator raft snapshot save \
      /opt/openbao/backups/bao-$(date +\%F).snap

    Use a dedicated token with a policy limited to snapshot operations rather than the root token. Push snapshots to off-site object storage; RamNode S3-compatible storage or any external bucket works well. Restore with bao operator raft snapshot restore.

    10. Upgrades

    Because the install is a single binary, upgrades are straightforward:

    shell
    # Fetch and install the new binary (repeat the step 2 commands)
    sudo systemctl restart openbao
    # Then unseal again unless you have configured auto-unseal
    bao operator unseal

    Read the release notes before upgrading. Some provider-specific built-in seals are being moved to external plugins in later releases, and there are periodic security fixes worth tracking on the OpenBao releases page.

    RamNode platform notes

    • OpenBao does not send email, so RamNode's prohibition on running mail services does not affect it. If you wire OpenBao events into an alerting pipeline, route notifications through an external API-based service rather than an on-box SMTP relay.
    • Keep 8200 closed to the public internet until TLS is active. Never expose an OpenBao listener with tls_disable = true on a public interface.

    Troubleshooting

    • Server starts but stays sealed after a reboot: this is expected without auto-unseal. Run bao operator unseal three times, or configure a Transit or KMS seal.
    • Error checking seal status ... connection refused: confirm BAO_ADDR uses https and matches your api_addr, and that the service is running.
    • Permission denied on the data directory: confirm /opt/openbao/data is owned by openbao and is mode 700.
    • mlock errors in the journal: confirm the systemd unit grants CAP_IPC_LOCK and sets LimitMEMLOCK=infinity.

    Check logs with:

    shell
    journalctl -u openbao -f