Add credential-change delegated applier flow
This commit is contained in:
File diff suppressed because it is too large
Load Diff
217
scripts/openbao-apply-credential-change-appliers.py
Executable file
217
scripts/openbao-apply-credential-change-appliers.py
Executable file
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
REPO_DIR = Path(__file__).resolve().parents[1]
|
||||
|
||||
APPLIERS: dict[str, dict[str, Any]] = {
|
||||
"nonprod": {
|
||||
"title": "Credential change non-production metadata applier",
|
||||
"policy_name": "credential-change-nonprod-applier",
|
||||
"policy_file": "openbao/policies/credential-change-nonprod-applier.hcl",
|
||||
"token_role": "credential-change-nonprod-applier",
|
||||
"max_ttl": "1h",
|
||||
},
|
||||
"prod": {
|
||||
"title": "Credential change production metadata applier",
|
||||
"policy_name": "credential-change-prod-applier",
|
||||
"policy_file": "openbao/policies/credential-change-prod-applier.hcl",
|
||||
"token_role": "credential-change-prod-applier",
|
||||
"max_ttl": "30m",
|
||||
},
|
||||
}
|
||||
|
||||
DISALLOWED_POLICIES = ("root", "platform-admin")
|
||||
|
||||
|
||||
class BaoRunner:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
kubectl: str,
|
||||
namespace: str,
|
||||
release: str,
|
||||
dry_run: bool,
|
||||
use_token_helper: bool,
|
||||
token: str | None,
|
||||
) -> None:
|
||||
self.kubectl_parts = shlex.split(kubectl)
|
||||
self.namespace = namespace
|
||||
self.pod = f"{release}-0"
|
||||
self.dry_run = dry_run
|
||||
self.use_token_helper = use_token_helper
|
||||
self.token = token
|
||||
|
||||
def run(
|
||||
self, args: list[str], input_text: str | None = None
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
rendered = "bao " + shlex.join(args)
|
||||
if self.dry_run:
|
||||
print(f"DRY-RUN: {rendered}")
|
||||
return subprocess.CompletedProcess(args, 0, "", "")
|
||||
|
||||
if self.use_token_helper:
|
||||
cmd = (
|
||||
self.kubectl_parts
|
||||
+ ["exec", "-i", "-n", self.namespace, self.pod, "--", "bao"]
|
||||
+ args
|
||||
)
|
||||
proc_input = input_text
|
||||
else:
|
||||
if not self.token:
|
||||
raise RuntimeError(
|
||||
"OpenBao token is required unless --use-token-helper is set"
|
||||
)
|
||||
cmd = (
|
||||
self.kubectl_parts
|
||||
+ [
|
||||
"exec",
|
||||
"-i",
|
||||
"-n",
|
||||
self.namespace,
|
||||
self.pod,
|
||||
"--",
|
||||
"sh",
|
||||
"-c",
|
||||
'read -r BAO_TOKEN; export BAO_TOKEN; exec bao "$@"',
|
||||
"sh",
|
||||
]
|
||||
+ args
|
||||
)
|
||||
proc_input = self.token + "\n" + (input_text or "")
|
||||
|
||||
result = subprocess.run(cmd, input=proc_input, capture_output=True, text=True)
|
||||
if result.stdout:
|
||||
print(result.stdout, end="")
|
||||
if result.returncode != 0:
|
||||
if result.stderr:
|
||||
print(result.stderr, file=sys.stderr, end="")
|
||||
raise SystemExit(result.returncode)
|
||||
if result.stderr:
|
||||
print(result.stderr, file=sys.stderr, end="")
|
||||
return result
|
||||
|
||||
|
||||
def read_token(
|
||||
token_file: str | None, dry_run: bool, use_token_helper: bool
|
||||
) -> str | None:
|
||||
if dry_run or use_token_helper:
|
||||
return None
|
||||
if token_file:
|
||||
path = Path(token_file)
|
||||
if not path.exists():
|
||||
raise SystemExit(f"ERROR: OPENBAO_TOKEN_FILE does not exist: {path}")
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
token = lines[0].strip() if lines else ""
|
||||
if not token:
|
||||
raise SystemExit(f"ERROR: OPENBAO_TOKEN_FILE is empty: {path}")
|
||||
return token
|
||||
token = getpass.getpass("OpenBao token: ")
|
||||
if not token:
|
||||
raise SystemExit("ERROR: empty OpenBao token")
|
||||
return token
|
||||
|
||||
|
||||
def selected_appliers(selector: str) -> list[dict[str, Any]]:
|
||||
if selector == "all":
|
||||
return [APPLIERS["nonprod"], APPLIERS["prod"]]
|
||||
try:
|
||||
return [APPLIERS[selector]]
|
||||
except KeyError:
|
||||
raise SystemExit(f"ERROR: applier must be one of {sorted(APPLIERS) + ['all']}")
|
||||
|
||||
|
||||
def role_args(applier: dict[str, Any]) -> list[str]:
|
||||
return [
|
||||
"write",
|
||||
f"auth/token/roles/{applier['token_role']}",
|
||||
f"allowed_policies={applier['policy_name']}",
|
||||
f"disallowed_policies={','.join(DISALLOWED_POLICIES)}",
|
||||
"orphan=true",
|
||||
"renewable=false",
|
||||
f"token_explicit_max_ttl={applier['max_ttl']}",
|
||||
"token_no_default_policy=true",
|
||||
"token_type=service",
|
||||
]
|
||||
|
||||
|
||||
def write_policy(runner: BaoRunner, applier: dict[str, Any], policy_dir: Path) -> None:
|
||||
policy_file = policy_dir / Path(applier["policy_file"]).name
|
||||
if not policy_file.exists():
|
||||
raise SystemExit(f"ERROR: missing policy file: {policy_file}")
|
||||
if runner.dry_run:
|
||||
print(f"DRY-RUN: bao policy write {applier['policy_name']} {policy_file}")
|
||||
return
|
||||
runner.run(
|
||||
["policy", "write", applier["policy_name"], "-"],
|
||||
input_text=policy_file.read_text(encoding="utf-8"),
|
||||
)
|
||||
print(f"OK: policy {applier['policy_name']} applied")
|
||||
|
||||
|
||||
def apply_applier(runner: BaoRunner, applier: dict[str, Any], policy_dir: Path) -> None:
|
||||
write_policy(runner, applier, policy_dir)
|
||||
runner.run(role_args(applier))
|
||||
runner.run(["read", f"auth/token/roles/{applier['token_role']}"])
|
||||
print(
|
||||
"OK: applier role "
|
||||
f"{applier['token_role']} configured for policy {applier['policy_name']}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Apply OpenBao credential-change delegated applier policies and token roles."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--applier", choices=["nonprod", "prod", "all"], default="all"
|
||||
)
|
||||
parser.add_argument("--policy-dir", default="openbao/policies")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
parser.add_argument(
|
||||
"--use-token-helper",
|
||||
action="store_true",
|
||||
help="Use the OpenBao CLI token helper inside the pod",
|
||||
)
|
||||
parser.add_argument("--namespace", default=None)
|
||||
parser.add_argument("--release", default=None)
|
||||
parser.add_argument("--kubectl", default=None)
|
||||
args = parser.parse_args()
|
||||
|
||||
namespace = args.namespace or os.environ.get("OPENBAO_NAMESPACE", "openbao")
|
||||
release = args.release or os.environ.get("OPENBAO_RELEASE", "openbao")
|
||||
kubectl = args.kubectl or os.environ.get("KUBECTL", "kubectl")
|
||||
token_file = os.environ.get("OPENBAO_TOKEN_FILE")
|
||||
token = read_token(token_file, args.dry_run, args.use_token_helper)
|
||||
|
||||
runner = BaoRunner(
|
||||
kubectl=kubectl,
|
||||
namespace=namespace,
|
||||
release=release,
|
||||
dry_run=args.dry_run,
|
||||
use_token_helper=args.use_token_helper,
|
||||
token=token,
|
||||
)
|
||||
|
||||
if not args.dry_run:
|
||||
runner.run(["status"])
|
||||
|
||||
policy_dir = REPO_DIR / args.policy_dir
|
||||
for applier in selected_appliers(args.applier):
|
||||
apply_applier(runner, applier, policy_dir)
|
||||
|
||||
print("NEXT: issue short-lived child tokens through an approved custody path only.")
|
||||
print("NEXT: run scripts/credential-change.py applier-apply <CCR> with that ambient delegated authority.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -11,6 +11,9 @@ ESO_NAMESPACE="${ESO_NAMESPACE:-external-secrets}"
|
||||
ESO_SERVICE_ACCOUNT="${ESO_SERVICE_ACCOUNT:-external-secrets}"
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
POLICY_FILE="${POLICY_FILE:-$REPO_DIR/openbao/policies/external-secrets-issue-core.hcl}"
|
||||
NEXT_KV_PATH="${OPENBAO_ESO_NEXT_PATH:-platform/workloads/issue-core/issue-core/issue-core-runtime}"
|
||||
NEXT_FIELDS="${OPENBAO_ESO_NEXT_FIELDS:-ISSUE_CORE_API_KEY and GITEA_BACKEND_TOKEN}"
|
||||
NEXT_TARGET="${OPENBAO_ESO_NEXT_TARGET:-ExternalSecret/issue-core-runtime}"
|
||||
DRY_RUN=0
|
||||
|
||||
usage() {
|
||||
@@ -125,13 +128,12 @@ remote_bao "$token" write "auth/kubernetes/role/${ROLE_NAME}" \
|
||||
|
||||
remote_bao "$token" read "auth/kubernetes/role/${ROLE_NAME}"
|
||||
|
||||
cat <<'NEXT'
|
||||
cat <<NEXT
|
||||
|
||||
External Secrets OpenBao role configured.
|
||||
|
||||
Next steps:
|
||||
1. Sync the external-secrets and openbao-secretstore ArgoCD Applications.
|
||||
2. Provision platform/workloads/issue-core/issue-core/issue-core-runtime
|
||||
with ISSUE_CORE_API_KEY and GITEA_BACKEND_TOKEN without printing values.
|
||||
3. Confirm ExternalSecret/issue-core-runtime becomes Ready.
|
||||
2. Provision ${NEXT_KV_PATH} with ${NEXT_FIELDS} without printing values.
|
||||
3. Confirm ${NEXT_TARGET} becomes Ready.
|
||||
NEXT
|
||||
|
||||
Reference in New Issue
Block a user