mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 10:13:58 +00:00
- knoe-db.yaml: switch to CNPG-managed TLS cert with serverAltDNSNames (pg.prole.org + knoe-db-rw cluster service) — removes static serverTLSSecret/serverCASecret - dns.yml: add pg.prole.org A record to prole_k3s_dns_records (10.0.0.3, 10.0.0.6) for Ansible-managed split-horizon DNS via Samba AD DC - k3s.cfg: align KNOE_HOME paths to ~/dev/prole, add PROLE_KDC_* vars, remove hardcoded KUBECTL_CONTEXT (kubeconfig current-context is authoritative) - prod.cfg: add PROLE_KDC_STORAGE_CLASS = prole-iscsi - onepassword.py: skip vault check gracefully when no 1Password session active (non-TTY) - CLAUDE.md: document production postgres connection string and DNS/CA cert ops Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
159 lines
4.5 KiB
Python
159 lines
4.5 KiB
Python
"""1Password CLI integration for knoe secret management.
|
|
|
|
All secrets are stored in the 'knoey' vault so they are isolated from the
|
|
user's personal 1Password vaults. The 'administrator' item holds the
|
|
database master password.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import secrets
|
|
import shutil
|
|
import string
|
|
import subprocess
|
|
|
|
_VAULT = "knoey"
|
|
_ADMIN_ITEM = "administrator"
|
|
|
|
|
|
def op_available() -> bool:
|
|
return shutil.which("op") is not None
|
|
|
|
|
|
def _op(*args: str, check: bool = True, timeout: int = 15) -> subprocess.CompletedProcess:
|
|
if not op_available():
|
|
raise RuntimeError(
|
|
"1Password CLI (op) not found. Install: brew install 1password-cli"
|
|
)
|
|
try:
|
|
return subprocess.run(
|
|
["op", *args],
|
|
capture_output=True,
|
|
text=True,
|
|
check=check,
|
|
timeout=timeout,
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
return subprocess.CompletedProcess(
|
|
args=["op", *args], returncode=1, stdout="", stderr="timeout"
|
|
)
|
|
|
|
|
|
def ensure_op_signed_in() -> None:
|
|
"""Ensure the op CLI has an active session; trigger sign-in if not.
|
|
|
|
When running non-interactively (no TTY on stdin) and no session exists,
|
|
skip rather than hang or crash — the installer can proceed without 1Password
|
|
for k3s/k3d modes where secrets are managed separately.
|
|
"""
|
|
import sys
|
|
|
|
if not op_available():
|
|
raise RuntimeError(
|
|
"1Password CLI (op) not found. Install: brew install 1password-cli"
|
|
)
|
|
result = subprocess.run(
|
|
["op", "whoami"],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
if not sys.stdin.isatty():
|
|
print(
|
|
"[WARN] No active 1Password session and no TTY — skipping op signin.",
|
|
flush=True,
|
|
)
|
|
return
|
|
subprocess.run(["op", "signin"], check=True)
|
|
|
|
|
|
def _op_session_active() -> bool:
|
|
"""Return True if op has an active session."""
|
|
r = subprocess.run(["op", "whoami"], capture_output=True, text=True)
|
|
return r.returncode == 0
|
|
|
|
|
|
def ensure_knoey_vault() -> None:
|
|
"""Create the 'knoey' vault if it does not already exist.
|
|
|
|
Skips silently when running non-interactively without an op session.
|
|
"""
|
|
import sys
|
|
|
|
if not _op_session_active():
|
|
if not sys.stdin.isatty():
|
|
print("[WARN] No active 1Password session — skipping vault check.", flush=True)
|
|
return
|
|
result = _op("vault", "list", "--format", "json", check=True)
|
|
try:
|
|
vaults = json.loads(result.stdout or "[]")
|
|
except json.JSONDecodeError:
|
|
vaults = []
|
|
names = [v.get("name", "") for v in vaults]
|
|
if _VAULT not in names:
|
|
_op("vault", "create", _VAULT)
|
|
print(f"[INFO] Created 1Password vault '{_VAULT}'")
|
|
else:
|
|
print(f"[INFO] 1Password vault '{_VAULT}' already exists")
|
|
|
|
|
|
def get_secret(item: str, field: str = "password") -> str:
|
|
"""Retrieve a field value from an item in the knoey vault."""
|
|
result = _op(
|
|
"item", "get", item,
|
|
"--vault", _VAULT,
|
|
"--fields", field,
|
|
"--reveal",
|
|
check=False,
|
|
)
|
|
if result.returncode != 0:
|
|
return ""
|
|
return result.stdout.strip()
|
|
|
|
|
|
def set_secret(item: str, field: str, value: str) -> None:
|
|
"""Set a field on an existing item, or create the item if absent."""
|
|
check_result = _op("item", "get", item, "--vault", _VAULT, check=False)
|
|
if check_result.returncode == 0:
|
|
_op(
|
|
"item", "edit", item,
|
|
"--vault", _VAULT,
|
|
f"{field}={value}",
|
|
)
|
|
else:
|
|
_op(
|
|
"item", "create",
|
|
"--category", "login",
|
|
"--title", item,
|
|
"--vault", _VAULT,
|
|
f"{field}={value}",
|
|
)
|
|
|
|
|
|
def get_administrator_password() -> str:
|
|
"""Return the administrator password from the knoey vault."""
|
|
return get_secret(_ADMIN_ITEM, "password")
|
|
|
|
|
|
def ensure_administrator_secret() -> str:
|
|
"""Return the administrator password, creating the item if absent."""
|
|
pw = get_secret(_ADMIN_ITEM, "password")
|
|
if pw:
|
|
return pw
|
|
pw = _generate_password()
|
|
_op(
|
|
"item", "create",
|
|
"--category", "login",
|
|
"--title", _ADMIN_ITEM,
|
|
"--vault", _VAULT,
|
|
f"password={pw}",
|
|
)
|
|
print(f"[INFO] Created 1Password item '{_ADMIN_ITEM}' in vault '{_VAULT}'")
|
|
return pw
|
|
|
|
|
|
def _generate_password(length: int = 32) -> str:
|
|
alphabet = string.ascii_letters + string.digits
|
|
return "".join(secrets.choice(alphabet) for _ in range(length))
|