Security AWS

OpenBao 2.7.2 on AWS User Guide

| Product: OpenBao 2.7.2 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the OpenBao 2.7.2 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg identity, secrets and PKI listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

OpenBao is an open source secrets manager, maintained under the Linux Foundation and API compatible with HashiCorp Vault. It stores and versions secrets (the KV engine), issues short-lived tokens bound to policies, and records every request in an audit log, so applications fetch their database passwords and API keys from one place instead of carrying them in files.

A stock OpenBao server starts uninitialised: whoever first calls sys/init receives the unseal keys and the root token. On a public address that is a race anyone can win. The cloudimg image closes it and makes the server safe to run on the internet:

  • No takeover window. First boot initialises OpenBao on the instance itself, over loopback, before any public port opens. nginx starts only after OpenBao reports initialized: true, and it never forwards a write to sys/init. The image ships with no storage, no keys, no token and no certificate.
  • Keys unique to your instance. First boot creates five unseal keys (any three unseal the vault) and a root token, and writes them to one file, /root/openbao-credentials.txt, readable by root only. The OpenBao service account cannot read it. Keys and tokens are never on a command line, in a service's environment or in a log.
  • HTTPS from the first boot. nginx serves the web UI (/ui/) and the API (/v1/) on port 443 with a certificate generated on your instance's first boot (an end-entity certificate naming the instance's DNS names and addresses). Port 80 only redirects to HTTPS. OpenBao listens on 127.0.0.1:8200 and cannot be reached from the network.
  • Auto-unseal at boot, your choice. While the unseal keys are on the instance, openbao-unseal.service unseals OpenBao after every reboot so applications keep working unattended. Step 7 moves the keys off the instance and switches this off.
  • Audit log on. A file audit device records every request in /var/log/openbao/audit.log (readable by the service only), with tokens and secret values replaced by HMACs. It is rotated daily and kept for 14 days.
  • Dangerous endpoints closed. The raw storage endpoint is off, metrics and profiling need a token, and nginx refuses sys/raw, raft join and bootstrap, root token generation and rekey from the network (use them on the instance). Requests are logged with the real client address, and a forged X-Forwarded-For header is ignored.
  • Metadata service blocked. OpenBao, nginx and the first boot and unseal services are blocked from the instance metadata service, and the AMI requires IMDSv2. No secrets engine that fetches URLs is enabled by default.
  • Pinned and patched. OpenBao 2.7.2 is the official release package, verified against the OpenBao release signing key and pinned by its SHA-256. Ubuntu 24.04 LTS is fully patched at build time with unattended security upgrades switched on.

What is on the image: OpenBao 2.7.2 (/usr/bin/bao, the official openbao package), nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.

Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large (t3.medium is enough for a small team)

Prerequisites

  • An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet.
  • A security group that allows TCP 22 (SSH) from your administrators and TCP 443 (the UI and the API) from your administrators and the hosts whose applications read secrets. Port 80 is optional (it only redirects to HTTPS). Never open 8200 or 8201: they listen on loopback only and refuse connections from the network even if a security group allows them.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick OpenBao 2.7.2 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose your instance type, key pair, subnet and a security group as above.

With the AWS CLI (replace the values in angle brackets):

aws ec2 create-security-group --region <region> --vpc-id <your-vpc> \
  --group-name openbao --description "OpenBao: SSH and HTTPS from admins and applications"
aws ec2 authorize-security-group-ingress --region <region> --group-id <sg-id> --ip-permissions \
  'IpProtocol=tcp,FromPort=22,ToPort=22,IpRanges=[{CidrIp=<your-admin-cidr>}]' \
  'IpProtocol=tcp,FromPort=443,ToPort=443,IpRanges=[{CidrIp=<your-client-cidr>}]'
aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
  --instance-type m5.large --key-name <your-key-pair> \
  --security-group-ids <sg-id> --subnet-id <your-subnet> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=openbao}]'

