Back to Home

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

| October 14, 2026 | 7 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 is fine for bootstrapping (the first setup of a new system) 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 (for emergencies). 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 moved its menus after most guides were written. Identity providers are now under Zero Trust > Integrations > Identity providers, not under Settings. The Test button showed the exact identity that OpenBao sees later:

{
  "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 to be reproducible, so it is 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"

I wrote the role as a JSON body. I did not 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. Tetragon’s unexpected-egress policy reports exactly that kind of connection. 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 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 changed yet, because the policy write was the first step.

Cause

I copied the script’s header from the older bootstrap.sh. That script 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 it saves the configuration. So this also proved that OpenBao can 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 goes to disk. 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 only to a login. If someone loosens the Access policy, 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. It was also wrong in the old one.
  5. A new outbound dependency needs a runtime-policy change. Without the Cloudflare ranges, every login triggers a security alert.

What is 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)
  20. Part 20: Headlamp SSO, or Teaching kube-apiserver to Trust Cloudflare Access
  21. Part 21: pve-02 Loses Power, and Pi-hole Has Nowhere to Go
  22. Part 22: Turning Four Days of Fixes into AI Runbooks
  23. Part 23: microVM Sandboxes on Proxmox
  24. Part 24: Pi-hole Without a Home Host
  25. Part 25: It Was the Router
  26. Part 26: TLS on the Front, Plaintext Behind It
  27. Part 27: One Bad Power Adapter, Two Different Deaths
  28. Part 28: Observability, Take Three: Making It Worth Looking At
  29. Part 29: One Meter for Every Model, LiteLLM on Workers AI
  30. Part 30: Proxmox Joins the SSO, and a Plan Token Loses Admin
  31. Part 31: First Backups, Postgres, OpenBao and etcd to R2
  32. Part 32: Alerts That Reach Me, Even When the Lab Is Dead
  33. Part 33: The Firewall Held, the Tailnet Did Not
  34. Part 34: From Watching to Blocking, and a Record of Who Did What
  35. Part 35: Wazuh Without the 8 GB, Endpoint Security for One Mac and Three Hosts
  36. Part 36: The Fans Were Loud, the CPUs Were Idle
  37. Part 37: A Power Trip, a Search Domain and a Wildcard
  38. Part 38: An Autonomous AI Pentester in a Sealed Box
  39. Part 39: Wasm Pods on k0s, and the Label That Kept Them Packed

References