A policy file can be valid HCL and still grant the wrong access. The more useful test is not whether the file can be uploaded, but whether a token made from it can read the one path it needs and is refused everywhere else. That distinction is easy to miss when a policy change is reviewed as a few lines of text and then applied directly to a shared OpenBao cluster.
This guide builds a disposable OpenBao 2.6.2 development server, mounts a temporary KV v2 engine, and tests a narrow read policy with both an allowed and a denied request. A successful result is specific: the limited token reads ci/build/release, receives read from the capabilities command for that path, and receives deny plus an HTTP 403 for ci/admin/config. Nothing in this procedure needs an existing secret service or production credential.
OpenBao 2.6.2 was released on 18 August 2026. Its release notes include fixes for two security advisories, including a core issue concerning internal operation types and a PKI IP-SAN restriction issue. That is a reason to put a controlled upgrade and policy regression test on the same maintenance plan. It is not evidence that a particular deployment is affected. Check the release notes, upgrade guidance, and the configuration of the service being changed before making that determination.
Start with the policy question
Assume a CI job needs to retrieve one build value from a KV v2 mount. It should not be able to inspect administrative configuration in the same mount. The policy below deliberately grants only read to the KV v2 API path for the build subtree:
path "ci/data/build/*" {
capabilities = ["read"]
}
The data component is important. KV v2 exposes a versioned API; the logical CLI path ci/build/release is translated to ci/data/build/release for a data read. A policy written for ci/build/* may look plausible but does not authorize the KV v2 data endpoint. Write policies against the API path that OpenBao evaluates, then test them through the same CLI or API style the workload will use.
Keep this file in the repository that owns the workload configuration, not in a shell history or an administrator’s home directory. It contains authorization intent and should be reviewed with the same care as an IAM policy. The test data below is synthetic. Do not copy a real secret into a development server just to prove the policy works.
Download a pinned OpenBao release
Use a release archive and its published checksum manifest rather than a floating download URL. The following Linux x86-64 example downloads OpenBao 2.6.2 into a disposable directory:
set -eu
version=2.6.2
archive="openbao_${version}_linux_amd64.tar.gz"
base="https://github.com/openbao/openbao/releases/download/v${version}"
curl -fsSLO "$base/$archive"
curl -fsSLO "$base/checksums.txt"
grep " $archive$" checksums.txt | sha256sum -c -
tar -xzf "$archive"
./bao version
Do not extract or execute the archive until the checksum command prints OK. The grep selects the manifest entry for the chosen platform, rather than validating an unrelated archive. In the isolated Linux amd64 validation run for this article, the checksum check completed successfully and bao version reported OpenBao 2.6.2.
For a long-lived server, prefer the packaging and signature-verification process documented by the OpenBao project and pin the selected package version in configuration management. The archive is useful here because the lab must remain separate from the host’s package state.
Make policy parsing an early gate
Format the candidate policy before a server exists. bao policy fmt catches HCL syntax errors and normalizes the file, which makes a review diff easier to read.
./bao policy fmt ci-read.hcl
This command does not prove the authorization boundary. It only proves that the policy parses. A malformed list such as the following was rejected during validation with an error saying that the parser expected a comma or the end of the list:
path "ci/data/build/*" {
capabilities = ["read"
}
Treat that failure as useful feedback before a change reaches an API. Correct the syntax, rerun the formatter, and review the resulting diff. Do not use formatting success as a reason to skip the token test; a syntactically correct wildcard can still cover too much of the mount.
Use a short-lived development server
OpenBao development mode runs in memory, starts unsealed, and generates sensitive material. It is suitable only for a locally controlled test. Do not expose it on a network interface, reuse its root token, or point application clients at it.
Start it with an explicit temporary root token and a loopback-only address. Replace the placeholders with values appropriate for an isolated local lab; do not use this command against an existing service.
./bao server -dev \
-dev-root-token-id=<temporary-lab-token> \
-dev-listen-address=<loopback-address>
In another terminal, set the address and temporary lab token. Then create a KV v2 mount and two synthetic values: one that the limited token should retrieve and one that it must not retrieve.
export BAO_ADDR=http://<bao-address>
export BAO_TOKEN=<temporary-lab-token>
./bao secrets enable -path=ci kv-v2
./bao kv put ci/build/release build=2026-08-19
./bao kv put ci/admin/config mode=private
./bao policy write ci-read ci-read.hcl
The mount name, sample key, and values are deliberately generic. In a real pipeline, preserve the path structure but replace the test values with harmless data. The root token is used only to set up this disposable fixture. It must not be passed to the workload being tested.
Issue a limited token and test both sides
Create a token attached only to ci-read, capture it without printing it, and use that token for every check that follows:
limited_token=$(./bao token create -policy=ci-read -format=json | \
python3 -c 'import sys, json; print(json.load(sys.stdin)["auth"]["client_token"])')
BAO_TOKEN="$limited_token" ./bao token capabilities ci/data/build/release
BAO_TOKEN="$limited_token" ./bao kv get -field=build ci/build/release
BAO_TOKEN="$limited_token" ./bao token capabilities ci/data/admin/config
BAO_TOKEN="$limited_token" ./bao kv get ci/admin/config
The first capability check should print read, and the KV command should return the synthetic build value. The capability check for the administrative path should print deny. The final read is intentionally expected to fail with a permission-denied response. Make that expected failure an assertion in CI rather than allowing the shell to ignore it:
if BAO_TOKEN="$limited_token" ./bao kv get ci/admin/config; then
echo "policy allowed an unexpected read" >&2
exit 1
fi
The isolated test produced exactly that split: read and the expected value for ci/build/release, then deny and an HTTP 403 permission-denied response for ci/admin/config. Testing both paths matters. A successful allowed read alone cannot detect a wildcard that accidentally gives the job access to the administrative subtree.
Move the test into the change process
Use a disposable OpenBao instance for pull-request or pre-deployment tests. The fixture should create its own mount and synthetic data, upload the candidate policy, issue a limited token, run the allowed and denied assertions, and then terminate the server. Keep raw logs out of the repository because dev-mode startup output and CLI diagnostics can expose temporary tokens, addresses, or file paths.
A production rollout needs additional checks. Confirm the target mount’s version, namespace if namespaces are in use, auth method, and exact workload identity. Review whether the application needs list, create, update, or other capabilities in addition to read; adding them because a test failed without understanding the API path defeats the purpose of least privilege. Finally, test the actual workload identity in a non-production environment before changing the production policy.
The finished test is small but gives a policy review an observable boundary. OpenBao accepted the policy, the limited token read only the intended KV v2 path, and a nearby administrative path was refused. That is stronger evidence than a clean HCL file and gives an upgrade or policy change a repeatable regression check.