First boot takes well under a minute. It names the server after the instance's public DNS name (or its private DNS name when it has no public one). To use your own name from the first boot, add user data that writes it before first boot runs, for example #!/bin/sh followed by mkdir -p /etc/cloudimg && echo vault.example.com > /etc/cloudimg/openbao-domain.preset, or change it later with openbao-set-domain (Step 12).

Step 2: Connect to your instance

Version SSH user
OpenBao 2.7.2 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

Step 3: Check the services

First boot creates the certificate, initialises and unseals OpenBao on loopback, and only then lets nginx start:

systemctl is-active openbao openbao-unseal nginx
sudo test -f /var/lib/cloudimg/openbao-firstboot.done && echo FIRSTBOOT_DONE
active
active
active
FIRSTBOOT_DONE

The first boot log shows the order: OpenBao was initialised before nginx started.

sudo journalctl -b -o short-iso -u openbao-firstboot -u nginx | grep -E 'OPENBAO_INITIALISED|Start(ed|ing) nginx' | cut -d' ' -f3-
openbao-firstboot.sh[1012]: [2026-11-07T22:30:41+0000] openbao-firstboot: OPENBAO_INITIALISED 2026-11-07T22:30:41.512893004Z
systemd[1]: Starting nginx.service - A high performance web server and a reverse proxy server...
systemd[1]: Started nginx.service - A high performance web server and a reverse proxy server.

Only SSH and nginx listen on the network; OpenBao (8200, and its cluster port 8201) is on loopback:

sudo ss -tlnH | awk '{print $4}' | sort -u
0.0.0.0:22
0.0.0.0:443
0.0.0.0:80
127.0.0.1:8200
127.0.0.1:8201
127.0.0.53%lo:53
127.0.0.54:53
[::]:22
[::]:443
[::]:80

The bao command line talks to the local listener:

export BAO_ADDR=http://127.0.0.1:8200
bao status -format=json | jq -c '{initialized, sealed, t, n, version, storage_type}'
{"initialized":true,"sealed":false,"t":3,"n":5,"version":"2.7.2","storage_type":"raft"}

The self-test checks OpenBao on loopback and through nginx over HTTPS, checks that a request without a token is refused, and writes, reads and deletes a value with the root token:

sudo openbao-selftest
openbao-selftest: cubbyhole round trip over https://ec2-203-0-113-10.compute-1.amazonaws.com/ OK
OPENBAO_SELFTEST_OK

Step 4: Read your instance's details

/root/openbao-credentials.txt (root only) holds the URLs, the certificate fingerprint, the root token (OPENBAO_ROOT_TOKEN) and the five unseal keys (OPENBAO_UNSEAL_KEY_1 to _5). Show everything except the secrets:

sudo grep -vE '^OPENBAO_(ROOT_TOKEN|UNSEAL_KEY_[0-9])=' /root/openbao-credentials.txt
# OpenBao 2.7.2 - written on first boot by openbao-firstboot.service. Unique to this instance. Keep it secret.
OPENBAO_URL=https://ec2-203-0-113-10.compute-1.amazonaws.com
OPENBAO_UI_URL=https://ec2-203-0-113-10.compute-1.amazonaws.com/ui/
OPENBAO_TLS_SHA256=3F:9A:...:C2
BAO_ADDR=http://127.0.0.1:8200
OPENBAO_UNSEAL_THRESHOLD=3
#
# Sign in to the UI (https://ec2-203-0-113-10.compute-1.amazonaws.com/ui/, method Token) with OPENBAO_ROOT_TOKEN, or on this instance:
#   export BAO_ADDR=http://127.0.0.1:8200
#   export BAO_TOKEN="$(sudo sed -n 's/^OPENBAO_ROOT_TOKEN=//p' /root/openbao-credentials.txt)"
# FIRST STEPS (see the guide): create an admin policy + token, then revoke this root token; copy the five unseal
# keys to separate safe places OFF this instance and delete them from this file. While the keys are here,
# openbao-unseal.service unseals OpenBao on every boot (auto-unseal); without them, unseal with: bao operator unseal
# The certificate is self-signed and unique to this instance (fingerprint above). Your own name:
#   sudo openbao-set-domain vault.example.com      Self-test: sudo openbao-selftest
# User guide: https://www.cloudimg.co.uk/guides/openbao-2-7-2-on-ubuntu-24-04-aws/

