secrets: distribute write-scoped Gitea PAT + homelab refresh-creds
Adds secrets/gitea-pat.yaml (SOPS-encrypted, dtoro PAT with read+write scopes) so any enrolled client can push to dtoro/Homelab-Docs — not just where I have SSH. Recipient set = hello.yaml's (hubris, apps, republic); expand alongside hello.yaml when enrolling new clients. bin/homelab gains 'refresh-creds': decrypts gitea-pat.yaml, rewrites /etc/homelab-context/git-credentials with the write token, repoints git's --system credential helper. Re-execs via sudo for non-root callers (same pattern as 'homelab secret'). After this lands, 'homelab client add/remove' and wiki edits can run from any client. The initial bootstrap still needs an operator-supplied read-only PAT (chicken-and-egg); 'refresh-creds' upgrades the client to write afterwards. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
50
bin/homelab
50
bin/homelab
@@ -278,6 +278,52 @@ def cmd_secret(args: argparse.Namespace) -> int:
|
||||
return subprocess.call(["sops", "-d", str(path)], env=env)
|
||||
|
||||
|
||||
def cmd_refresh_creds(args: argparse.Namespace) -> int:
|
||||
"""Replace /etc/homelab-context/git-credentials with the write-scoped PAT
|
||||
from secrets/gitea-pat.yaml so push (not just pull) works from this client.
|
||||
"""
|
||||
if os.geteuid() != 0:
|
||||
# Need root for the decrypt + creds-file write.
|
||||
return subprocess.call(["sudo", "-E", sys.argv[0], "refresh-creds"])
|
||||
pat_file = CONTEXT / "secrets" / "gitea-pat.yaml"
|
||||
if not pat_file.exists():
|
||||
die(f"no {pat_file} — has the sync pulled it yet? Try `homelab sync`.")
|
||||
if not AGE_KEY.exists():
|
||||
die(f"no age key at {AGE_KEY} — bootstrap first.")
|
||||
env = {**os.environ, "SOPS_AGE_KEY_FILE": str(AGE_KEY)}
|
||||
proc = subprocess.run(["sops", "-d", str(pat_file)],
|
||||
capture_output=True, text=True, env=env)
|
||||
if proc.returncode != 0:
|
||||
die(f"could not decrypt {pat_file} — is this client a recipient? "
|
||||
f"sops error: {proc.stderr.strip()}")
|
||||
pat_data = yaml.safe_load(proc.stdout) or {}
|
||||
user = pat_data.get("user")
|
||||
token = pat_data.get("token")
|
||||
if not user or not token:
|
||||
die("decrypted gitea-pat.yaml missing user or token")
|
||||
# Find the host from the existing remote.
|
||||
remote_url = subprocess.run(
|
||||
["git", "-C", str(CONTEXT), "remote", "get-url", "origin"],
|
||||
capture_output=True, text=True,
|
||||
).stdout.strip()
|
||||
# Match http(s)://host or scp-style gitea@host:path
|
||||
proto = "https"
|
||||
host = "git.hubris.network"
|
||||
if remote_url.startswith(("http://", "https://")):
|
||||
proto = remote_url.split("://", 1)[0]
|
||||
host = remote_url.split("://", 1)[1].split("/", 1)[0]
|
||||
creds_dir = Path("/etc/homelab-context")
|
||||
creds_dir.mkdir(parents=True, exist_ok=True)
|
||||
creds_file = creds_dir / "git-credentials"
|
||||
creds_file.write_text(f"{proto}://{user}:{token}@{host}\n")
|
||||
creds_file.chmod(0o600)
|
||||
subprocess.run(["git", "config", "--system", "credential.helper",
|
||||
f"store --file={creds_file}"], check=True)
|
||||
print(f"refreshed {creds_file} (user={user}, scope=write)")
|
||||
print("`git push` from /opt/homelab-context now works.")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_sync(args: argparse.Namespace) -> int:
|
||||
if sys.platform == "darwin":
|
||||
return subprocess.call(
|
||||
@@ -473,6 +519,10 @@ def main() -> int:
|
||||
sp = sub.add_parser("sync", help="manually trigger homelab-context-sync")
|
||||
sp.set_defaults(func=cmd_sync)
|
||||
|
||||
sp = sub.add_parser("refresh-creds",
|
||||
help="swap the read-only bootstrap PAT for the write-scoped one from secrets/gitea-pat.yaml")
|
||||
sp.set_defaults(func=cmd_refresh_creds)
|
||||
|
||||
sp = sub.add_parser("mcp", help="call an MCP tool (requires 'mcp' CLI installed)")
|
||||
sp.add_argument("tool")
|
||||
sp.add_argument("args", nargs=argparse.REMAINDER)
|
||||
|
||||
Reference in New Issue
Block a user