Loading

Back to Home

Homelab Rebuild Part 19: Logging In to OpenBao with Cloudflare Access and Google

| October 14, 2026 | 6 min to read

Intro

Since Part 5, the only way into OpenBao was the root token: a credential with unlimited power and no expiry, copied from a file on my laptop whenever I needed to read a secret. That’s fine for bootstrapping and bad for daily use.

I already pay for Cloudflare, and Cloudflare Access can act as an OIDC provider for any application. So the plan was: OpenBao trusts Cloudflare Access, Cloudflare Access trusts Google, and only my Gmail address gets in.

TL;DR: Cloudflare Access “SaaS application” (OIDC) in front, Google as the login method, and OpenBao’s oidc auth method with one role bound to my email. Logins give a 1-hour admin token (8 hours max), in the web UI and the CLI. The root token is now break-glass only. The one surprise was my own setup script talking plain HTTP to a TLS-only OpenBao.

The Situation

  • OpenBao 2.7.1: three pods with Raft, TLS from my lab CA, UI and API reachable only over Tailscale (https://openbao.<tailnet>.ts.net)
  • Auth methods before: kubernetes (for External Secrets) and the root token
  • Cloudflare Zero Trust team <team>.cloudflareaccess.com
  • Tetragon watches outbound connections from the openbao namespace (Part 11) and alerts on anything outside the cluster and LAN (Part 17)

Setup

The chain of trust

browser or bao CLI
  -> OpenBao (auth/oidc, role admin)
     -> Cloudflare Access (SaaS app "OpenBao", OIDC)
        -> Google (identity provider, my Google account's 2FA)

OpenBao never talks to Google. It only trusts Cloudflare’s ID token and checks one claim: email.

Google as the login method

In Google Cloud I created an OAuth client of type Web application, with the consent screen in Testing mode and my Gmail as the only test user. Its redirect URI is Cloudflare’s callback:

https://<team>.cloudflareaccess.com/cdn-cgi/access/callback

Cloudflare’s menus have moved since most guides were written. Identity providers are now under Zero Trust > Integrations > Identity providers, not under Settings. The Test button showed exactly the identity OpenBao would see:

{
  "name": "Frans Caisar Ramadhan",
  "email": "<my gmail>"
}

The OpenBao app in Cloudflare Access

Under Access controls > Applications > SaaS application, with protocol OIDC:

SettingValue
Redirect URLshttps://openbao.<tailnet>.ts.net/ui/vault/auth/oidc/oidc/callback and http://localhost:8250/oidc/callback
Scopesopenid, email, profile
PKCEoff (OpenBao uses the client secret)
Identity providersGoogle only
PolicyAllow, include one email address

The first redirect URL is for the web UI; the second is where the bao CLI listens during bao login.

The OpenBao side, as a script

I wanted this reproducible, so it’s a script in the repo, oidc.sh. It writes an admin policy, enables auth/oidc, and creates the role. The root token and the client secret come in on stdin, so neither ends up in shell history or process arguments:

{ jq -r .root_token ~/.config/openbao/init-lab.json; pbpaste; } \
  | kubectl -n openbao exec -i openbao-0 -c openbao -- sh -c "$(cat kubernetes/apps/openbao/oidc.sh)" oidc \
      "https://<team>.cloudflareaccess.com/cdn-cgi/access/sso/oidc/<client-id>" "<client-id>" \
      "<my gmail>" "openbao.<tailnet>.ts.net"

The role is written as a JSON body. I didn’t trust the CLI to parse a map like bound_claims from a plain key=value string:

{
  "role_type": "oidc",
  "user_claim": "email",
  "bound_claims": {"email": ["<my gmail>"]},
  "oidc_scopes": ["openid", "email", "profile"],
  "allowed_redirect_uris": [
    "https://openbao.<tailnet>.ts.net/ui/vault/auth/oidc/oidc/callback",
    "http://localhost:8250/oidc/callback"
  ],
  "token_policies": ["admin"],
  "token_ttl": "1h",
  "token_max_ttl": "8h"
}

Before the script read the clipboard, I checked what was on it without printing it: 64 characters, no whitespace, and not equal to the client ID. The Access app and Google each have a client secret, and pasting the wrong one is an easy mistake.

Letting OpenBao reach Cloudflare

OpenBao fetches Cloudflare’s discovery document and signing keys, and exchanges the login code for a token. Those are outbound connections from openbao to the internet, which is exactly what Tetragon’s unexpected-egress policy reports. I added Cloudflare’s published IPv4 ranges (from api.cloudflare.com/client/v4/ips) to that namespace’s allow list, and nowhere else. After setup, Tetragon had exported zero events from openbao.

Issue 1: My own script spoke HTTP to a TLS-only server

Symptom

The first run stopped at its first command:

Error uploading policy: Error making API request.
URL: PUT http://127.0.0.1:8200/v1/sys/policies/acl/admin
Code: 400. Raw Message:
Client sent an HTTP request to an HTTPS server.

Nothing had changed yet, since the policy write was the first step.

Cause

I had copied the script’s header from the older bootstrap.sh, which sets BAO_ADDR=http://127.0.0.1:8200. That was true before OpenBao got TLS. The pods already carry the right values:

BAO_ADDR=https://127.0.0.1:8200
BAO_CACERT=/openbao/userconfig/openbao-tls/ca.crt

Fix

Remove the override from both scripts and use the pod’s environment. The second run went through:

Success! Uploaded policy: admin
Success! Enabled oidc auth method at: oidc/
oidc ready: role admin for <my gmail>

OpenBao validates the discovery URL when the config is saved, so this also proved it could reach Cloudflare.

Result

Web UI: Method OIDC, a popup to Cloudflare Access, Google sign-in, and back into OpenBao. OpenBao recorded an identity alias for my email on the oidc mount.

CLI: bao 2.7.1 (the server’s version) now comes from the repo’s mise.toml. With -no-store, nothing touches disk, and I printed only the token’s metadata:

BAO_ADDR=https://openbao.<tailnet>.ts.net bao login -method=oidc -no-store -format=json role=admin
# policies: ['admin', 'default'] | ttl: 3600 s | renewable: True
BeforeAfter
Daily loginroot token from a fileGoogle, through Cloudflare Access
Token lifetimeno expiry1 hour, 8 hours max
Who can log inwhoever has the fileone email address, checked by Cloudflare and again by OpenBao
Second factornonemy Google account’s 2FA
Root tokendaily usebreak-glass

Lessons

  1. An identity proxy can be an OIDC provider. Cloudflare Access sat in front of Google and gave OpenBao a standard OIDC issuer, with its own policy as a second check.
  2. Bind the role to a claim, not just to a login. Even if the Access policy were loosened, OpenBao’s bound_claims still accepts only one email.
  3. Keep secrets out of arguments and chat. Clipboard to stdin, checked by length and shape, never printed.
  4. Copied boilerplate carries old assumptions. One stale BAO_ADDR line from before TLS broke a new script and had been wrong in the old one too.
  5. A new outbound dependency needs a runtime-policy change. Without the Cloudflare ranges, every login would have triggered a security alert.

What’s left: moving the root token and recovery keys off my laptop into a password manager.

Series

This post is part of Homelab Rebuild:

  1. Part 1: Upgrading Proxmox VE 8 to 9
  2. Part 2: Starting Over When the IaC Is Broken
  3. Part 3: When Tailscale Breaks Your Cilium Gateway
  4. Part 4: Living on the Tailnet
  5. Part 5: Secrets Without Secrets in Git
  6. Part 6: Certificates, DNS and a Private CA
  7. Part 7: Self-Hosted CI and Getting Back to IaC
  8. Part 8: Stateful Workloads on Unequal Hardware
  9. Part 9: Going Public Safely with Cloudflare Tunnel
  10. Part 10: When the Hypervisor Went Quiet
  11. Part 11: Hardening the Cluster in Five Layers
  12. Part 12: Observability with Coroot, and Losing pve-01
  13. Part 13: The Host That Kept Losing Power
  14. Part 14: Observability, Take Two with OpenTelemetry
  15. Part 15: Requests and Limits for Everything
  16. Part 16: Handing the Cluster to FluxCD
  17. Part 17: Making the Guardrails Earn Their Keep
  18. Part 18: When the API Server Outgrew Its VM
  19. Part 19: Logging In to OpenBao with Cloudflare Access and Google (this post)

References