To see the root token itself (it starts with s.), run sudo sed -n 's/^OPENBAO_ROOT_TOKEN=//p' /root/openbao-credentials.txt. The full file, with the unseal keys, is sudo cat /root/openbao-credentials.txt.

Step 5: Sign in to the web UI

Open OPENBAO_UI_URL from Step 4 in your browser. The certificate is self-signed and unique to your instance, so the browser warns about it once: compare the fingerprint it shows with OPENBAO_TLS_SHA256 before you continue (or replace the certificate, Step 12). Do not add it to your operating system's trust store. On the sign-in page the method is Token: paste the root token and choose Sign In.

The OpenBao 2.7.2 sign-in page served over HTTPS by the instance: Namespace (Root), Method Token, an empty Token field and the Sign In button

The UI opens on Secrets Engines. Because you signed in with a root token, it warns that it will not keep the token in your browser, so you sign in again after a refresh or in a new window. A fresh instance has no secrets engine of its own yet (the screenshot was taken after Step 8 enabled secret/); Policies lists the ACL policies and Raft Storage shows the single storage node.

Secrets Engines after signing in with the root token: the kv engine secret/ from Step 8, the main navigation (Secrets engines, Access, Policies, Tools, Raft Storage, Seal OpenBao) and the warning that a root token is not stored by the browser

The UI signs in by sending the token to auth/token/lookup-self over HTTPS. The same request on the instance, through nginx, shows what the root token is (the token is read into a header file, never onto a command line). The certificate's directory also holds its private key and is readable by root only, so first copy the public certificate to your home directory; your applications need the same file:

D=$(cat /etc/cloudimg/openbao-domain)
sudo cat /etc/ssl/openbao/openbao.crt > ~/openbao.crt
umask 077
sudo sed -n 's/^OPENBAO_ROOT_TOKEN=/X-Vault-Token: /p' /root/openbao-credentials.txt > ~/root.h
curl -s --cacert ~/openbao.crt --connect-to ::127.0.0.1:443 -H @"$HOME/root.h" \
  "https://$D/v1/auth/token/lookup-self" | jq -c '{policies: .data.policies, ttl: .data.ttl}'
curl -s -o /dev/null -w 'without a token: %{http_code}\n' --cacert ~/openbao.crt \
  --connect-to ::127.0.0.1:443 "https://$D/v1/auth/token/lookup-self"
shred -u ~/root.h
{"policies":["root"],"ttl":0}
without a token: 403

Step 6: Create an admin policy and token

The root token can do anything and never expires. Use it once, to create an administrator policy and a token for day-to-day work, then revoke it (Step 7). The admin token is written to ~/.openbao-admin-token (yours only) and never printed:

export BAO_ADDR=http://127.0.0.1:8200
export BAO_TOKEN="$(sudo sed -n 's/^OPENBAO_ROOT_TOKEN=//p' /root/openbao-credentials.txt)"
bao policy write admin - <<'EOF'
path "*" {
  capabilities = ["create", "read", "update", "patch", "delete", "list", "sudo"]
}
EOF
umask 077
bao token create -orphan -policy=admin -ttl=768h -renewable=true -display-name=admin -field=token > ~/.openbao-admin-token
unset BAO_TOKEN
BAO_TOKEN="$(cat ~/.openbao-admin-token)" bao token lookup -format=json | jq -c '{policies: .data.policies, orphan: .data.orphan, renewable: .data.renewable}'
Success! Uploaded policy: admin
{"policies":["admin","default"],"orphan":true,"renewable":true}

The admin token is created as an orphan (-orphan). A token created without it is a child of the token that created it, and revoking the root token in Step 7 would revoke the admin token with it.

The admin token lasts 32 days; renew it with bao token renew before then, or sign in with a longer-term auth method (userpass, OIDC or AppRole). From here on the guide uses the admin token:

export BAO_ADDR=http://127.0.0.1:8200
export BAO_TOKEN="$(cat ~/.openbao-admin-token)"
bao token capabilities sys/policies/acl/admin
create, delete, list, patch, read, sudo, update

