OpenBao Secret Structure
The portal uses OpenBao (Vault-compatible) as a secrets backend.
Connection Details
| Setting | Value |
|---|---|
| Address (in-cluster) | http://openbao.openbao.svc.cluster.local:8200 |
| Token secret | dev-portal-secrets → VAULT_TOKEN in namespace dev-portal |
| Secrets engine | KV v2 |
| Mount path | ai-factory |
Per-User Secret Path
ai-factory/data/apisix/<username>
├── key: "<32-char base64url API key>"
├── allowed_models: ["route-a", "route-b"]
├── email: "jane.doe@example.org" (these three: present once the owner has
├── name: "Jane Doe" logged in or generated a key since they
└── sub: "<Authentik user id>" were introduced)
| Field | Description |
|---|---|
key | The user's current API key |
allowed_models | Route names the user has been granted access to |
email, name, sub | Who the drawer belongs to. Authentik is authoritative for these; the copy here comes from the OIDC userinfo when a key is generated and is refreshed on every OIDC login, best effort, so it can be stale. An empty claim never overwrites a stored value. A login never creates a drawer, so a user who has not generated a key has nothing here, and drawers created before these fields existed get them on the owner's next login (there is no backfill job). Once consumer names are Authentik's short usernames, these are how you find and contact the person behind one. |
Manual Operations
In exceptional circumstances you can update secrets directly using the bao CLI:
# Read a user's secret
bao kv get ai-factory/apisix/<username>
# Grant model access manually
bao kv patch ai-factory/apisix/<username> allowed_models='["route-a","route-b"]'
# Delete a user's secret permanently (all versions; the portal does the same on revoke)
bao kv metadata delete ai-factory/apisix/<username>
Direct edits to OpenBao bypass the portal's route whitelist sync. Always prefer the Admin panel to keep APISIX and OpenBao in sync.
Renaming a User
The drawer label, the ExternalSecret, the Kubernetes Secret and the ApisixConsumer are all named after the username Authentik sends. If that username changes (for example when Authentik switches from email addresses to short usernames), nothing renames itself: the old objects survive with a still-valid key and the user's next key generation creates a second set. Do the rename together with the change in Authentik, not afterwards. By hand:
- Copy the drawer to the new label (
bao kv getthe old one,bao kv putthe new one with the same fields), thenbao kv metadata deletethe old label. - Create the ExternalSecret and ApisixConsumer under the new name the same way the portal does (
ProvisionResourcesink8s.go: the ExternalSecret'sremoteRef.keyisapisix/<new label>), then delete the old pair. - Update any hand-written consumer or secret names in the infra repo (KFP door and MLflow bridge lists in
values-dev.yaml).
sub is Authentik's own id for the user and survives a username or email change, so it is how you confirm that an old drawer and a new one belong to the same person. It is per Authentik instance: dev and prod give the same person different values.
Rotating the OpenBao Token
- Generate a new token in OpenBao with the required policy
- Update the Kubernetes secret:
kubectl patch secret dev-portal-secrets -n dev-portal \
--type='json' \
-p='[{"op":"replace","path":"/data/VAULT_TOKEN","value":"'$(echo -n "<new-token>" | base64)'"}]' - Restart the portal API:
kubectl rollout restart deployment/dev-portal-api -n dev-portal
Required OpenBao Policy
path "ai-factory/data/apisix/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "ai-factory/metadata/apisix/*" {
capabilities = ["list", "delete"]
}