Broker audit-core dynamic database credentials
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

This commit is contained in:
codex 2026-08-10 19:36:30 +02:00
parent d140e829eb
commit 9f6bdffec4
5 changed files with 245 additions and 12 deletions

View file

@ -1,5 +1,5 @@
version: 1
updated: "2026-06-27"
updated: "2026-08-10"
owner_repo: railiance-platform
owner_domain: financials
workplan_id: RAILIANCE-WP-0005
@ -24,6 +24,84 @@ grant_classes:
- break-glass
grants:
- id: rapp-postgres/audit-core-runtime
title: audit-core runtime PostgreSQL lease
status: active
grant_class: self-service
credential_type: openbao-database-credential
issuer: openbao
audience: audit-core
description: Short-lived runtime connection to the audit-core database.
openbao:
namespace: openbao
token_role: audit-core-runtime-broker
issuer_policy: credential-broker-audit-core-runtime-issuer
policies: [credential-broker-audit-core-runtime]
disallowed_policies: [root, platform-admin]
mount_paths: [database/creds/audit-core-runtime]
database:
role: audit-core-runtime
host: platform-pg-rw.databases.svc.cluster.local
port: 5432
name: audit_core
env: {PGUSER: username, PGPASSWORD: password, PGHOST: host, PGPORT: port, PGDATABASE: database}
ttl: {default: 15m, max: 1h, renewable: false, requires_human_above: 1h}
actors:
allowed_types: [human-operator, approved-agent, ci-runner]
required_subject_binding: keycape-or-kubernetes-service-account
authorization:
flex_auth_required: false
flex_auth_mode: optional-preflight
approval_required: false
purpose_required: true
allowed_purpose_examples: [audit-core-runtime]
delivery:
allowed: [exec-env]
preferred: exec-env
denied: [chat, state-hub-body, git, command-line-token-argument, llm-prompt]
exec_env: {child_only: true, redact_logs: true}
audit: {openbao_audit_required: true, state_hub_metadata_allowed: true, record_secret_values: false}
revocation: {required: true, by_accessor: true, on_exec_exit: true, on_denied_request: false}
- id: rapp-postgres/audit-core-migration
title: audit-core migration PostgreSQL lease
status: active
grant_class: self-service
credential_type: openbao-database-credential
issuer: openbao
audience: audit-core
description: Short-lived migration connection to the audit-core database.
openbao:
namespace: openbao
token_role: audit-core-migration-broker
issuer_policy: credential-broker-audit-core-migration-issuer
policies: [credential-broker-audit-core-migration]
disallowed_policies: [root, platform-admin]
mount_paths: [database/creds/audit-core-migration]
database:
role: audit-core-migration
host: platform-pg-rw.databases.svc.cluster.local
port: 5432
name: audit_core
env: {PGUSER: username, PGPASSWORD: password, PGHOST: host, PGPORT: port, PGDATABASE: database}
ttl: {default: 10m, max: 30m, renewable: false, requires_human_above: 30m}
actors:
allowed_types: [human-operator, approved-agent, ci-runner]
required_subject_binding: keycape-or-kubernetes-service-account
authorization:
flex_auth_required: false
flex_auth_mode: optional-preflight
approval_required: false
purpose_required: true
allowed_purpose_examples: [audit-core-migration]
delivery:
allowed: [exec-env]
preferred: exec-env
denied: [chat, state-hub-body, git, command-line-token-argument, llm-prompt]
exec_env: {child_only: true, redact_logs: true}
audit: {openbao_audit_required: true, state_hub_metadata_allowed: true, record_secret_values: false}
revocation: {required: true, by_accessor: true, on_exec_exit: true, on_denied_request: false}
- id: ops-warden/warden-sign
title: Ops Warden OpenBao SSH signing smoke token
status: pilot

View file