Step 7: Secure the unseal keys and the root token

The unseal keys are stored on the instance so it can unseal itself after a reboot. Anyone who obtains a copy of the instance's disk (a snapshot, a backup) also has the keys, so for production move them off the instance:

  1. Copy the five unseal keys to five separate safe places off the instance (for example, a password manager entry for each of five key holders). Any three unseal the vault. From your workstation:

    bash ssh -i <your-key.pem> ubuntu@<public-ip> "sudo grep '^OPENBAO_UNSEAL_KEY_' /root/openbao-credentials.txt"

  2. Revoke the root token once your admin token works (Step 6). You can create a new root token later with three unseal keys (bao operator generate-root on the instance), so keep the keys safe:

    bash export BAO_ADDR=http://127.0.0.1:8200 BAO_TOKEN="$(sudo sed -n 's/^OPENBAO_ROOT_TOKEN=//p' /root/openbao-credentials.txt)" bao token revoke -self sudo sed -i '/^OPENBAO_ROOT_TOKEN=/d' /root/openbao-credentials.txt

  3. Switch auto-unseal off by deleting the keys from the instance. From the next reboot OpenBao stays sealed until three key holders unseal it (Step 10):

    bash sudo sed -i '/^OPENBAO_UNSEAL_KEY_/d' /root/openbao-credentials.txt sudo systemctl restart openbao-unseal sudo journalctl -u openbao-unseal -o cat | grep -o 'openbao-unseal: .*' | tail -n 1

    text openbao-unseal: auto-unseal is OFF (no unseal keys in /root/openbao-credentials.txt): OpenBao stays sealed; unseal it with 'bao operator unseal' or the UI

    To switch it back on, add three of the key lines (OPENBAO_UNSEAL_KEY_1=...) back to the file.

If you keep auto-unseal on, restrict who can snapshot the volume, and keep the root volume encrypted with a KMS key only your administrators can use.

Step 8: Store and read a secret

Enable a KV version 2 secrets engine at secret/, write a secret and read it back:

export BAO_ADDR=http://127.0.0.1:8200
export BAO_TOKEN="$(cat ~/.openbao-admin-token)"
bao secrets enable -path=secret kv-v2
bao kv put -mount=secret myapp/database username=appuser password="$(openssl rand -hex 16)" > /dev/null
bao kv put -mount=secret billing/stripe api_key="$(openssl rand -hex 16)" > /dev/null
bao kv get -mount=secret -field=username myapp/database; echo
bao kv metadata get -mount=secret -format=json myapp/database | jq -c '{current_version: .data.current_version}'
Success! Enabled the kv-v2 secrets engine at: secret/
appuser
{"current_version":1}

In the web UI the secret is under Secrets engines > secret/ > myapp/ > database. Values are masked until you choose the eye icon, and Metadata and Version show its history.

The myapp/database secret in the OpenBao web UI: keys password and username with their values masked, the Copy, Version 1 and Create new version actions, and the Metadata tab

Step 9: Give an application a read-only token

A policy grants paths; a token carries policies. This policy lets an application read the secrets under secret/myapp/ and nothing else:

export BAO_ADDR=http://127.0.0.1:8200
export BAO_TOKEN="$(cat ~/.openbao-admin-token)"
bao policy write myapp-read - <<'EOF'
path "secret/data/myapp/*" {
  capabilities = ["read"]
}
path "secret/metadata/myapp/*" {
  capabilities = ["read", "list"]
}
EOF
umask 077
bao token create -policy=myapp-read -ttl=24h -display-name=myapp -field=token > ~/.myapp-token
export BAO_TOKEN="$(cat ~/.myapp-token)"
echo "read:  $(bao kv get -mount=secret -field=username myapp/database)"
bao kv put -mount=secret myapp/database username=changed > /dev/null 2>&1 && echo "write: ALLOWED" || echo "write: refused"
bao kv get -mount=secret billing/stripe > /dev/null 2>&1 && echo "other path: ALLOWED" || echo "other path: refused"
bao policy list > /dev/null 2>&1 && echo "sys: ALLOWED" || echo "sys: refused"
Success! Uploaded policy: myapp-read
read:  appuser
write: refused
other path: refused
sys: refused

