From bf250fc6a9fcba8e4413e8b272ac4fb78fca2954 Mon Sep 17 00:00:00 2001 From: dtoro Date: Tue, 7 Jul 2026 22:20:58 +0200 Subject: [PATCH] cleanup: archive stale secrets/ + secrets-issuance/, add Infisical bootstrap script, update SOPS paths to archive/ --- .sops.yaml | 36 +++-- .../secrets-issuance}/backup.service | 0 .../secrets-issuance}/backup.sh | 0 .../secrets-issuance}/backup.timer | 0 .../secrets-issuance}/deploy/deploy.sh | 0 .../deploy/webhook/install.sh | 0 .../webhook/secrets-issuance-deploy.service | 0 .../deploy/webhook/webhook.py | 0 .../secrets-issuance}/server.py | 0 .../secrets-issuance}/server.service | 0 .../secrets-sops-backup}/README.md | 0 .../secrets-sops-backup}/gitea-pat.yaml | 0 .../secrets-sops-backup}/hello.yaml | 0 .../hermes-house-users.yaml | 0 .../netbird-authentik-oidc.yaml | 0 .../secrets-sops-backup}/netbird-pat.yaml | 0 .../oikos-approval-hmac.yaml | 0 .../oikos-console-deploy-secret.yaml | 0 .../openrouter-api-key.yaml | 0 .../secrets-sops-backup}/rotation.md | 0 .../turn-shared-secret.yaml | 0 .../yuvomi-api-token.yaml | 0 cmd/oikos/main.go | 2 +- scripts/bootstrap-infisical.sh | 138 ++++++++++++++++++ 24 files changed, 156 insertions(+), 20 deletions(-) rename {secrets-issuance => archive/secrets-issuance}/backup.service (100%) rename {secrets-issuance => archive/secrets-issuance}/backup.sh (100%) rename {secrets-issuance => archive/secrets-issuance}/backup.timer (100%) rename {secrets-issuance => archive/secrets-issuance}/deploy/deploy.sh (100%) rename {secrets-issuance => archive/secrets-issuance}/deploy/webhook/install.sh (100%) rename {secrets-issuance => archive/secrets-issuance}/deploy/webhook/secrets-issuance-deploy.service (100%) rename {secrets-issuance => archive/secrets-issuance}/deploy/webhook/webhook.py (100%) rename {secrets-issuance => archive/secrets-issuance}/server.py (100%) rename {secrets-issuance => archive/secrets-issuance}/server.service (100%) rename {secrets => archive/secrets-sops-backup}/README.md (100%) rename {secrets => archive/secrets-sops-backup}/gitea-pat.yaml (100%) rename {secrets => archive/secrets-sops-backup}/hello.yaml (100%) rename {secrets => archive/secrets-sops-backup}/hermes-house-users.yaml (100%) rename {secrets => archive/secrets-sops-backup}/netbird-authentik-oidc.yaml (100%) rename {secrets => archive/secrets-sops-backup}/netbird-pat.yaml (100%) rename {secrets => archive/secrets-sops-backup}/oikos-approval-hmac.yaml (100%) rename {secrets => archive/secrets-sops-backup}/oikos-console-deploy-secret.yaml (100%) rename {secrets => archive/secrets-sops-backup}/openrouter-api-key.yaml (100%) rename {secrets => archive/secrets-sops-backup}/rotation.md (100%) rename {secrets => archive/secrets-sops-backup}/turn-shared-secret.yaml (100%) rename {secrets => archive/secrets-sops-backup}/yuvomi-api-token.yaml (100%) create mode 100755 scripts/bootstrap-infisical.sh diff --git a/.sops.yaml b/.sops.yaml index c036174..7dd12f5 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -1,14 +1,12 @@ # SOPS recipient rules. # -# Each rule pins one or more age public keys as recipients for files matching -# the path_regex. The build_host_files.py generator doesn't read this file; -# `sops` does — to encrypt a new secret, run `sops -e secrets/.yaml` -# from the repo root and SOPS will pick the matching rule below. +# SOPS is DR-fallback only — Infisical is the active secrets backend. +# Files live in archive/secrets-sops-backup/ for cold recovery. +# To encrypt a new DR secret: sops -e archive/secrets-sops-backup/.yaml # -# To grant a secret to a new client: add their age public key (from +# To grant a DR secret to a new client: add their age public key (from # inventory.yaml `hosts..age_pubkey`) to the relevant rule below, then -# run `sops updatekeys secrets/.yaml` to re-encrypt without rotating -# the ciphertext payload. +# run `sops updatekeys archive/secrets-sops-backup/.yaml` to re-encrypt. # # To revoke: remove the recipient from the relevant rule and run # `sops updatekeys` (this is what `homelab client remove` calls). Past @@ -16,7 +14,7 @@ # underlying credential if compromise is suspected. creation_rules: - - path_regex: ^secrets/hello\.yaml$ + - path_regex: ^archive/secrets-sops-backup/hello\.yaml$ # The "hello" secret is encrypted to every enrolled client so the bootstrap # decrypt test works for everyone. Add each new client's age_pubkey when # they enrol; re-key with `sops updatekeys -y secrets/hello.yaml`. @@ -29,7 +27,7 @@ creation_rules: age1rtwvdct6avjkr3cyxv3vue3vqx4d524fjfr3vk7xrnvyrylnry5sm54sn4, age1pwtdws2thdh7vzp2dzttl3zxgcs2tgpcsjsqgw3q04nyml4kvuqq467u4x - - path_regex: ^secrets/gitea-pat\.yaml$ + - path_regex: archive/secrets-sops-backupgitea-pat\.yaml$ # Write-scoped Gitea PAT (dtoro user). Same recipient list as hello.yaml # since every enrolled client should be able to push (homelab client # add/remove, wiki edits, etc.). @@ -42,17 +40,17 @@ creation_rules: age1rtwvdct6avjkr3cyxv3vue3vqx4d524fjfr3vk7xrnvyrylnry5sm54sn4, age1pwtdws2thdh7vzp2dzttl3zxgcs2tgpcsjsqgw3q04nyml4kvuqq467u4x - - path_regex: ^secrets/gitea-tokens\.yaml$ + - path_regex: archive/secrets-sops-backupgitea-tokens\.yaml$ # Workstations only. age: >- # placeholder — fill with age_pubkey of: republic-laptop, mac-mini, strong, hubris - - path_regex: ^secrets/webhook-hmacs\.yaml$ + - path_regex: archive/secrets-sops-backupwebhook-hmacs\.yaml$ # LXCs that run a webhook receiver. age: >- # placeholder — fill with age_pubkey of: apps, caddy - - path_regex: ^secrets/turn-shared-secret\.yaml$ + - path_regex: archive/secrets-sops-backupturn-shared-secret\.yaml$ # coturn TURN long-term-credential password. Consumed by hubris (which # renders /etc/turnserver.conf + /opt/management.json on the VPS via # `homelab render-vps-configs`). Other recipients are convenience for @@ -65,7 +63,7 @@ creation_rules: age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h, age1pwtdws2thdh7vzp2dzttl3zxgcs2tgpcsjsqgw3q04nyml4kvuqq467u4x - - path_regex: ^secrets/netbird-authentik-oidc\.yaml$ + - path_regex: archive/secrets-sops-backupnetbird-authentik-oidc\.yaml$ # Authentik OIDC client secret for the netbird-dashboard provider. # Consumed by hubris to render /opt/management.json on the VPS # (PKCEAuthorizationFlow.ProviderConfig.ClientSecret). @@ -77,7 +75,7 @@ creation_rules: age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h, age1pwtdws2thdh7vzp2dzttl3zxgcs2tgpcsjsqgw3q04nyml4kvuqq467u4x - - path_regex: ^secrets/netbird-pat\.yaml$ + - path_regex: archive/secrets-sops-backupnetbird-pat\.yaml$ # NetBird API Personal Access Token. Consumed by the dns-sync job on the # `dns` LXC (107) to reconcile Technitium -> NetBird managed DNS zone. # (When 107 is enrolled, add its age_pubkey here and updatekeys.) @@ -87,7 +85,7 @@ creation_rules: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6, age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs - - path_regex: ^secrets/openrouter-api-key\.yaml$ + - path_regex: archive/secrets-sops-backupopenrouter-api-key\.yaml$ # OpenRouter API key consumed by the `hermes` wrapper (bin/hermes) when # spawning a Goose session. Recipients are any host that should run a # Nous-Hermes agent. Add a host's age_pubkey here, then @@ -99,7 +97,7 @@ creation_rules: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs, age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h - - path_regex: ^secrets/yuvomi-api-token\.yaml$ + - path_regex: archive/secrets-sops-backupyuvomi-api-token\.yaml$ # Named Bearer token for the Yuvomi REST API, consumed by yuvomi-mcp on # LXC 129 (house). age: >- @@ -108,7 +106,7 @@ creation_rules: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs, age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h - - path_regex: ^secrets/hermes-house-users\.yaml$ + - path_regex: archive/secrets-sops-backuphermes-house-users\.yaml$ # Signal number → Yuvomi user_id mapping (PII). Consumed by hermesd on LXC 129. age: >- age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6, @@ -116,7 +114,7 @@ creation_rules: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs, age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h - - path_regex: ^secrets/oikos-approval-hmac\.yaml$ + - path_regex: archive/secrets-sops-backupoikos-approval-hmac\.yaml$ # HMAC signing key for Oikos approval-grant tokens (oikos/approve.py). # Recipients: apps (105, runs the approval engine alongside homelab-mcp) # and hubris (admin/debug decrypt). See OIKOS.md "Approval engine". @@ -124,7 +122,7 @@ creation_rules: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6, age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0 - - path_regex: ^secrets/oikos-console-deploy-secret\.yaml$ + - path_regex: archive/secrets-sops-backupoikos-console-deploy-secret\.yaml$ # Shared HMAC secret for the Gitea deploy webhook (id 14) -> # oikos-console-deploy.service on apps (105). Generated + registered # with Gitea before the apps-side install ran (see diff --git a/secrets-issuance/backup.service b/archive/secrets-issuance/backup.service similarity index 100% rename from secrets-issuance/backup.service rename to archive/secrets-issuance/backup.service diff --git a/secrets-issuance/backup.sh b/archive/secrets-issuance/backup.sh similarity index 100% rename from secrets-issuance/backup.sh rename to archive/secrets-issuance/backup.sh diff --git a/secrets-issuance/backup.timer b/archive/secrets-issuance/backup.timer similarity index 100% rename from secrets-issuance/backup.timer rename to archive/secrets-issuance/backup.timer diff --git a/secrets-issuance/deploy/deploy.sh b/archive/secrets-issuance/deploy/deploy.sh similarity index 100% rename from secrets-issuance/deploy/deploy.sh rename to archive/secrets-issuance/deploy/deploy.sh diff --git a/secrets-issuance/deploy/webhook/install.sh b/archive/secrets-issuance/deploy/webhook/install.sh similarity index 100% rename from secrets-issuance/deploy/webhook/install.sh rename to archive/secrets-issuance/deploy/webhook/install.sh diff --git a/secrets-issuance/deploy/webhook/secrets-issuance-deploy.service b/archive/secrets-issuance/deploy/webhook/secrets-issuance-deploy.service similarity index 100% rename from secrets-issuance/deploy/webhook/secrets-issuance-deploy.service rename to archive/secrets-issuance/deploy/webhook/secrets-issuance-deploy.service diff --git a/secrets-issuance/deploy/webhook/webhook.py b/archive/secrets-issuance/deploy/webhook/webhook.py similarity index 100% rename from secrets-issuance/deploy/webhook/webhook.py rename to archive/secrets-issuance/deploy/webhook/webhook.py diff --git a/secrets-issuance/server.py b/archive/secrets-issuance/server.py similarity index 100% rename from secrets-issuance/server.py rename to archive/secrets-issuance/server.py diff --git a/secrets-issuance/server.service b/archive/secrets-issuance/server.service similarity index 100% rename from secrets-issuance/server.service rename to archive/secrets-issuance/server.service diff --git a/secrets/README.md b/archive/secrets-sops-backup/README.md similarity index 100% rename from secrets/README.md rename to archive/secrets-sops-backup/README.md diff --git a/secrets/gitea-pat.yaml b/archive/secrets-sops-backup/gitea-pat.yaml similarity index 100% rename from secrets/gitea-pat.yaml rename to archive/secrets-sops-backup/gitea-pat.yaml diff --git a/secrets/hello.yaml b/archive/secrets-sops-backup/hello.yaml similarity index 100% rename from secrets/hello.yaml rename to archive/secrets-sops-backup/hello.yaml diff --git a/secrets/hermes-house-users.yaml b/archive/secrets-sops-backup/hermes-house-users.yaml similarity index 100% rename from secrets/hermes-house-users.yaml rename to archive/secrets-sops-backup/hermes-house-users.yaml diff --git a/secrets/netbird-authentik-oidc.yaml b/archive/secrets-sops-backup/netbird-authentik-oidc.yaml similarity index 100% rename from secrets/netbird-authentik-oidc.yaml rename to archive/secrets-sops-backup/netbird-authentik-oidc.yaml diff --git a/secrets/netbird-pat.yaml b/archive/secrets-sops-backup/netbird-pat.yaml similarity index 100% rename from secrets/netbird-pat.yaml rename to archive/secrets-sops-backup/netbird-pat.yaml diff --git a/secrets/oikos-approval-hmac.yaml b/archive/secrets-sops-backup/oikos-approval-hmac.yaml similarity index 100% rename from secrets/oikos-approval-hmac.yaml rename to archive/secrets-sops-backup/oikos-approval-hmac.yaml diff --git a/secrets/oikos-console-deploy-secret.yaml b/archive/secrets-sops-backup/oikos-console-deploy-secret.yaml similarity index 100% rename from secrets/oikos-console-deploy-secret.yaml rename to archive/secrets-sops-backup/oikos-console-deploy-secret.yaml diff --git a/secrets/openrouter-api-key.yaml b/archive/secrets-sops-backup/openrouter-api-key.yaml similarity index 100% rename from secrets/openrouter-api-key.yaml rename to archive/secrets-sops-backup/openrouter-api-key.yaml diff --git a/secrets/rotation.md b/archive/secrets-sops-backup/rotation.md similarity index 100% rename from secrets/rotation.md rename to archive/secrets-sops-backup/rotation.md diff --git a/secrets/turn-shared-secret.yaml b/archive/secrets-sops-backup/turn-shared-secret.yaml similarity index 100% rename from secrets/turn-shared-secret.yaml rename to archive/secrets-sops-backup/turn-shared-secret.yaml diff --git a/secrets/yuvomi-api-token.yaml b/archive/secrets-sops-backup/yuvomi-api-token.yaml similarity index 100% rename from secrets/yuvomi-api-token.yaml rename to archive/secrets-sops-backup/yuvomi-api-token.yaml diff --git a/cmd/oikos/main.go b/cmd/oikos/main.go index 4c3307e..06b38cb 100644 --- a/cmd/oikos/main.go +++ b/cmd/oikos/main.go @@ -299,7 +299,7 @@ func runSecret(ctx context.Context, cfg config.Config) { sub := os.Args[2] secretsDir := cfg.SecretsDir if secretsDir == "" { - secretsDir = "secrets" + secretsDir = "archive/secrets-sops-backup" } sopsBackend := secrets.NewSOPSBackend(secretsDir) diff --git a/scripts/bootstrap-infisical.sh b/scripts/bootstrap-infisical.sh new file mode 100755 index 0000000..963ad52 --- /dev/null +++ b/scripts/bootstrap-infisical.sh @@ -0,0 +1,138 @@ +#!/bin/bash +# Bootstrap Infisical from zero. +# Run after: docker compose up -d postgres +# Produces: .env with all OIKOS_INFISICAL_* values, admin creds in .infisical-credentials +set -euo pipefail + +INFISICAL_URL="${INFISICAL_URL:-http://localhost:8080}" +ADMIN_EMAIL="${ADMIN_EMAIL:-oikos@hubris.network}" +ADMIN_PASS="${ADMIN_PASS:-oikos-admin-$(openssl rand -hex 4)}" +PROJECT_NAME="${PROJECT_NAME:-oikos}" +ORG_NAME="${ORG_NAME:-Oikos}" + +# ── 1. Generate keys ─────────────────────────────────────────────── +echo "=== [1/6] Generating keys ===" +ENCRYPTION_KEY=$(python3 -c "import secrets, string; print(''.join(secrets.choice(string.ascii_letters+string.digits) for _ in range(32)))") +JWT_SIGNUP=$(openssl rand -hex 32) +JWT_REFRESH=$(openssl rand -hex 32) +JWT_AUTH=$(openssl rand -hex 32) +JWT_SERVICE=$(openssl rand -hex 32) +JWT_MFA=$(openssl rand -hex 32) +JWT_PROVIDER=$(openssl rand -hex 32) + +# ── 2. Write .env ────────────────────────────────────────────────── +cat > .env </dev/null || true +DROP DATABASE IF EXISTS infisical; +CREATE DATABASE infisical; +SQL + +echo "=== [3/6] Starting Infisical + Redis ===" +docker compose --profile infisical up infisical redis -d --wait 2>&1 | tail -2 + +# Wait for healthy +for i in $(seq 1 30); do + if curl -sf "$INFISICAL_URL/api/status" > /dev/null 2>&1; then break; fi + sleep 2 +done +echo "Infisical ready" + +# ── 4. Create admin ──────────────────────────────────────────────── +echo "=== [4/6] Creating admin account ===" +SIGNUP=$(curl -s -X POST "$INFISICAL_URL/api/v1/admin/signup" \ + -H "Content-Type: application/json" \ + -d "{\"email\":\"$ADMIN_EMAIL\",\"password\":\"$ADMIN_PASS\",\"firstName\":\"Oikos\",\"lastName\":\"Admin\",\"organizationName\":\"$ORG_NAME\"}") +ORG_ID=$(echo "$SIGNUP" | python3 -c "import sys,json; print(json.load(sys.stdin)['organization']['id'])") +USER_TOKEN=$(echo "$SIGNUP" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])") +echo "Admin created, org=$ORG_ID" + +# ── 5. Get org-scoped token ──────────────────────────────────────── +ORG_TOKEN=$(curl -s -X POST "$INFISICAL_URL/api/v3/auth/select-organization" \ + -H "Authorization: Bearer $USER_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"organizationId\":\"$ORG_ID\"}" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])") + +# ── 6. Create project + identity ─────────────────────────────────── +echo "=== [5/6] Creating project + machine identity ===" +PROJECT=$(curl -s -X POST "$INFISICAL_URL/api/v1/projects" \ + -H "Authorization: Bearer $ORG_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"projectName\":\"$PROJECT_NAME\",\"slug\":\"$PROJECT_NAME\"}") +PROJECT_ID=$(echo "$PROJECT" | python3 -c "import sys,json; print(json.load(sys.stdin)['project']['id'])") +echo "Project: $PROJECT_NAME ($PROJECT_ID)" + +# Create machine identity +IDENTITY=$(curl -s -X POST "$INFISICAL_URL/api/v1/identities" \ + -H "Authorization: Bearer $ORG_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"$PROJECT_NAME-api\",\"role\":\"admin\",\"organizationId\":\"$ORG_ID\",\"projectIds\":[\"$PROJECT_ID\"]}") +IDENTITY_ID=$(echo "$IDENTITY" | python3 -c "import sys,json; print(json.load(sys.stdin)['identity']['id'])") +echo "Identity: $PROJECT_NAME-api ($IDENTITY_ID)" + +# Attach Universal Auth +UA=$(curl -s -X POST "$INFISICAL_URL/api/v1/auth/universal-auth/identities/$IDENTITY_ID" \ + -H "Authorization: Bearer $ORG_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"clientIdTtl":0,"clientSecretTtl":0,"accessTokenTtl":2592000,"accessTokenMaxTtl":2592000,"accessTokenNumUsesLimit":0}') +CLIENT_ID=$(echo "$UA" | python3 -c "import sys,json; print(json.load(sys.stdin)['identityUniversalAuth']['clientId'])") +echo "UA attached, clientId=$CLIENT_ID" + +# Create client secret +SECRET=$(curl -s -X POST "$INFISICAL_URL/api/v1/auth/universal-auth/identities/$IDENTITY_ID/client-secrets" \ + -H "Authorization: Bearer $ORG_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"description":"default","clientSecretTtl":0,"clientSecretNumUsesLimit":0}') +CLIENT_SECRET=$(echo "$SECRET" | python3 -c "import sys,json; print(json.load(sys.stdin)['clientSecret'])") +echo "Client secret created" + +# Add identity to project +curl -s -X POST "$INFISICAL_URL/api/v1/projects/$PROJECT_ID/memberships/identities/$IDENTITY_ID" \ + -H "Authorization: Bearer $ORG_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"roles":[{"role":"admin","isTemporary":false}]}' > /dev/null +echo "Identity added to project" + +# ── 7. Write credentials ────────────────────────────────────────── +echo "=== [6/6] Writing credentials ===" +cat >> .env < .infisical-credentials <