@ -0,0 +1,17 @@
# rapp-postgres package boundary
`rapp-postgres` owns the PostgreSQL package manifests, consumer declarations,
role/database provisioning surface, isolation tests, database-specific
operations, and recovery procedure for the shared `platform-pg` service.
`railiance-platform` retains cluster-wide governance: the CNPG operator,
Kubernetes access, storage-class policy, monitoring substrate, approved S3
backup target and its credentials, and the OpenBao credential-broker grant
catalog. It configures the OpenBao database secrets engine from the creation
and revocation statements owned in the rapp-postgres playbook; it does not copy
those procedures into this repo.
The package never commits credentials. A consumer request is declared and
provisioned by rapp-postgres, while the workload receives a short-lived lease
through the platform broker. The upstream backup target and database-engine
bootstrap are governance prerequisites, not package-owned secret material.

View file

@ -29,7 +29,7 @@ REQUIRED_DENIED_MODES = {
"llm-prompt",
}
ALLOWED_CREDENTIAL_TYPES = {"openbao-token"}
ALLOWED_CREDENTIAL_TYPES = {"openbao-token", "openbao-database-credential"}
ALLOWED_GRANT_CLASSES = {"self-service", "approval-required", "break-glass"}
ALLOWED_GRANT_STATUSES = {"pilot", "active", "deprecated", "disabled"}
DISALLOWED_POLICIES = {"root", "platform-admin"}
@ -151,6 +151,16 @@ def validate_grant(
)
require_list(openbao.get("mount_paths"), f"{prefix}.openbao.mount_paths", errors)
if credential_type == "openbao-database-credential":
database = require_dict(grant_obj.get("database"), f"{prefix}.database", errors)
require_nonempty_string(database.get("role"), f"{prefix}.database.role", errors)
require_nonempty_string(database.get("host"), f"{prefix}.database.host", errors)
require_nonempty_string(database.get("name"), f"{prefix}.database.name", errors)
env = require_dict(database.get("env"), f"{prefix}.database.env", errors)
required_fields = {"username", "password", "host", "port", "database"}
if set(env.values()) != required_fields:
errors.append(f"{prefix}.database.env must map exactly {sorted(required_fields)}")
ttl = require_dict(grant_obj.get("ttl"), f"{prefix}.ttl", errors)
default_ttl = ttl_seconds(ttl.get("default"), f"{prefix}.ttl.default", errors)
max_ttl = ttl_seconds(ttl.get("max"), f"{prefix}.ttl.max", errors)
@ -228,6 +238,8 @@ def validate_grant(
)
if preferred and preferred not in allowed:
errors.append(f"{prefix}.delivery.preferred must be in delivery.allowed")
if credential_type == "openbao-database-credential" and allowed != {"exec-env"}:
errors.append(f"{prefix}.delivery.allowed must be exec-env only for database credentials")
if "local-token-file" in allowed:
local_file = require_dict(
delivery.get("local_token_file"),

View file

@ -125,7 +125,10 @@ def get_grant(catalog: dict[str, Any], grant_id: str) -> dict[str, Any]:
fail("catalog grants must be a list")
for grant in grants:
if isinstance(grant, dict) and grant.get("id") == grant_id:
if grant.get("credential_type") != "openbao-token":
if grant.get("credential_type") not in {
"openbao-token",
"openbao-database-credential",
}:
fail(
f"unsupported credential_type for {grant_id}: {grant.get('credential_type')}"
)
@ -414,6 +417,7 @@ class BaoRunner:
dry_run: bool,
use_token_helper: bool,
issuer_token: str | None,
local_bao: bool = False,
) -> None:
self.kubectl_parts = shlex.split(kubectl)
self.namespace = namespace
@ -421,6 +425,7 @@ class BaoRunner:
self.dry_run = dry_run
self.use_token_helper = use_token_helper
self.issuer_token = issuer_token
self.local_bao = local_bao
def run(
self, args: list[str], *, input_text: str | None = None, quiet: bool = False
@ -429,6 +434,28 @@ class BaoRunner:
print("DRY-RUN: bao " + shlex.join(args))
return subprocess.CompletedProcess(args, 0, "", "")
if self.local_bao:
if not self.issuer_token:
raise RuntimeError("issuer token is required for --local-bao")
env = os.environ.copy()
env["BAO_TOKEN"] = self.issuer_token
env["VAULT_TOKEN"] = self.issuer_token
result = subprocess.run(
["bao", *args], input=input_text, env=env,
capture_output=True, text=True, check=False,
)
if result.returncode != 0:
if result.stdout and not quiet:
print(redact(result.stdout, [self.issuer_token]), end="")
if result.stderr:
print(redact(result.stderr, [self.issuer_token]), file=sys.stderr, end="")
raise SystemExit(result.returncode)
if result.stdout and not quiet:
print(redact(result.stdout, [self.issuer_token]), end="")
if result.stderr and not quiet:
print(redact(result.stderr, [self.issuer_token]), file=sys.stderr, end="")
return result
if self.use_token_helper:
cmd = (
self.kubectl_parts
@ -474,6 +501,54 @@ class BaoRunner:
print(redact(result.stderr), file=sys.stderr, end="")
return result
def run_with_token(
self, token: str, args: list[str], *, quiet: bool = False
) -> subprocess.CompletedProcess[str]:
if self.dry_run:
print("DRY-RUN: bao " + shlex.join(args))
return subprocess.CompletedProcess(args, 0, "", "")
if self.local_bao:
env = os.environ.copy()
env["BAO_TOKEN"] = token
env["VAULT_TOKEN"] = token
result = subprocess.run(
["bao", *args], env=env, capture_output=True, text=True, check=False
)
if result.returncode != 0:
if result.stdout and not quiet:
print(redact(result.stdout, [token]), end="")
if result.stderr:
print(redact(result.stderr, [token]), file=sys.stderr, end="")
raise SystemExit(result.returncode)
if result.stdout and not quiet:
print(redact(result.stdout, [token]), end="")
if result.stderr and not quiet:
print(redact(result.stderr, [token]), file=sys.stderr, end="")
return result
cmd = (
self.kubectl_parts
+ [
"exec", "-i", "-n", self.namespace, self.pod, "--", "sh", "-c",
'read -r BAO_TOKEN; export BAO_TOKEN; export VAULT_TOKEN="$BAO_TOKEN"; exec bao "$@"',
"sh",
]
+ args
)
result = subprocess.run(
cmd, input=token + "\n", capture_output=True, text=True, check=False
)
if result.returncode != 0:
if result.stdout and not quiet:
print(redact(result.stdout, [token]), end="")
if result.stderr:
print(redact(result.stderr, [token]), file=sys.stderr, end="")
raise SystemExit(result.returncode)
if result.stdout and not quiet:
print(redact(result.stdout, [token]), end="")
if result.stderr and not quiet:
print(redact(result.stderr, [token]), file=sys.stderr, end="")
return result
def token_create_args(
grant: dict[str, Any], ttl: str, wrap_ttl: str | None = None
@ -501,6 +576,16 @@ def parse_token_create(stdout: str) -> tuple[str, str]:
return token, accessor
def parse_database_credential(stdout: str) -> tuple[str, str, str]:
try:
payload = json.loads(stdout)
return payload["lease_id"], payload["data"]["username"], payload["data"]["password"]
except Exception as exc: # noqa: BLE001
raise SystemExit(
f"ERROR: could not parse database credential response: {exc}"
) from exc
def parse_wrap_create(stdout: str) -> dict[str, Any]:
try:
payload = json.loads(stdout)
@ -793,6 +878,12 @@ def command_exec(
args=args, grant=grant, ttl=ttl, purpose=args.purpose, delivery="exec-env"
)
extra_env, program = split_env_prefix(args.command)
credential_type = grant["credential_type"]
child_env = [*extra_env.keys()]
if credential_type == "openbao-token":
child_env.append("VAULT_TOKEN")
else:
child_env.extend((grant.get("database") or {}).get("env", {}).keys())
if args.dry_run:
emit_json(
{
@ -804,7 +895,7 @@ def command_exec(
"delivery_mode": "exec-env",
"authorization_mode": authz.mode,
"decision_id": authz.decision_id,
"child_env": sorted([*extra_env.keys(), "VAULT_TOKEN"]),
"child_env": sorted(child_env),
"child_command": program,
}
)
@ -837,6 +928,28 @@ def command_exec(
)
result = runner.run(token_create_args(grant, ttl), quiet=True)
token, accessor = parse_token_create(result.stdout)
lease_handle = accessor
secret_values = [token]
injected: dict[str, str]
if credential_type == "openbao-token":
injected = {"VAULT_TOKEN": token}
else:
database = grant.get("database") or {}
role = database.get("role")
env_map = database.get("env") or {}
credential = runner.run_with_token(
token, ["read", "-format=json", f"database/creds/{role}"], quiet=True
)
lease_handle, username, password = parse_database_credential(credential.stdout)
values = {
"username": username,
"password": password,
"host": str(database.get("host", "")),
"port": str(database.get("port", "5432")),
"database": str(database.get("name", "")),
}
injected = {name: values[field] for name, field in env_map.items()}
secret_values.extend((username, password))
record_state_hub(
args,
state_hub_metadata(
@ -847,12 +960,12 @@ def command_exec(
ttl=ttl,
delivery="exec-env",
authz=authz,
lease_handle=accessor,
lease_handle=lease_handle,
),
)
env = os.environ.copy()
env.update(extra_env)
env["VAULT_TOKEN"] = token
env.update(injected)
exit_code = 1
try:
child = subprocess.run(
@ -860,14 +973,14 @@ def command_exec(
)
exit_code = child.returncode
if child.stdout:
print(redact(child.stdout, [token]), end="")
print(redact(child.stdout, secret_values), end="")
if child.stderr:
print(redact(child.stderr, [token]), file=sys.stderr, end="")
print(redact(child.stderr, secret_values), file=sys.stderr, end="")
return child.returncode
finally:
runner.run(
["write", "auth/token/revoke-accessor", f"accessor={accessor}"], quiet=True
)
if credential_type == "openbao-database-credential":
runner.run(["lease", "revoke", lease_handle], quiet=True)
runner.run(["write", "auth/token/revoke-accessor", f"accessor={accessor}"], quiet=True)
record_state_hub(
args,
state_hub_metadata(
@ -878,7 +991,7 @@ def command_exec(
ttl=ttl,
delivery="exec-env",
authz=authz,
lease_handle=accessor,
lease_handle=lease_handle,
exit_code=exit_code,
),
)
@ -962,6 +1075,10 @@ def build_parser() -> argparse.ArgumentParser:
action="store_true",
help="Use the OpenBao CLI token helper inside the pod",
)
parser.add_argument(
"--local-bao", action="store_true",
help="Run the workstation bao CLI instead of execing into the server pod",
)
parser.add_argument("--lease-dir", type=Path, default=DEFAULT_LEASE_DIR)
parser.add_argument(
"--actor", default=os.environ.get("CREDENTIAL_ACTOR", DEFAULT_ACTOR)
@ -1063,6 +1180,7 @@ def main() -> int:
dry_run=args.dry_run,
use_token_helper=args.use_token_helper,
issuer_token=issuer_token,
local_bao=args.local_bao,
)
if args.command_name == "request":

View file

@ -50,6 +50,14 @@ def sample_grant() -> dict:
class CredentialHelperTests(unittest.TestCase):
def test_database_credential_response_is_parsed_without_emission(self) -> None:
lease_id, username, password = credential.parse_database_credential(
'{"lease_id":"database/creds/runtime/lease-1","data":{"username":"leased-user","password":"leased-secret"}}'
)
self.assertEqual(lease_id, "database/creds/runtime/lease-1")
self.assertEqual(username, "leased-user")
self.assertEqual(password, "leased-secret")
def test_ttl_over_max_is_rejected(self) -> None:
with self.assertRaises(SystemExit):
credential.validate_issue_request(