An application on another host reads the secret over HTTPS with the same token (the server's certificate fingerprint is in Step 4). On the instance, through nginx:

D=$(cat /etc/cloudimg/openbao-domain)
printf 'X-Vault-Token: %s\n' "$(cat ~/.myapp-token)" > ~/.myapp.h
curl -s --cacert ~/openbao.crt --connect-to ::127.0.0.1:443 -H @"$HOME/.myapp.h" \
  "https://$D/v1/secret/data/myapp/database" | jq -r '.data.data.username'
curl -s -o /dev/null -w 'write over HTTPS: %{http_code}\n' --cacert ~/openbao.crt --connect-to ::127.0.0.1:443 \
  -H @"$HOME/.myapp.h" -H 'Content-Type: application/json' -d '{"data":{"username":"changed"}}' "https://$D/v1/secret/data/myapp/database"
shred -u ~/.myapp.h
appuser
write over HTTPS: 403

The policy is also under Policies > ACL Policies > myapp-read in the web UI, where you can edit it or download it.

The myapp-read ACL policy in the OpenBao web UI: read on secret/data/myapp/* and read and list on secret/metadata/myapp/*, with Download policy and Edit policy

From your workstation, check the fingerprint first and then use the API or the bao CLI with BAO_ADDR=https://<openbao-host> and BAO_CACERT pointing at a copy of the certificate (~/openbao.crt from Step 5):

openssl s_client -connect <openbao-host>:443 -servername <openbao-host> </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256

Step 10: Reboot and unseal

OpenBao seals itself whenever it stops. With the unseal keys still on the instance, openbao-unseal.service unseals it at boot and nginx starts after it; your secrets are kept in OpenBao's integrated storage on the root volume:

sudo reboot

After the reboot:

export BAO_ADDR=http://127.0.0.1:8200
bao status -format=json | jq -c '{sealed}'
BAO_TOKEN="$(cat ~/.openbao-admin-token)" bao kv get -mount=secret -field=username myapp/database; echo
sudo journalctl -b -u openbao-unseal -o cat | grep -o 'openbao-unseal: .*' | tail -n 1
{"sealed":false}
appuser
openbao-unseal: unsealed

With auto-unseal switched off (Step 7), OpenBao is sealed after a reboot and the API answers 503 until three key holders unseal it, on the instance or in the web UI. bao operator unseal prompts for one key at a time without echoing it:

export BAO_ADDR=http://127.0.0.1:8200
bao operator unseal
bao operator unseal
bao operator unseal

Step 11: Audit log

Every request is logged in /var/log/openbao/audit.log (owner openbao, mode 0600) as one JSON object per line. Tokens and secret values appear only as HMACs, and the client address is the real TCP peer:

sudo tail -n 1 /var/log/openbao/audit.log | jq -c '{type, path: .request.path, token: (.auth.client_token // "" | .[0:12]), remote: .request.remote_address}'
{"type":"response","path":"secret/data/myapp/database","token":"hmac-sha256:","remote":"127.0.0.1"}

The path is that of the last request; the token always starts hmac-sha256:, never with s..

To check whether a token you hold appears in the log, compute its HMAC with bao audit hash rather than searching for the token itself. logrotate rotates the file daily and keeps 14 days; OpenBao reopens it on rotation.

Step 12: Use your own name and certificate

Point a DNS name at the instance (an Elastic IP keeps the address stable), then make it the server's name. openbao-set-domain renders nginx and OpenBao's API address for the new name, makes a new self-signed certificate for it, and updates the URLs in /root/openbao-credentials.txt. It restarts OpenBao, which seals it; with auto-unseal on it is unsealed again at once:

sudo openbao-set-domain vault.example.com

The new certificate has a new fingerprint (shown as OPENBAO_TLS_SHA256 in the credentials file); copy it again with sudo cat /etc/ssl/openbao/openbao.crt > ~/openbao.crt and give your applications the new copy.

To use a certificate from a public certificate authority instead, run openbao-set-domain first, then install your certificate and key as /etc/ssl/openbao/openbao.crt and /etc/ssl/openbao/openbao.key (root only) and run sudo systemctl reload nginx. Clients then no longer need BAO_CACERT.

Step 13: Operate and harden

Metadata service. OpenBao, nginx and the first boot and unseal services are blocked from the instance metadata service:

systemctl show -p IPAddressDeny openbao nginx openbao-unseal openbao-firstboot
IPAddressDeny=fd00:ec2::254/128 ::ffff:169.254.169.254/128 169.254.169.254/32 168.63.129.16/32

IPAddressDeny=fd00:ec2::254/128 ::ffff:169.254.169.254/128 169.254.169.254/32 168.63.129.16/32

IPAddressDeny=fd00:ec2::254/128 ::ffff:169.254.169.254/128 169.254.169.254/32 168.63.129.16/32

IPAddressDeny=fd00:ec2::254/128 ::ffff:169.254.169.254/128 169.254.169.254/32 168.63.129.16/32

The order of the entries varies between boots.

Endpoints refused from the network. Without a token every API path except health, seal status, leader, init status and sign-in answers 403. nginx also refuses the raw storage, raft join, root token generation and rekey endpoints and any write to sys/init, even with a valid token; run those operations on the instance against http://127.0.0.1:8200:

D=$(cat /etc/cloudimg/openbao-domain)
for p in sys/mounts sys/raw/core sys/generate-root/attempt; do
  printf '%s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' --cacert ~/openbao.crt --connect-to ::127.0.0.1:443 "https://$D/v1/$p")"
done
printf 'PUT sys/init %s\n' "$(curl -s -o /dev/null -w '%{http_code}' -X PUT -d '{}' --cacert ~/openbao.crt --connect-to ::127.0.0.1:443 "https://$D/v1/sys/init")"
curl -s --cacert ~/openbao.crt --connect-to ::127.0.0.1:443 "https://$D/v1/sys/init"
sys/mounts 403
sys/raw/core 403
sys/generate-root/attempt 403
PUT sys/init 403
{"initialized":true}

Backups. Take a snapshot of the integrated storage with the admin token and copy it off the instance. A snapshot is encrypted by OpenBao, and restoring it needs the unseal keys of the vault it came from:

export BAO_ADDR=http://127.0.0.1:8200
BAO_TOKEN="$(cat ~/.openbao-admin-token)" bao operator raft snapshot save ~/openbao-$(date +%F).snap
ls -l ~/openbao-*.snap | awk '{print $1}'
-rw-------

Logs. sudo journalctl -u openbao -u openbao-unseal -u openbao-firstboot. nginx keeps its access log in /var/log/nginx without query strings and logs errors only at crit, because request lines can carry tokens.

Security group. Allow TCP 443 only from your administrators and the hosts whose applications read secrets, and TCP 22 only from your administrators.

Updates. Unattended security upgrades stay on for Ubuntu and nginx. OpenBao is pinned to the release package this image was built with; new OpenBao releases arrive as new versions of this listing. To move to a new version, take a snapshot, launch the new version and restore the snapshot into it with bao operator raft snapshot restore -force (you need the original unseal keys).

Support and licensing

OpenBao is developed by the OpenBao project under the Linux Foundation and distributed under the Mozilla Public License 2.0. The licence is in /usr/share/doc/openbao/copyright:

dpkg-query -W -f='${Package} ${Version}\n' openbao
head -n 3 /usr/share/doc/openbao/copyright
openbao 2.7.2
Copyright (c) 2015 HashiCorp, Inc.

Mozilla Public License, version 2.0

OpenBao is a fork of HashiCorp Vault 1.14, so the licence file keeps the original copyright line.

cloudimg provides the packaging, the first boot automation, the HTTPS front end and the hardening, with 24/7 support by email and live chat at support@cloudimg.co.uk.

All product names are trademarks of their respective owners. cloudimg is not affiliated with or endorsed by the OpenBao project or the Linux Foundation.