From 7c17bda609c41728df4baa39b3757cdc0ef76232 Mon Sep 17 00:00:00 2001 From: Ruk Doe Date: Mon, 2 Nov 2026 16:30:38 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Infisical=20=E3=82=92=E5=8F=96=E3=82=8A?= =?UTF-8?q?=E9=99=A4=E3=81=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 秘密は全部 SOPS(SopsSecret)に移したので、Infisical の本体・operator・push-bridge と、その道具と記述を消す。 - bootstrap: render.sh の infisical の chart・復号・inline(infisical-secrets)と infisical-values.sops.yaml。 .sops.yaml などの encrypted_regex から Infisical だけのキーを外す。apiserver-rbac から secrets.infisical.com - talos-boot: infisical の値のダミーと INFISICAL_VALUES、infisical の Pod と Postgres の確認 - deploy/infisical をまるごと(Argo CD が prune する)。smtp-relay の NetworkPolicy から infisical の namespace - matrix: hookshot の Infisical の webhook と hook-infisical - recovery: infisical.sh・infisical-ca.mjs と手順の 6・7。秘密は git の SOPS なので復元では何もしない - tools: t.ps1 のログインの中継、Dockerfile の infisical CLI、compose の INFISICAL_PROFILE - README・docs・各 deploy の README の置き場の記述を SOPS に。decisions.md に「秘密は SOPS で git に置く」 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01T8TqVdo6LUrWKdVQgbScVU --- .github/workflows/talos-boot.yml | 27 +--- .github/workflows/tools.yml | 2 +- .sops.yaml | 2 +- README.md | 31 ++-- ROADMAP.md | 2 +- bootstrap/README.md | 107 +++---------- bootstrap/apiserver-rbac.yaml | 4 +- bootstrap/argocd-repos.yaml | 6 +- bootstrap/argocd-values.sops.yaml | 7 +- bootstrap/cert-manager-values.sops.yaml | 4 +- bootstrap/infisical-values.sops.yaml | 121 --------------- bootstrap/render.sh | 19 +-- bootstrap/talos.md | 6 +- bootstrap/talos.sops.yaml | 2 +- compose.yaml | 3 +- deploy/backup/README.md | 2 +- deploy/cloudflare-ddns/application.yaml | 2 +- deploy/external-dns/README.md | 2 +- deploy/forgejo/README.md | 15 +- deploy/forgejo/forgejo.yaml | 5 +- deploy/forgejo/runner.yaml | 4 +- deploy/headlamp/application.yaml | 2 +- deploy/infisical/httproute.yaml | 20 --- deploy/infisical/operator-rbac.yaml | 23 --- deploy/infisical/operator.yaml | 41 ----- deploy/infisical/push-bridge.yaml | 49 ------ deploy/infisical/smtp-secret.yaml | 24 --- deploy/infisical/volsync.yaml | 45 ------ deploy/matrix/README.md | 17 +- deploy/matrix/hookshot.yaml | 26 +--- deploy/matrix/secrets.sops.yaml | 5 +- deploy/renovate/README.md | 2 +- deploy/renovate/renovate.yaml | 4 +- deploy/rybbit/README.md | 4 +- deploy/smtp-relay/README.md | 12 +- deploy/smtp-relay/network-policy.yaml | 2 - deploy/sops-secrets-operator/application.yaml | 2 +- deploy/volsync/application.yaml | 2 +- docs/cloudflare-tokens.md | 19 ++- docs/decisions.md | 44 +++--- pulumi/README.md | 2 +- pulumi/entra-smtp.ts | 2 +- pulumi/notifications.ts | 2 +- recovery/README.md | 75 +++------ recovery/infisical-ca.mjs | 22 --- recovery/infisical.sh | 86 ----------- renovate.json | 5 +- tools/Dockerfile | 5 - tools/README.md | 17 +- tools/t.ps1 | 145 +++--------------- 50 files changed, 193 insertions(+), 884 deletions(-) delete mode 100644 bootstrap/infisical-values.sops.yaml delete mode 100644 deploy/infisical/httproute.yaml delete mode 100644 deploy/infisical/operator-rbac.yaml delete mode 100644 deploy/infisical/operator.yaml delete mode 100644 deploy/infisical/push-bridge.yaml delete mode 100644 deploy/infisical/smtp-secret.yaml delete mode 100644 deploy/infisical/volsync.yaml delete mode 100644 recovery/infisical-ca.mjs delete mode 100755 recovery/infisical.sh diff --git a/.github/workflows/talos-boot.yml b/.github/workflows/talos-boot.yml index c2c8da20..28a282f2 100644 --- a/.github/workflows/talos-boot.yml +++ b/.github/workflows/talos-boot.yml @@ -1,6 +1,6 @@ # bootstrap/render.sh の machine config を runner の QEMU(KVM)で起動し、クラスタが立つところまで見る。 # 秘密はゼロ(使い捨てとダミー)。外に副作用を出さない: ArgoCD の extraObjects と通知先、 -# 本物の ClusterIssuer(自己署名に差し替え)、infisical の SMTP を抜く。手順の元は bootstrap/talos.md。 +# 本物の ClusterIssuer(自己署名に差し替え)を抜く。手順の元は bootstrap/talos.md。 name: talos boot on: @@ -113,32 +113,17 @@ jobs: yq -I4 'del(.sops) | del(.notifications.notifiers) | del(.githubAppRepoCreds.stringData.githubAppPrivateKey) | .extraObjects = []' \ bootstrap/argocd-values.sops.yaml > /tmp/argocd-values.yaml - # infisical: 1 つめ(chart の値)はパスワードをダミーにして SMTP の env を抜く。2 つめ(Secret)は作り直す - # (ENCRYPTION_KEY は 16 バイトの hex でないと infisical が起動しない)。render.sh は `---` で分ける - { - yq -I4 'select(document_index == 0) | del(.sops) - | (.. | select(tag == "!!str" and test("^ENC\\["))) |= "ci-dummy" - | .infisical.extraEnv |= map(select(.name | test("^SMTP_") | not))' \ - bootstrap/infisical-values.sops.yaml - echo --- - yq -I4 "select(document_index == 1) | del(.sops) - | del(.stringData.CLIENT_ID_GITHUB_LOGIN) | del(.stringData.CLIENT_SECRET_GITHUB_LOGIN) - | .stringData.ENCRYPTION_KEY = \"$(openssl rand -hex 16)\" - | .stringData.AUTH_SECRET = \"$(openssl rand -base64 32)\" - | .stringData.TELEMETRY_ENABLED = \"false\"" \ - bootstrap/infisical-values.sops.yaml - } > /tmp/infisical-values.yaml # cert-manager: 2 つめ(DNS-01 の Secret)のトークンをダミーに yq -I4 'del(.sops) | (.. | select(tag == "!!str" and test("^ENC\\["))) |= "ci-dummy"' \ bootstrap/cert-manager-values.sops.yaml > /tmp/cert-manager-values.yaml # sops-age: クラスタの age の鍵はダミー(operator は CI では復号しない。Secret の形だけ見る) yq -I4 'del(.sops) | (.. | select(tag == "!!str" and test("^ENC\\["))) |= "AGE-SECRET-KEY-CI-DUMMY"' \ bootstrap/sops-age.sops.yaml > /tmp/sops-age.yaml - for f in /tmp/controlplane.yaml /tmp/argocd-values.yaml /tmp/infisical-values.yaml /tmp/cert-manager-values.yaml /tmp/sops-age.yaml; do + for f in /tmp/controlplane.yaml /tmp/argocd-values.yaml /tmp/cert-manager-values.yaml /tmp/sops-age.yaml; do ! grep -q 'ENC\[' "$f" || { echo "::error::$f に暗号文が残っている"; exit 1; } done SECRETS=/tmp/secrets.yaml CONTROLPLANE=/tmp/controlplane.yaml \ - ARGOCD_VALUES=/tmp/argocd-values.yaml INFISICAL_VALUES=/tmp/infisical-values.yaml \ + ARGOCD_VALUES=/tmp/argocd-values.yaml \ CERT_MANAGER_VALUES=/tmp/cert-manager-values.yaml SOPS_AGE=/tmp/sops-age.yaml \ ./bootstrap/render.sh /tmp/out talosctl config endpoint 127.0.0.1 @@ -240,9 +225,6 @@ jobs: ro argocd deploy/argocd-server ro argocd deploy/argocd-repo-server ro argocd sts/argocd-application-controller - # postgres の PVC は既定の StorageClass(OpenEBS LVM の thin)なので、ここで PV の経路も見ている - ro infisical deploy/infisical - kubectl -n infisical wait --for=jsonpath='{.status.phase}'=Bound pvc --all --timeout=1m # 上の一覧に無い Pod の取りこぼし止め for i in $(seq 24); do BAD=$(kubectl get pods -A --no-headers --field-selector=status.phase!=Running,status.phase!=Succeeded) @@ -464,10 +446,9 @@ jobs: until kubectl wait --for=condition=Ready "pod/$p" --timeout=10s >/dev/null 2>&1; do sleep 5; done [ "$(kubectl exec "$p" -- cat /data/marker)" = hello ] || fail "再起動のあと $p の中身が無い" done - kubectl -n infisical rollout status statefulset/postgresql --timeout=5m kubectl -n kube-system rollout status ds/lvm-thinpool --timeout=3m kubectl -n kube-system logs ds/lvm-thinpool -c pool - echo " 再起動のあとも t-src・t-dst・infisical の Postgres が上がった" + echo " 再起動のあとも t-src・t-dst が上がった" - name: 失敗したときの様子 if: failure() diff --git a/.github/workflows/tools.yml b/.github/workflows/tools.yml index 674140af..2eaae3d2 100644 --- a/.github/workflows/tools.yml +++ b/.github/workflows/tools.yml @@ -20,5 +20,5 @@ jobs: - run: >- docker run --rm tools sh -euc ' kubectl version --client; talosctl version --client --short; helm version --short; - sops --version; age --version; age-keygen --version; infisical --version; restic version; + sops --version; age --version; age-keygen --version; restic version; pulumi version; bun --version; cf --version' diff --git a/.sops.yaml b/.sops.yaml index dd516f6c..a025ed6e 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -19,7 +19,7 @@ creation_rules: # 秘密を含む chart の値(と、2 つめのドキュメントの Secret)。秘密のキーだけ。キーを足したらここにも足す # (当たらないキーは平文のままコミットされる)。 - path_regex: ^bootstrap/[^/]+-values\.sops\.yaml$ - encrypted_regex: ^(service\.webhook\.mattermost|githubAppPrivateKey|password|postgresPassword|ENCRYPTION_KEY|AUTH_SECRET|CLIENT_ID_GITHUB_LOGIN|CLIENT_SECRET_GITHUB_LOGIN|CF_DNS_API_TOKEN)$ + encrypted_regex: ^(service\.webhook\.mattermost|githubAppPrivateKey|CF_DNS_API_TOKEN)$ mac_only_encrypted: true age: age1sgnnkn67wpt4wgjsff4c36w3cmjkk557ng504e4aczx2lzls9y6ssy0x9r # クラスタの中で SopsSecret を復号する age の鍵(sops-secrets-operator が使う)。machine config に載せる diff --git a/README.md b/README.md index 7844bd61..b5452387 100644 --- a/README.md +++ b/README.md @@ -18,32 +18,35 @@ | | | | --- | --- | | `deploy/` | Argo CD が再帰的に同期するアプリのマニフェスト。**`_` で始まるディレクトリは同期しない** (動かしていないものの置き場。下の「動かしていないもの」) | -| [`bootstrap/`](bootstrap/) | クラスタそのものを組む層。**ここにあるものは全部 machine config に載る**([render.sh](bootstrap/render.sh) が描く)。Talos の設定([talos.sops.yaml](bootstrap/talos.sops.yaml))、Helm で入れるもの(Cilium・Argo CD 本体・Infisical・cert-manager)、RBAC。PR ごとに [talos-boot](.github/workflows/talos-boot.yml) が VM で起動して確かめる | +| [`bootstrap/`](bootstrap/) | クラスタそのものを組む層。**ここにあるものは全部 machine config に載る**([render.sh](bootstrap/render.sh) が描く)。Talos の設定([talos.sops.yaml](bootstrap/talos.sops.yaml))、Helm で入れるもの(Cilium・Argo CD 本体・cert-manager・OpenEBS LVM)、RBAC。PR ごとに [talos-boot](.github/workflows/talos-boot.yml) が VM で起動して確かめる | | [`pulumi/`](pulumi/) | Cloudflare(R2・DNS・ゾーンの設定・通知)・Entra(リダイレクト URI・CI の信頼の設定)・NetBird の設定を Pulumi で持つ。**PR で差分がコメントされ、main に入ると当たる** | | [`mado/`](mado/) | **`main` が落ちたときの入口**。JCOM 側の PC(10.10.0.2)で [mado](https://github.com/danything/mado)(OIDC でログインしてブラウザから RDP する exe)を動かす(`md.doany.io`。クラスタの外) | | [`recovery/`](recovery/) | **まっさらから建て直す入口**(手順・PV を戻す道具・暗号化した鍵)。git と restic だけで戻す | -| [`tools/`](tools/) + `compose.yaml` | **手元の運用のコマンド(cf・sops・infisical・kubectl・talosctl など)はコンテナで動かす**。`tools/t.ps1 <コマンド>`(Windows の WSL コンテナ。wslc-compose)。手元にコマンドは入れない | +| [`tools/`](tools/) + `compose.yaml` | **手元の運用のコマンド(cf・sops・kubectl・talosctl など)はコンテナで動かす**。`tools/t.ps1 <コマンド>`(Windows の WSL コンテナ。wslc-compose)。手元にコマンドは入れない | | [`docs/`](docs/) | [決定の記録](docs/decisions.md)、[Entra ID の認可](docs/entra.md)、[家のルーター](docs/router.md)、[Cloudflare の API トークン](docs/cloudflare-tokens.md) | | [`ROADMAP.md`](ROADMAP.md) | やることと進捗(なぜそうしたかは `docs/decisions.md`) | | `deploy/argocd.yaml` | このリポジトリ自身の Application 定義(他のリポジトリと同じ場所) | -| [`.sops.yaml`](.sops.yaml) | `bootstrap/*.sops.yaml` を SOPS(age)で暗号化する規則 | +| [`.sops.yaml`](.sops.yaml) | `*.sops.yaml` を SOPS(age)で暗号化する規則 | ## 秘密の扱い -**アプリの秘密は Infisical**(https://il.doany.io、このクラスタでセルフホスト)にあり、平文も暗号文も git には入らない。 -各アプリの `*-secrets.yaml` は `InfisicalSecret` で、フォルダは `//`、シークレット名が -そのまま Secret のキーになる。値を変えるのは Infisical の UI だけでよく、`secrets.infisical.com/auto-reload: "true"` -の注釈がある Deployment は Pod ごと入れ替わる。詳しくは [`bootstrap/README.md`](bootstrap/README.md)。 +**秘密は全部 SOPS + age で暗号化して git に置く。** 平文は git に入らない。 -**Infisical より下の層だけは Infisical から取れない**ので、`bootstrap/*.sops.yaml`(クラスタの CA、Infisical 自身の鍵、 -Argo CD が git を読む GitHub App 鍵、cert-manager の Cloudflare トークン、fj.doany.io の資格情報など)は **SOPS + age** で暗号化してある。 -鍵は [`recovery/sops-age.key.age`](recovery/)(バックアップの `env.age` と同じパスフレーズ)。 -当てるのは machine config(`bootstrap/render.sh` が復号して載せる)。書き方は [`bootstrap/README.md`](bootstrap/README.md)。 +- **アプリの秘密**は各アプリの `deploy/secrets.sops.yaml`(このリポジトリでは `deploy/<アプリ>/secrets.sops.yaml`)の `SopsSecret`。 + クラスタの中の [sops-secrets-operator](deploy/sops-secrets-operator/) が復号して `Secret` にし、 + `reloader.stakater.com/auto: "true"` の注釈がある workload は [Reloader](deploy/reloader/) が入れ替える。 + クラスタの鍵(`bootstrap/sops-age.sops.yaml`)と管理者の鍵の両方で暗号化する([`.sops.yaml`](.sops.yaml))。 +- **クラスタを組む層の秘密**(`bootstrap/*.sops.yaml`。クラスタの CA、Argo CD が git を読む GitHub App 鍵、 + cert-manager の Cloudflare トークン、fj.doany.io の資格情報、クラスタの age の鍵)は machine config に載る(`bootstrap/render.sh` が復号して載せる)。 + 書き方は [`bootstrap/README.md`](bootstrap/README.md)。 + +管理者の鍵は [`recovery/sops-age.key.age`](recovery/)(バックアップの `env.age` と同じパスフレーズ)。 +2026-11-02 まではアプリの秘密をセルフホストの Infisical に置いていた(やめた理由は [docs/decisions.md](docs/decisions.md)「秘密は SOPS で git に置く」)。 ```shell # 鍵はリポジトリの .home/(git に入らない)に置く。コマンドは tools/t.ps1 で動かす(tools/README.md) tools/t.ps1 sh -c 'mkdir -p ~/.config/sops/age && age -d -o ~/.config/sops/age/keys.txt recovery/sops-age.key.age' -tools/t.ps1 sops -d bootstrap/infisical-values.sops.yaml # 中を見る(画面に出すと平文になるので注意) +tools/t.ps1 sops edit deploy/matrix/secrets.sops.yaml # 直して push する(画面に出すと平文になるので -d で見るときは注意) ``` ## ホスト名の付け方 @@ -53,7 +56,7 @@ tools/t.ps1 sops -d bootstrap/infisical-values.sops.yaml # 中を見る(画 | アプリ名 | 規則 | 結果 | | --- | --- | --- | | 2 語以上 | **それぞれの頭文字** | NetBird(Net + Bird)→ `nb`、Argo CD → `ac`、AdGuard Home → `ah` | -| 1 語 | **頭文字と最後の子音** | denpa → `dp`、hubble → `hl`、infisical → `il`、tamasagashi → `ts`、yosegaki → `yk`、proxy → `px` | +| 1 語 | **頭文字と最後の子音** | denpa → `dp`、hubble → `hl`、tamasagashi → `ts`、yosegaki → `yk`、proxy → `px` | **1 文字で足りていたものはそのまま。** 先に取ったもの勝ちで、`a`(auth)・`d`(AdGuard)・ `h`(headlamp)・`l`(lgtm)・`w`(worklog)・`x`(xool)・`y`(yuzuriha)・`m`(matrix)・`e`(element)は 1 文字で置いてある。 @@ -82,7 +85,7 @@ headlamp が 1 文字なのは、規則どおりだと head + lamp でも hubble | [`netbird/`](deploy/netbird/) | NetBird (VPN。combined コンテナ + 内蔵 IdP) | | [`3proxy/`](deploy/3proxy/) | 3proxy (国内IP経由の HTTPS フォワードプロキシ) | | [`volsync/`](deploy/volsync/)・[`backup/`](deploy/backup/) | バックアップ(VolSync で PVC のスナップショットを restic → Cloudflare R2)と、その見張り・etcd のスナップショット | -| [`infisical/`](deploy/infisical/) | 秘密の配布。本体は `bootstrap/` にあり、ここには公開経路と operator と即時反映のブリッジと SMTP のパスワード | +| [`sops-secrets-operator/`](deploy/sops-secrets-operator/)・[`reloader/`](deploy/reloader/) | 秘密の配布(git の `SopsSecret` を復号して Secret に)と、Secret が変わったときの Pod の入れ替え | | [`cluster/`](deploy/cluster/) | metrics-server(Talos に無い。`kubectl top` と各 UI の使用量表示) | | [`smtp-relay/`](deploy/smtp-relay/) | メールの中継。アプリの SMTP を受けて Microsoft 365 へ Graph で送る(Exchange Online の SMTP AUTH の Basic 認証の廃止に備えて)。**送る側の設定は [deploy/smtp-relay/README.md](deploy/smtp-relay/README.md)** | | [`renovate/`](deploy/renovate/) | Renovate(GitHub の danything・5ym と Forgejo の doa の全リポジトリの依存を上げる PR)。CronJob で 15 分おき。**秘密の入れ方は [deploy/renovate/README.md](deploy/renovate/README.md)** | diff --git a/ROADMAP.md b/ROADMAP.md index 4d74a115..6707f3d2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -41,7 +41,7 @@ Z440 は Intel AMT 9.x(ME 9.1)を持つ。OS が止まっていても**電源の - **場所**: AMT は内蔵の I218-LM(`eno1`)にしか乗らないので、`eno1` を 10.10 側(`jcom0`)に回して 10.10.0.3 に置いた(#667)。作業用 PC から直接届く - **設定(MEBx。起動中に Ctrl+P)**: IPv4 は固定で 10.10.0.3/24・ゲートウェイ 10.10.0.1、IPv6 は無効、SOL と IDE-R は有効、Network Access は Activate、 - User Consent はなし、電源は「ON in S0, ME Wake in S3, S4-5」。パスワードは Infisical + User Consent はなし、電源は「ON in S0, ME Wake in S3, S4-5」。パスワードは Infisical に置いていた(2026-11-02 にやめた) - **守り**: パスワード 1 つで電源もコンソールも握れるので**家の外に出さない**(DMZ と IPv6 の公開は 10.0.0.2 と bond0 の MAC だけなので当たらない)。 口は 16992・16993(管理)と 16994・16995(SOL・IDE-R)。ノード自身からは自分の AMT に届かない - **つなぎ方**: Windows は Intel の [Device Management Toolkit の Console](https://github.com/device-management-toolkit/console)(`console_windows_x64.exe`。常駐させない)。 diff --git a/bootstrap/README.md b/bootstrap/README.md index 8a9aa34e..6f79a1f8 100644 --- a/bootstrap/README.md +++ b/bootstrap/README.md @@ -6,8 +6,8 @@ ArgoCD はここを見ない(例外は [argocd-repos.yaml](argocd-repos.yaml) | | 何を | いつ | | --- | --- | --- | -| **machine config**(このディレクトリ) | Talos の設定、Helm で入れるもの(cilium・cert-manager・argocd・infisical・lvm-localpv)、`apiserver-rbac.yaml`、`storage.yaml`、SOPS 済みの Secret 2 つ | `apply.sh` → `upgrade-k8s`([talos.md](talos.md)「当て方」) | -| **ArgoCD**(`deploy/`) | 平文のマニフェスト(`ClusterIssuer`・Gateway・auth・`InfisicalSecret` など)。2026-10 まではここにあって、Application `bootstrap` が当てていた | main へのマージ(prune / selfHeal 付き) | +| **machine config**(このディレクトリ) | Talos の設定、Helm で入れるもの(cilium・cert-manager・argocd・lvm-localpv)、`apiserver-rbac.yaml`、`storage.yaml`、SOPS 済みの Secret 2 つ | `apply.sh` → `upgrade-k8s`([talos.md](talos.md)「当て方」) | +| **ArgoCD**(`deploy/`) | 平文のマニフェスト(`ClusterIssuer`・Gateway・auth など)と SOPS 済みの `SopsSecret`。2026-10 まではここにあって、Application `bootstrap` が当てていた | main へのマージ(prune / selfHeal 付き) | ## ファイル @@ -21,7 +21,6 @@ ArgoCD はここを見ない(例外は [argocd-repos.yaml](argocd-repos.yaml) | [talos-secrets.sops.yaml](talos-secrets.sops.yaml) | クラスタの CA 一式(丸ごと SOPS) | | [cilium-values.yaml](cilium-values.yaml) | chart の値と版(下の「Helm で入れるもの」) | | [argocd-values.sops.yaml](argocd-values.sops.yaml) | chart の値と版。通知先の URL と GitHub App の鍵だけ SOPS | -| [infisical-values.sops.yaml](infisical-values.sops.yaml) | chart の値と版(DB と Redis のパスワードだけ SOPS)。2 つめのドキュメントが infisical の起動に要る Secret(`ENCRYPTION_KEY` など) | | [cert-manager-values.sops.yaml](cert-manager-values.sops.yaml) | chart の値と版。2 つめのドキュメントが DNS-01 の Cloudflare のトークンの Secret(トークンだけ SOPS) | | [apiserver-rbac.yaml](apiserver-rbac.yaml) / [apiserver-ca.crt](apiserver-ca.crt) | API サーバの入口の RBAC と CA(下の「API サーバの入口」) | | [lvm-localpv-values.yaml](lvm-localpv-values.yaml) | chart の値と版(OpenEBS LVM LocalPV。PVC の置き場。下の「PVC の置き場」) | @@ -47,8 +46,7 @@ prune を `allowEmpty` 無しでは拒むが、頼らない)。 ArgoCD の平文のマニフェストは `deploy/` にある: [deploy/gateway/](../deploy/gateway/)(Gateway と http → https)、[deploy/auth/](../deploy/auth/)(SSO の oauth2-proxy)、 -[deploy/cert-manager/](../deploy/cert-manager/)(`ClusterIssuer`)、[deploy/argocd/](../deploy/argocd/)(HTTPRoute と OIDC の `InfisicalSecret`)、 -[deploy/infisical/smtp-secret.yaml](../deploy/infisical/smtp-secret.yaml)。 +[deploy/cert-manager/](../deploy/cert-manager/)(`ClusterIssuer`)、[deploy/argocd/](../deploy/argocd/)(HTTPRoute と OIDC の `SopsSecret`)。 `ClusterIssuer` や Gateway は本体(cert-manager・cilium)が machine config に居ても、書き換えるのは平文のマニフェストなので ArgoCD に任せる。 @@ -66,11 +64,11 @@ ArgoCD に任せる。 `# renovate:` の行から `render.sh` が chart(`depName`)と repo(`registryUrl`)を、次の `# version:` の行から版を読む。 版をコメントにしているのは、値として書くと chart に渡ってしまうから。Renovate も同じ 2 行を見て上げる(`renovate.json` の customManagers)。 Talos・Kubernetes・Gateway API の版も同じ形で、[talos.sops.yaml](talos.sops.yaml) の先頭のコメントにある。 -秘密を含む argocd・infisical・cert-manager は `-values.sops.yaml` で、秘密の値だけ暗号化してある(上の SOPS の段落)。 +秘密を含む argocd・cert-manager は `-values.sops.yaml` で、秘密の値だけ暗号化してある(上の SOPS の段落)。 `render.sh` が復号してから helm に渡す。**2 つめのドキュメントがあれば、それは chart とは別に machine config に載せる Secret** -(infisical の `infisical-secrets`、cert-manager の Cloudflare のトークン。ArgoCD は復号できないのでここに置く)で、 +(cert-manager の Cloudflare のトークン。ArgoCD は復号できないのでここに置く)で、 `render.sh` が `---` で分けて別の inlineManifest にする。 -argocd・infisical の版は自動マージしない(`renovate.json` の `bootstrap charts`)。lvm-localpv も(`openebs lvm`。`storage.yaml` の lvm-driver のイメージと同じ PR)。 +argocd の版は自動マージしない(`renovate.json` の `bootstrap charts`)。lvm-localpv も(`openebs lvm`。`storage.yaml` の lvm-driver のイメージと同じ PR)。 Namespace は chart が描かないので、`render.sh` が kube-system 以外のものに付ける。 ## git と live がずれていないか @@ -110,7 +108,7 @@ kubectl -n kube-system get gateway doany ## PVC の置き場 **OpenEBS LVM LocalPV**(thin)。PVC ごとに VG `data` の thin プールから LV を切り、xfs で渡す。 -infisical の Postgres が要るので bootstrap 層に居る。選んだ理由と組んだ形は [../docs/decisions.md](../docs/decisions.md)「ストレージを OpenEBS LVM LocalPV に…」。 +各アプリの PVC が載る土台なので bootstrap 層に居る。選んだ理由と組んだ形は [../docs/decisions.md](../docs/decisions.md)「ストレージを OpenEBS LVM LocalPV に…」。 | 何を | 誰が | | --- | --- | @@ -128,16 +126,14 @@ infisical の Postgres が要るので bootstrap 層に居る。選んだ理由 ## Secret -平文の Secret はアプリのリポジトリにはコミットしない。値は **Infisical**(https://il.doany.io、このクラスタでセルフホスト)が持ち、 -**Infisical 純正 operator** が各リポジトリの `InfisicalSecret` を見て Infisical から値を取り、普通の `Secret` を作る。 +平文の Secret はどのリポジトリにもコミットしない。**値は SOPS で暗号化して git に置き**(各リポジトリの `deploy/` の +`SopsSecret`)、クラスタの中の [sops-secrets-operator](../deploy/sops-secrets-operator/) が復号して普通の `Secret` を作る +(理由は [../docs/decisions.md](../docs/decisions.md)「秘密は SOPS で git に置く」。2026-11-02 までは Infisical)。 -- **本体**(chart + Postgres/Redis)だけがここ(machine config)。`infisical-values.sops.yaml` の 2 つめのドキュメントの - `ENCRYPTION_KEY` が DB の暗号鍵で、これを失うと Infisical の中身が全部読めなくなる(クラスタで一番失ってはいけない値)。 - Postgres の PVC は既定の `local-path`(OpenEBS LVM)で、VolSync が日次でスナップショットから取っている([../deploy/infisical/volsync.yaml](../deploy/infisical/volsync.yaml))。 -- **公開経路・operator・push-bridge は [`deploy/infisical/`](../deploy/infisical/)**(ArgoCD 自身は operator に依存しないので、ここに置く理由がない)。 -- 認証は Infisical の Kubernetes auth: operator が SA `infisical-auth` のトークンで Infisical にログインし、 - Infisical がそのトークンで TokenReview を呼んで本物か確かめる(なので SA に `system:auth-delegator` を付けてある。 - `deploy/infisical/operator-rbac.yaml`)。長命の資格情報はどこにも無い(`identityId` は秘密ではない)。 +- **operator が復号に使うクラスタの age の鍵だけがここ**([sops-age.sops.yaml](sops-age.sops.yaml)。machine config が + `sops-secrets-operator` の namespace ごと入れる)。Argo CD も CI もこの鍵を持たない。 +- `SopsSecret` はクラスタの鍵と管理者の鍵の両方で暗号化する([../.sops.yaml](../.sops.yaml) の `deploy/` の規則)ので、手元で `sops edit` できる。 +- Secret が変わったときの Pod の入れ替えは [Reloader](../deploy/reloader/)(workload に `reloader.stakater.com/auto: "true"`)。 ### API サーバの入口(GitHub Actions と人) @@ -155,74 +151,17 @@ Headlamp が利用者本人の id_token を API サーバへ転送する。ア **ArgoCD には当てさせない**(ArgoCD も CI も要らずに効く層なので、締め出されたときに直せる)。 変えたら `render.sh` → `apply-config`(RBAC は inlineManifest なので `upgrade-k8s` まで)。 -### Infisical の中の構成 - -| | | -| --- | --- | -| Project | `doa`(slug `doa`)、環境は `prod` だけ | -| フォルダ | `//`(例: `/denpa/denpa-oidc`、`/worklog/ghcr-pull`) | -| シークレット名 | 作られる `Secret` の **キー名そのまま**(`client-id`、`.dockerconfigjson` など) | -| Machine Identity | `infisical-operator`(Kubernetes auth、許可 SA は `infisical/infisical-auth`、project の viewer) | -| 管理者 | 個人アカウント。資格情報はリポジトリに置かない。復旧手順は下の「Infisical の管理者アカウント」 | -| 共有値 | `/shared/entra`(Entra 共用アプリの client-id / client-secret / issuer / role / tenant-id)と `/shared/smtp`(info@doany.io)。各アプリのフォルダは `${prod.shared.entra.client-secret}` のような参照で、ローテーションは shared 側の 1 回で済む(operator が展開する) | -| SMTP | info@doany.io から、クラスタの中の中継 [deploy/smtp-relay](../deploy/smtp-relay/README.md) 経由(Graph の sendMail)。パスワード(中継のアプリのクライアントシークレット)だけ Infisical の `/infisical/infisical-smtp` に置き、[deploy/infisical/smtp-secret.yaml](../deploy/infisical/smtp-secret.yaml) が Secret にする | - -### Infisical の管理者アカウント - -初期化(`POST /api/v1/admin/bootstrap`)で作った `admin@doany.io` は現存しない -(2026-09-02 にログインを試して "Failed to find user")。いまの管理者は個人アカウントで、 -資格情報はこのリポジトリに置かない。 - -ロックアウトしたときの復旧は、SMTP(info@doany.io 経由)が生きていればログイン画面の -パスワードリセット。それも届かない場合は `infisical` ns の PostgreSQL に入って -`users` テーブルから管理者を特定し、Infisical のドキュメントにある手順でリセットする。 - -初期化で一緒に作られる "Instance Admin Identity"(root 相当の Machine Identity)は -セットアップ後に削除してある。必要なら UI の Organization Settings > Identities で作り直す。 - -DB の PVC は VolSync で戻すが、Secret は手元の restic.env から作る(ほかの VolSync の Secret が Infisical から来るので)。戻したら operator の -Kubernetes auth の CA も差し替える。手順は [../recovery/README.md](../recovery/README.md) の 6・7。 - -### `InfisicalSecret` に sync-options の注釈は要らない - -- **prune されない。** operator は CR の annotation(Argo CD の tracking-id も)を作った `Secret` にコピーするが、 - Argo CD は `Secret` を丸ごと管理の対象から外している(`argocd-values.sops.yaml` の `resource.exclusions`)ので見ない -- **CRD が無くても止まらない。** まっさらなクラスタでは CRD(Infisical の operator・VolSync)がまだ無く、同じ同期の中の CR の - dry-run で同期が丸ごと落ちる。これは ApplicationSet(`argocd-repos.yaml`)の syncOptions の `SkipDryRunOnMissingResource=true` で - 全部のアプリに効かせてある。ApplicationSet の外の Application(chart を入れるものなど)で CR を書くときだけ、そちらに付ける - ### アプリ側の書き方 -各リポジトリの `deploy/` に `InfisicalSecret` を置く。フォルダ内のシークレットを全部そのままキーにするので、 -Infisical にキーを足せば InfisicalSecret は触らずに済む(push-bridge が数秒で同期、保険のポーリングは既定5分)。 +各リポジトリの `deploy/secrets.sops.yaml` に namespace ごと 1 つの `SopsSecret` を置き、`secretTemplates` に Secret を並べる +(例: [../deploy/matrix/secrets.sops.yaml](../deploy/matrix/secrets.sops.yaml))。暗号化するのは `data`・`stringData` の値だけ +(先頭のコメントと名前は平文のまま読める)。各リポジトリの `.sops.yaml` もこのリポジトリの `deploy/` の規則と同じ鍵 2 つにする。 -```yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: denpa-oidc - namespace: denpa -spec: - hostAPI: http://infisical.infisical.svc:8080/api - authentication: - kubernetesAuth: - identityId: f2bc3416-f196-4626-9bf3-c11693b32cc6 - autoCreateServiceAccountToken: true - serviceAccountRef: - name: infisical-auth - namespace: infisical - secretsScope: - projectSlug: doa - envSlug: prod - secretsPath: /denpa/denpa-oidc - managedKubeSecretReferences: - - secretName: denpa-oidc - secretNamespace: denpa +```shell +tools/t.ps1 sops edit deploy/<アプリ>/secrets.sops.yaml # 直して push すると、Argo CD が当てた時点で Secret が変わる ``` -`kubernetes.io/dockerconfigjson` のような型が要るときは `managedKubeSecretReferences` に -`secretType` を足す。ラベル等は `template`(`includeAllSecrets: true` を忘れずに — 無いと中身を捨てる)。 - -値を変えるときは Infisical の UI(または CLI)で書き換えるだけ。Git には何も入らない。 -`secrets.infisical.com/auto-reload: "true"` の注釈がある Deployment は自動で入れ替わる。 -注釈が無いもの(Helm チャート系)だけ `kubectl rollout restart` する。 +**sync-options の注釈は要らない。** まっさらなクラスタでは `SopsSecret` の CRD がまだ無く、同じ同期の中の CR の +dry-run で同期が丸ごと落ちる。これは ApplicationSet(`argocd-repos.yaml`)の syncOptions の `SkipDryRunOnMissingResource=true` で +全部のアプリに効かせてある。ApplicationSet の外の Application(chart を入れるものなど)で CR を書くときだけ、そちらに付ける。 +作られた `Secret` は Argo CD が丸ごと管理の対象から外している(`argocd-values.sops.yaml` の `resource.exclusions`)ので prune されない。 diff --git a/bootstrap/apiserver-rbac.yaml b/bootstrap/apiserver-rbac.yaml index e85f23d9..08219514 100644 --- a/bootstrap/apiserver-rbac.yaml +++ b/bootstrap/apiserver-rbac.yaml @@ -70,15 +70,13 @@ rules: - metrics.k8s.io resources: ["*"] verbs: ["get", "list", "watch"] - # クラスタに実際に居る CRD の群。InfisicalSecret は値ではなく参照しか持たないので - # 読めても秘密は出ない。 + # クラスタに実際に居る CRD の群。秘密の値を持つものは入れない。 - apiGroups: - gateway.networking.k8s.io - argoproj.io - cert-manager.io - acme.cert-manager.io - cilium.io - - secrets.infisical.com - volsync.backube resources: ["*"] verbs: ["get", "list", "watch"] diff --git a/bootstrap/argocd-repos.yaml b/bootstrap/argocd-repos.yaml index 8c89189d..fd414d5d 100644 --- a/bootstrap/argocd-repos.yaml +++ b/bootstrap/argocd-repos.yaml @@ -63,7 +63,7 @@ spec: - CreateNamespace=true # PVC の dataSourceRef(VolSync の populator)は作るときにしか書けない。今ある PVC には当てない(ignoreDifferences) - RespectIgnoreDifferences=true - # CRD を入れるもの(Infisical の operator・VolSync)と同じ同期の中に CR があるので、まっさらなクラスタで + # CRD を入れるもの(sops-secrets-operator・VolSync)と同じ同期の中に CR があるので、まっさらなクラスタで # CRD がまだ無くても dry-run で落ちないようにする。リソースごとの注釈は要らない - SkipDryRunOnMissingResource=true # Argo CD が作る namespace に付ける。VolSync の mover を root で動かす(../deploy/volsync/application.yaml)。 @@ -101,7 +101,7 @@ spec: goTemplateOptions: - missingkey=error generators: - # owner を改名したら必ずここと Infisical /argocd/forgejo-repo-creds の url を直す + # owner を改名したら必ずここと deploy/argocd/secrets.sops.yaml の forgejo-repo-creds の url を直す # (Forgejo の 307 を ArgoCD は追わず、repo-creds は URL の接頭辞で照合する)。 # 同じリポジトリが GitHub にもあると Application 名がぶつかる。移すときは GitHub 側を消してから。 - matrix: @@ -151,7 +151,7 @@ spec: - CreateNamespace=true # PVC の dataSourceRef(VolSync の populator)は作るときにしか書けない。今ある PVC には当てない(ignoreDifferences) - RespectIgnoreDifferences=true - # CRD を入れるもの(Infisical の operator・VolSync)と同じ同期の中に CR があるので、まっさらなクラスタで + # CRD を入れるもの(sops-secrets-operator・VolSync)と同じ同期の中に CR があるので、まっさらなクラスタで # CRD がまだ無くても dry-run で落ちないようにする。リソースごとの注釈は要らない - SkipDryRunOnMissingResource=true # Argo CD が作る namespace に付ける。VolSync の mover を root で動かす(../deploy/volsync/application.yaml)。 diff --git a/bootstrap/argocd-values.sops.yaml b/bootstrap/argocd-values.sops.yaml index d73346a3..18003921 100644 --- a/bootstrap/argocd-values.sops.yaml +++ b/bootstrap/argocd-values.sops.yaml @@ -29,14 +29,13 @@ configs: name: Microsoft Entra ID issuer: https://login.microsoftonline.com/22ac977d-b140-4dc8-8150-90d645d3475b/v2.0 clientID: b0fa498f-7e6a-4fe1-a1c6-16fbbb6f397e - # Secret argocd-oidc, built by oidc-secret.yaml from Infisical. + # Secret argocd-oidc, built by sops-secrets-operator from deploy/argocd/secrets.sops.yaml. clientSecret: $argocd-oidc:client-secret requestedScopes: - openid - profile - email - # Secret は管理対象から外す。Infisical operator の Secret は CR の tracking-id がコピーされ、 - # 外さないと sync で prune される。 + # Secret は管理対象から外す。Secret は sops-secrets-operator(など)が作り、Argo CD は持たない。 resource.exclusions: | - apiGroups: [""] kinds: ["Secret"] @@ -136,7 +135,7 @@ sops: kPmcW7H+UUKySYI3pNEsDUbFfsRpcdL/is9Mhpc6BsZuq4wqkIYs0Q== -----END AGE ENCRYPTED FILE----- recipient: age1sgnnkn67wpt4wgjsff4c36w3cmjkk557ng504e4aczx2lzls9y6ssy0x9r - encrypted_regex: ^(service\.webhook\.mattermost|githubAppPrivateKey|password|postgresPassword|ENCRYPTION_KEY|AUTH_SECRET|CLIENT_ID_GITHUB_LOGIN|CLIENT_SECRET_GITHUB_LOGIN|CF_DNS_API_TOKEN)$ + encrypted_regex: ^(service\.webhook\.mattermost|githubAppPrivateKey|CF_DNS_API_TOKEN)$ lastmodified: "2026-10-30T15:08:34Z" mac: ENC[AES256_GCM,data:IUTmLKy52nMOMJcPFmc823XbNmgCBvDHeoeDPpIYJwliohLnUwtZ+imdxp7/h7OocaP/Q19cxs+S+4q//1h4rV9qA7YZo33mWU8CUoRVfdsJWy0YZ3lhiJEJR4xtbsCXfWHKWeXQYV87TO79+kcYZ8WBbn25yeDhebBh+N9mx0k=,iv:flt6viuRm96UTSBa62rnIDlMAL6kHyLjI5766RoTDXg=,tag:D/A3SFgZ3vztLIwK1i003w==,type:str] mac_only_encrypted: true diff --git a/bootstrap/cert-manager-values.sops.yaml b/bootstrap/cert-manager-values.sops.yaml index 9ae9ef69..53d93b91 100644 --- a/bootstrap/cert-manager-values.sops.yaml +++ b/bootstrap/cert-manager-values.sops.yaml @@ -22,7 +22,7 @@ sops: IyM3TTxcSYEkM35DpB/oe7wnfrOyeDuxh5PLzedVyywkmYZQblInSQ== -----END AGE ENCRYPTED FILE----- recipient: age1sgnnkn67wpt4wgjsff4c36w3cmjkk557ng504e4aczx2lzls9y6ssy0x9r - encrypted_regex: ^(service\.webhook\.mattermost|githubAppPrivateKey|password|postgresPassword|ENCRYPTION_KEY|AUTH_SECRET|CLIENT_ID_GITHUB_LOGIN|CLIENT_SECRET_GITHUB_LOGIN|CF_DNS_API_TOKEN)$ + encrypted_regex: ^(service\.webhook\.mattermost|githubAppPrivateKey|CF_DNS_API_TOKEN)$ lastmodified: "2026-10-30T15:08:34Z" mac: ENC[AES256_GCM,data:lwkLBKlc/fbnJvJYCNC+gxvoJrR58uYugPX/0E5rX6X5NtLwjl2d89xi06nhhrpcIpunpWESWA6d/2myhrIOJTo9NUGxgbJbQXsZHbnRvV07c7oR2a29W9R3rtKKeJTl9IGc5GSoyo4sJ4PygoaHYzQmRgN8K5GVAI7BNJswmd4=,iv:SOqIQsANQ/8+/0jG4njrGzSDRivPyRQUAGwuUxQUpVM=,tag:e0fbD5/Gk4Rhyj2hwBvllQ==,type:str] mac_only_encrypted: true @@ -47,7 +47,7 @@ sops: IyM3TTxcSYEkM35DpB/oe7wnfrOyeDuxh5PLzedVyywkmYZQblInSQ== -----END AGE ENCRYPTED FILE----- recipient: age1sgnnkn67wpt4wgjsff4c36w3cmjkk557ng504e4aczx2lzls9y6ssy0x9r - encrypted_regex: ^(service\.webhook\.mattermost|githubAppPrivateKey|password|postgresPassword|ENCRYPTION_KEY|AUTH_SECRET|CLIENT_ID_GITHUB_LOGIN|CLIENT_SECRET_GITHUB_LOGIN|CF_DNS_API_TOKEN)$ + encrypted_regex: ^(service\.webhook\.mattermost|githubAppPrivateKey|CF_DNS_API_TOKEN)$ lastmodified: "2026-10-30T15:08:34Z" mac: ENC[AES256_GCM,data:lwkLBKlc/fbnJvJYCNC+gxvoJrR58uYugPX/0E5rX6X5NtLwjl2d89xi06nhhrpcIpunpWESWA6d/2myhrIOJTo9NUGxgbJbQXsZHbnRvV07c7oR2a29W9R3rtKKeJTl9IGc5GSoyo4sJ4PygoaHYzQmRgN8K5GVAI7BNJswmd4=,iv:SOqIQsANQ/8+/0jG4njrGzSDRivPyRQUAGwuUxQUpVM=,tag:e0fbD5/Gk4Rhyj2hwBvllQ==,type:str] mac_only_encrypted: true diff --git a/bootstrap/infisical-values.sops.yaml b/bootstrap/infisical-values.sops.yaml deleted file mode 100644 index 3fa1b291..00000000 --- a/bootstrap/infisical-values.sops.yaml +++ /dev/null @@ -1,121 +0,0 @@ -# chart と版。Renovate が上げ、render.sh が読む(README.md「Helm で入れるもの」)。 -# renovate: datasource=helm depName=infisical-standalone registryUrl=https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/ -# version: 1.12.0 -# infisical-standalone の chart の値(Postgres + Redis 同梱)。 -# 秘密(DB と Redis のパスワード、2 つめのドキュメントの Secret の鍵)の値だけ SOPS で暗号化してある(../.sops.yaml)。 -# 平文の部分はそのまま直してよい。秘密を変えるときは `sops edit bootstrap/infisical-values.sops.yaml`。 -# 各ドキュメントの末尾の `sops:` は SOPS のメタデータ(触らない)。 -# 描いて machine config に載せるのは render.sh(ArgoCD に置けない理由は docs/decisions.md)。 -infisical: - # 無いと名前が infisical-infisical-standalone-infisical になる - fullnameOverride: infisical - replicaCount: 1 - image: - # Docker Hub の匿名取得は絞られるので mirror.gcr.io から。chart の既定の tag は遅れるので固定する - repository: mirror.gcr.io/infisical/infisical - tag: v0.167.4 - # ENCRYPTION_KEY / AUTH_SECRET / SITE_URL は下の 2 つめのドキュメント(render.sh が別の inlineManifest にする) - kubeSecretRef: infisical-secrets - extraEnv: - # Kubernetes auth の TokenReview で kubernetes.default.svc を叩くのを SSRF ガードが拒むため - - name: ALLOW_INTERNAL_IP_CONNECTIONS - value: "true" - # 送るのはクラスタの中の中継(deploy/smtp-relay。Graph の sendMail)。ユーザー名は <テナント ID>@、 - # パスワードはそのクライアントシークレット。中継は TLS を話さない。 - # パスワードだけ Infisical 自身にある(smtp-secret.yaml)ので optional。まっさらな状態ではメール無しで - # 上がり、operator が Secret を作ったあと rollout restart でメールが有効になる。 - - name: SMTP_HOST - value: smtp-relay.smtp-relay.svc - - name: SMTP_PORT - value: "587" - - name: SMTP_USERNAME - value: 22ac977d-b140-4dc8-8150-90d645d3475b@fd45a316-97cc-4480-8da8-487109403520 - - name: SMTP_REQUIRE_TLS - value: "false" - - name: SMTP_FROM_ADDRESS - value: info@doany.io - - name: SMTP_FROM_NAME - value: Infisical - - name: SMTP_PASSWORD - valueFrom: - secretKeyRef: - name: infisical-smtp - key: SMTP_PASSWORD - optional: true -ingress: - # 公開は Gateway(deploy/infisical/httproute.yaml) - enabled: false - nginx: - # 同梱の ingress-nginx の切り替えも兼ねる - enabled: false -postgresql: - enabled: true - auth: - username: infisical - database: infisicalDB - password: ENC[AES256_GCM,data:wlMm2O23q4WNlRwffwVYsUUk4KkPNpuD91naRVZbgtqQ+c4NMDk9jNNn15HT8GCo,iv:nJx7sPa8QQaCA7B9fmvHRdBInMgGW6A8EnylaiK1nnw=,tag:tIr2dIACNDX6uqcZWHtGVQ==,type:str] - postgresPassword: ENC[AES256_GCM,data:UShd+eP5OksJW8Iy4COXWdAWsUd8fVsb+MiPlRwL8oU=,iv:wmHmVSU5HWsS0D8ykTHgWUiQ6ydP8QHzy8y3RomI+R4=,tag:njAsN+kX7/HiQCt5XZbrdg==,type:str] - primary: - persistence: - # chart を消しても PVC は残る。後ろ盾は VolSync の日次スナップショット(deploy/volsync/infisical.yaml) - storageClass: local-path - size: 5Gi -redis: - enabled: true - auth: - password: ENC[AES256_GCM,data:M+WupyRhc2FOxY3GGrQjeagXLYl1je2lsVPOTAbx3mGjjueG4/4IUgjeKK5EBmhT,iv:LbzZElFqO6IvxcvJ0RKiG3uqwHUC33WvwQVCtokhDKM=,tag:0kDzNb0WIMotjRwz0N2fFg==,type:str] - master: - persistence: - # キューとキャッシュだけ - enabled: false -sops: - age: - - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0dkw4ODdPQTlIRjQ5eGQ5 - Z3pPdWpzaXJzUVZINHJZSithNDNHRU4rZ0RvCjJBZ2xNYnU2K0pUaDV3K0dEMkFZ - VzhnaU1GYVYvcHVkbFRZMzhoVkdQSTQKLS0tIG5jOTl0RDVaalozQnUyZDBYdk83 - dWJMVkFKbWFYVUM3bzQwU1NFbEI4bzAKKCsNzczSTCeeQXf6nWJMGQp0cKlz7S9/ - qtsRZw60oOBsT5FVXjkteih1JsiKMPZ1lQ/Pyo+m9HG1dHRDo26Clw== - -----END AGE ENCRYPTED FILE----- - recipient: age1sgnnkn67wpt4wgjsff4c36w3cmjkk557ng504e4aczx2lzls9y6ssy0x9r - encrypted_regex: ^(service\.webhook\.mattermost|githubAppPrivateKey|password|postgresPassword|ENCRYPTION_KEY|AUTH_SECRET|CLIENT_ID_GITHUB_LOGIN|CLIENT_SECRET_GITHUB_LOGIN|CF_DNS_API_TOKEN)$ - lastmodified: "2026-10-30T15:08:34Z" - mac: ENC[AES256_GCM,data:prnnSJpB8SQTP/U4slFI3PfBrbVI1EbEFErwoaaN+kqWhqZnbua7zTqAMFwzmygJ525Ku88sqi2Wt1k1RwPoaGFR1FMObgmAPw1UavZ6MFp5Cv2r7bnvhnRxQL12KTko79t+8I3mzVmOCu8QsQ1eQV/WXMxBMuigNsEGW+msjWQ=,iv:AjIP77ppC18nE5Nc4Apkr+EjBjvzMbV0kHAYOaI2YNM=,tag:ultvRWNfPiNRO3QcltR2yQ==,type:str] - mac_only_encrypted: true - version: 3.13.3 ---- -# Infisical 自身の根っこの秘密(DB の暗号鍵など)。Infisical がほかの秘密を全部持つための層なので、 -# Infisical からは取れない。値のキーだけ SOPS で暗号化してある(../.sops.yaml)。 -# -# ENCRYPTION_KEY は Infisical が Postgres に置く全部を暗号化する鍵。失くしたり変えたりすると DB が丸ごと読めなくなる。 -# ここで唯一、何があっても残すべき値。AUTH_SECRET はセッションの署名だけなので、入れ替えてよい(全員ログアウトになる)。 -apiVersion: v1 -kind: Secret -metadata: - name: infisical-secrets - namespace: infisical -type: Opaque -stringData: - ENCRYPTION_KEY: ENC[AES256_GCM,data:9A0VreQ45AuqNv1OrT2WUIeL80dLumA9GIBBXf7NUmg=,iv:0syrMhtgIJz27uwwmovB4TxsD8knSFW1wl3lOaOqtlc=,tag:DjO8DoPPGR4YQXrdKc5VLw==,type:str] - AUTH_SECRET: ENC[AES256_GCM,data:5QChOKF42iagvY+GkHm6Z5vGqivQ0xJToFK9tDaVjhjAcCOak0hDHjaWM0w=,iv:haTEfu2atH+R6OqYnWZhQ7Ujnt0o0X5F45Twn7tMM2A=,tag:jn2PYb3wJ0O16ZQ5SjNHQg==,type:str] - SITE_URL: https://il.doany.io - TELEMETRY_ENABLED: "false" - CLIENT_ID_GITHUB_LOGIN: ENC[AES256_GCM,data:PhxlBNJyAbj0MpnNrx+b1woqlXw=,iv:RZ0J1dO4O6pLdxgrnXQmDL2HDnqDSLGnK5KiQjNUngU=,tag:e+lJugHw6z910GnwF4MWPA==,type:str] - CLIENT_SECRET_GITHUB_LOGIN: ENC[AES256_GCM,data:80QRjNxUoW/r7PVTvimqYDCaw6XRsL4LuH7BROIpqOSgFQPCY1AVnQ==,iv:oFAmKI9d50dfYy9GeZ7mMYeYyDHEs3tjcwPl9nJM86o=,tag:f8/ROablE0uN/o20dHvS/A==,type:str] -sops: - age: - - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0dkw4ODdPQTlIRjQ5eGQ5 - Z3pPdWpzaXJzUVZINHJZSithNDNHRU4rZ0RvCjJBZ2xNYnU2K0pUaDV3K0dEMkFZ - VzhnaU1GYVYvcHVkbFRZMzhoVkdQSTQKLS0tIG5jOTl0RDVaalozQnUyZDBYdk83 - dWJMVkFKbWFYVUM3bzQwU1NFbEI4bzAKKCsNzczSTCeeQXf6nWJMGQp0cKlz7S9/ - qtsRZw60oOBsT5FVXjkteih1JsiKMPZ1lQ/Pyo+m9HG1dHRDo26Clw== - -----END AGE ENCRYPTED FILE----- - recipient: age1sgnnkn67wpt4wgjsff4c36w3cmjkk557ng504e4aczx2lzls9y6ssy0x9r - encrypted_regex: ^(service\.webhook\.mattermost|githubAppPrivateKey|password|postgresPassword|ENCRYPTION_KEY|AUTH_SECRET|CLIENT_ID_GITHUB_LOGIN|CLIENT_SECRET_GITHUB_LOGIN|CF_DNS_API_TOKEN)$ - lastmodified: "2026-10-30T15:08:34Z" - mac: ENC[AES256_GCM,data:prnnSJpB8SQTP/U4slFI3PfBrbVI1EbEFErwoaaN+kqWhqZnbua7zTqAMFwzmygJ525Ku88sqi2Wt1k1RwPoaGFR1FMObgmAPw1UavZ6MFp5Cv2r7bnvhnRxQL12KTko79t+8I3mzVmOCu8QsQ1eQV/WXMxBMuigNsEGW+msjWQ=,iv:AjIP77ppC18nE5Nc4Apkr+EjBjvzMbV0kHAYOaI2YNM=,tag:ultvRWNfPiNRO3QcltR2yQ==,type:str] - mac_only_encrypted: true - version: 3.13.3 diff --git a/bootstrap/render.sh b/bootstrap/render.sh index c312b0c3..62b16ace 100755 --- a/bootstrap/render.sh +++ b/bootstrap/render.sh @@ -54,13 +54,12 @@ bver() { sed -n '/^# version: /{s/^# version: *//p;q;}' "$(vfile "$1")"; } CILIUM=$(bver cilium) CERTMGR=$(bver cert-manager) ARGOCD=$(bver argocd) -INFISICAL=$(bver infisical) LVM=$(bver lvm-localpv) -for v in "$TALOS" "$SCHEMATIC" "$K8S" "$GWAPI" "$SNAP" "$CILIUM" "$CERTMGR" "$ARGOCD" "$INFISICAL" "$LVM"; do +for v in "$TALOS" "$SCHEMATIC" "$K8S" "$GWAPI" "$SNAP" "$CILIUM" "$CERTMGR" "$ARGOCD" "$LVM"; do [ -n "$v" ] || { echo "ERROR: talos.sops.yaml / -values(.sops).yaml の # version: から版を読めなかった" >&2; exit 1; } done echo "talos=$TALOS k8s=$K8S gateway-api=$GWAPI external-snapshotter=$SNAP" -echo "cilium=$CILIUM cert-manager=$CERTMGR argo-cd=$ARGOCD infisical-standalone=$INFISICAL lvm-localpv=$LVM" +echo "cilium=$CILIUM cert-manager=$CERTMGR argo-cd=$ARGOCD lvm-localpv=$LVM" inline() { # $1=name stdin=manifest printf 'apiVersion: v1alpha1\nkind: KubeInlineManifestConfig\nname: %s\nmanifest: |-\n' "$1" @@ -116,12 +115,10 @@ decrypt() { # $1=name $2=env の上書き 'NR == 1 && $0 == "---" {next} !d && $0 == "---" {d=1; next} {print > (d ? s : v)}' "$WORK/$1-all.yaml" } decrypt argocd "${ARGOCD_VALUES:-}" -decrypt infisical "${INFISICAL_VALUES:-}" decrypt cert-manager "${CERT_MANAGER_VALUES:-}" # chart と repo は -values(.sops).yaml の `# renovate:` の行から読む。 # kube-system 以外は Namespace を先頭に付ける(chart は描かず、最初は誰も作らない)。 -# `updatedAt` は infisical の chart が付ける時刻(values で消せない)。残すと当て直すたびに再起動する。 chart() { # $1=name $2=namespace 残り=helm template に足す引数 _n=$1 _ns=$2 shift 2 @@ -140,7 +137,6 @@ chart() { # $1=name $2=namespace 残り=helm template に足す引数 fi helm template "$_n" "$_n/$_chart" \ --version "$(bver "$_n")" -n "$_ns" -f "$_v" "$@" --kube-version "$K8S" \ - | sed '/^ *updatedAt: "/d' \ | add_namespace "$_ns" } | inline "$_n" > "$WORK/inline-$_n.yaml" echo " $_n -> $(wc -c < "$WORK/inline-$_n.yaml") バイト" @@ -149,7 +145,7 @@ chart() { # $1=name $2=namespace 残り=helm template に足す引数 # `--api-versions` が無いと helm template は GatewayClass を黙って描かず、公開経路が全部死ぬ。 chart cilium kube-system --api-versions gateway.networking.k8s.io/v1/GatewayClass -# PVC の置き場。infisical の Postgres が要るので bootstrap 層に置く。chart(CSI ドライバ)と、 +# PVC の置き場。chart(CSI ドライバ)と、 # chart が描かないもの(StorageClass・VolumeSnapshotClass・thin プール)は storage.yaml。 chart lvm-localpv kube-system inline storage < bootstrap/storage.yaml > "$WORK/inline-storage.yaml" @@ -158,8 +154,6 @@ inline storage < bootstrap/storage.yaml > "$WORK/inline-storage.yaml" inline apiserver-rbac < bootstrap/apiserver-rbac.yaml > "$WORK/inline-apiserver-rbac.yaml" chart argocd argocd -# infisical は chart がパスワードを平文 env に焼くので ArgoCD に置けない(../docs/decisions.md)。 -chart infisical infisical # CRD は URL で渡す(理由は argocd-values.sops.yaml の `crds`)。版は chart の appVersion から取る。 ARGOCD_APP=$(helm show chart argocd/argo-cd --version "$ARGOCD" 2>/dev/null \ @@ -178,9 +172,8 @@ done chart cert-manager cert-manager -# ArgoCD が復号できない Secret(infisical の起動用と DNS-01 用)。-values.sops.yaml の 2 つめのドキュメント +# ArgoCD が復号できない Secret(DNS-01 用)。cert-manager-values.sops.yaml の 2 つめのドキュメント # (上の decrypt が分けたもの。無ければ `<` が失敗して止まる)。 -inline infisical-secrets < "$WORK/infisical-secret.yaml" > "$WORK/inline-infisical-secrets.yaml" inline cloudflare-secret < "$WORK/cert-manager-secret.yaml" > "$WORK/inline-cloudflare-secret.yaml" # クラスタの中で SopsSecret を復号する age の鍵(sops-secrets-operator が読む。Argo CD は復号できない)。CI は SOPS_AGE にダミーを渡す @@ -254,12 +247,10 @@ for n in "kube-apiserver:$K8S" "ghcr.io/siderolabs/kubelet:$K8S" \ "gateway-api/releases/download/$GWAPI/standard-install.yaml" \ 'name: argocd' argocd-server 'name: argocd-application-crd' manifests/crds/applicationset-crd.yaml \ 'kind: Namespace\nmetadata:\n name: argocd' \ - 'name: infisical' infisical-standalone 'metadata:\n namespace: infisical' \ - 'kind: Namespace\nmetadata:\n name: infisical' \ 'name: cert-manager' cert-manager-webhook 'name: cert-manager-crds' \ "cert-manager/releases/download/$CERTMGR/cert-manager.crds.yaml" \ 'kind: Namespace\nmetadata:\n name: cert-manager' \ - 'name: infisical-secrets' 'name: cloudflare-secret'; do + 'name: cloudflare-secret'; do grep -qF -- "$n" "$OUT/controlplane.yaml" || { echo "ERROR: '$n' が生成物に無い" >&2; exit 1; } done # 無いことを見る側(flannel はドキュメントが在るだけで有効になる) diff --git a/bootstrap/talos.md b/bootstrap/talos.md index 2d3b219e..07f2ca5a 100644 --- a/bootstrap/talos.md +++ b/bootstrap/talos.md @@ -23,7 +23,7 @@ SSH もシェルも無く gRPC API だけなので、**障害時は「直さず ## 組み立て **組み立ては [render.sh](render.sh) が全部やる。** 手で書くのは [talos.sops.yaml](talos.sops.yaml) の 1 本だけで、ほかから描いてくる部分 -(Cilium・cert-manager・argocd・infisical・lvm-localpv は `-values(.sops).yaml` から、StorageClass と thin プールは [storage.yaml](storage.yaml) から)と +(Cilium・cert-manager・argocd・lvm-localpv は `-values(.sops).yaml` から、StorageClass と thin プールは [storage.yaml](storage.yaml) から)と 1 か所で組む。**値をどこにも二度書かない**のが目的。CI([talos-boot](../.github/workflows/talos-boot.yml))も同じ `render.sh` を使う。 **描いたものは秘密を平文で含むので、手元に置かない。** [apply.sh](apply.sh) がコンテナの `/dev/shm` に描いて当て、終わったら消す。 @@ -32,7 +32,7 @@ SSH もシェルも無く gRPC API だけなので、**障害時は「直さず **Talos の秘密は 2 か所、どちらも SOPS でコミット済み。** [talos-secrets.sops.yaml](talos-secrets.sops.yaml) はクラスタの CA 一式で丸ごと、 [talos.sops.yaml](talos.sops.yaml) は `password` の値(下の fj.doany.io)**だけ**。**`talos-secrets.sops.yaml` を作り直すと別のクラスタになる** -(`apiserver-ca.crt` と Infisical の Kubernetes auth の CA も替わる)ので触らない。まっさらから組むときだけ +(`apiserver-ca.crt` も替わる)ので触らない。まっさらから組むときだけ `talosctl gen secrets -o bootstrap/talos-secrets.sops.yaml && sops -e -i bootstrap/talos-secrets.sops.yaml`。 **CI は PR ごとに使い捨ての秘密で `render.sh` を通し、QEMU で起動する**(talos-boot。`ENC[…]` は `ci-dummy` に置き換える)。 @@ -73,7 +73,7 @@ VM で確かめられないのは PT3 と bond の復旧方向。所要は apply ### machine config と inlineManifests -**Cilium・OpenEBS LVM・`apiserver-rbac.yaml`・ArgoCD・infisical・cert-manager と SOPS 済みの Secret 2 つは machine config の中にいる**ので、 +**Cilium・OpenEBS LVM・`apiserver-rbac.yaml`・ArgoCD・cert-manager と SOPS 済みの Secret 2 つは machine config の中にいる**ので、 直しただけでは何も起きない。`apply-config` は起動時にしか inlineManifests を読まず、**`upgrade-k8s`(同じ版でも)が SSA とインベントリで更新も削除もする**: ```shell diff --git a/bootstrap/talos.sops.yaml b/bootstrap/talos.sops.yaml index edc360fe..f7f5ced0 100644 --- a/bootstrap/talos.sops.yaml +++ b/bootstrap/talos.sops.yaml @@ -1,5 +1,5 @@ # ノード main の machine config のうち、手で書く部分の全部。render.sh が `gen config --config-patch` に渡す。 -# chart から描く部分(Cilium・ArgoCD・infisical・cert-manager・OpenEBS LVM)は render.sh が足す。版は下のコメント。 +# chart から描く部分(Cilium・ArgoCD・cert-manager・OpenEBS LVM)は render.sh が足す。版は下のコメント。 # # **SOPS で password の値だけ暗号化してある**(../.sops.yaml)。平文の部分はそのまま直してよい。 # パスワードを変えるときは `sops edit bootstrap/talos.sops.yaml`。render.sh が `sops -d` してから渡す。 diff --git a/compose.yaml b/compose.yaml index 46ff0678..0390b2ce 100644 --- a/compose.yaml +++ b/compose.yaml @@ -3,11 +3,10 @@ services: tools: build: tools image: gitops-tools:latest - # wslc は host ネットワークに対応していない。ブラウザのログインは t.ps1 が中継する + # wslc は host ネットワークに対応していない(ブラウザから戻ってくる方式のログインは使えない) working_dir: /repo environment: # ログインと鍵はリポジトリの .home/(.gitignore 済み) - HOME=/repo/.home - - INFISICAL_PROFILE volumes: - .:/repo diff --git a/deploy/backup/README.md b/deploy/backup/README.md index 142ef3e8..94aeb4cd 100644 --- a/deploy/backup/README.md +++ b/deploy/backup/README.md @@ -10,7 +10,7 @@ PVC のバックアップは VolSync([../volsync/application.yaml](../volsync/ap | [notify.yaml](notify.yaml) | 全部の ReplicationSource と etcd-snapshot が 26 時間以内に成功したかを、日次で通知の部屋に 1 行 | 21:00 | | [restore-check.yaml](restore-check.yaml) | 毎月 1 日、全部の PVC の最新を実際に取り出して開けるかを確かめる | 06:00(1 日) | -戻し方は [../../recovery/README.md](../../recovery/README.md) の 6(Infisical)と 8(ほかの PVC。populator で PVC を作ると埋まる)。 +戻し方は [../../recovery/README.md](../../recovery/README.md) の 6(populator で PVC を作ると埋まる)。 ## 秘密 diff --git a/deploy/cloudflare-ddns/application.yaml b/deploy/cloudflare-ddns/application.yaml index e2711a9e..188e2379 100644 --- a/deploy/cloudflare-ddns/application.yaml +++ b/deploy/cloudflare-ddns/application.yaml @@ -18,7 +18,7 @@ spec: targetRevision: 0.1.0 helm: valuesObject: - # API token は Infisical operator が作る Secret から(cloudflare-ddns-secrets.yaml)。 + # API token は sops-secrets-operator が作る Secret から(secrets.sops.yaml)。 # キーは CLOUDFLARE_API_TOKEN。 cloudflare: existingSecret: cloudflare-ddns-secrets diff --git a/deploy/external-dns/README.md b/deploy/external-dns/README.md index 05f1aa9a..1c1841cc 100644 --- a/deploy/external-dns/README.md +++ b/deploy/external-dns/README.md @@ -58,5 +58,5 @@ ExternalDNS は**自分が作ったレコードにしか触れない**。作っ ## 鍵 Cloudflare の Account API token「external-dns (doany.io DNS write)」。権限は **`doany.io` のゾーンだけの Zone Read + DNS Write**。 -Infisical `/external-dns/external-dns` の `cloudflare-api-token`。 +[secrets.sops.yaml](secrets.sops.yaml) の `external-dns` の `cloudflare-api-token`。 diff --git a/deploy/forgejo/README.md b/deploy/forgejo/README.md index b711d326..4a88eb3b 100644 --- a/deploy/forgejo/README.md +++ b/deploy/forgejo/README.md @@ -8,17 +8,16 @@ | [postgres.yaml](postgres.yaml) | DB | | [runner.yaml](runner.yaml) | Runner(v13)+ docker(dind)。`runs-on: ubuntu-latest` をそのまま拾う | | [httproute.yaml](httproute.yaml) | 公開経路。SSH は出さない(clone / push は HTTPS + トークン) | -| [forgejo-secrets.yaml](forgejo-secrets.yaml) | Infisical から Secret 3 つ | -| [argocd-creds.yaml](argocd-creds.yaml) | ArgoCD が Forgejo のリポジトリを読むための資格情報 | +| [secrets.sops.yaml](secrets.sops.yaml) | 秘密(SOPS。Secret `forgejo-db`・`forgejo-oauth`・`forgejo-runner` と VolSync の分) | バックアップは [volsync.yaml](volsync.yaml)(VolSync。PVC 2 本をスナップショットから、毎日 19:00 UTC)。 ## 使い始め -1. **Infisical に 2 つ入れる(同期より先に)** - - `/forgejo/forgejo-db`: `postgres-password` ── 英数字だけのランダム(`openssl rand -hex 32` など)。 +1. **[secrets.sops.yaml](secrets.sops.yaml) に 2 つ入れる(同期より先に。`tools/t sops edit`)** + - `forgejo-db`: `postgres-password` ── 英数字だけのランダム(`openssl rand -hex 32` など)。 postgres は最初の起動でしかパスワードを設定しないので、**後から変えるなら DB 側も変える** - - `/forgejo/forgejo-oauth`: `key` = `b0fa498f-7e6a-4fe1-a1c6-16fbbb6f397e`(Main のクライアント ID。秘密ではなく deploy/auth にも平文で書いてある)、`secret` = `${prod.auth.auth-secrets.oidc-client-secret}`(値は写さず参照) + - `forgejo-oauth`: `key` = `b0fa498f-7e6a-4fe1-a1c6-16fbbb6f397e`(Main のクライアント ID。秘密ではなく deploy/auth にも平文で書いてある)、`secret` = Main のクライアントシークレット(`deploy/auth/secrets.sops.yaml` の `oidc-client-secret` と同じ値) 2. Entra のアプリ登録 Main のリダイレクト URI に `https://fj.doany.io/user/oauth2/entra/callback`([pulumi/entra.ts](../../pulumi/entra.ts) にある) 3. main にマージ → ArgoCD が同期 4. **管理者を作る**(1 度だけ)。Entra のメールと同じにしておくと、Entra でログインしたときに自動で紐づいて管理者になる。 @@ -30,7 +29,7 @@ ``` **members を作る前にログインした人は、次のログインで入る** -5. **Runner の秘密を入れる**: Infisical `/forgejo/forgejo-runner` に `runner-secret` = 16 進 40 文字のランダム。 +5. **Runner の秘密を入れる**: secrets.sops.yaml の `forgejo-runner` に `runner-secret` = 16 進 40 文字のランダム。 値は画面に出さずに作って貼る(WSL なら `openssl rand -hex 20 | tr -d '\n' | clip.exe`。貼ったらクリップボードは消す)。 管理画面での操作は要らない。Forgejo と Runner の Pod が入れ替わり、Forgejo の init が同じ秘密で Runner を登録し ([forgejo.yaml](forgejo.yaml) の setup.sh の 5)、Runner は同じ秘密から uuid / token を出して繋ぐ([runner.yaml](runner.yaml))。 @@ -44,7 +43,7 @@ **Forgejo が唯一の置き場になる。** バックアップは VolSync(R2)の 1 日 1 回だけなので、手元の clone も捨てないこと。 -ArgoCD が読むトークンは Infisical `/argocd/forgejo-repo-creds`([argocd-creds.yaml](argocd-creds.yaml))。 +ArgoCD が読むトークンは [../argocd/secrets.sops.yaml](../argocd/secrets.sops.yaml) の `forgejo-repo-creds`。 **スコープは repository・organization・issue の読み取り。** issue が無いと ApplicationSet が `token does not have at least one of required scope(s): [read:issue]` で止まる。 ApplicationSet は GitHub 側と別の `repos-forgejo`(同居させると、Forgejo が空のクラスタで GitHub 側まで 1 本も作られない)。 @@ -54,7 +53,7 @@ ApplicationSet は GitHub 側と別の `repos-forgejo`(同居させると、Forg 1. Forgejo の「新しい移行」で組織 `doa` の下に取り込む(Issue / PR / リリースも)。非公開のまま入り、チーム members が読める 2. ワークフローを Forgejo 向けに直す(`.github/workflows/` のままで読まれる) - イメージは `ghcr.io/danything/` → `fj.doany.io/doa/`。push はワークフローの `secrets.GITHUB_TOKEN`(Forgejo のトークン)で通る - - クラスタが pull できるように、アプリの namespace に `imagePullSecrets` を足す(Forgejo の `read:package` トークンを Infisical から `kubernetes.io/dockerconfigjson` で) + - クラスタが pull できるように、アプリの namespace に `imagePullSecrets` を足す(Forgejo の `read:package` トークンをアプリの secrets.sops.yaml から `kubernetes.io/dockerconfigjson` で) - claude-review(GitHub App)のワークフローは消す。Forgejo では動かない 3. Forgejo で CI とイメージの push が通るのを確かめる 4. **GitHub のリポジトリを消す**。両方に `deploy/argocd.yaml` があると Application 名がぶつかる diff --git a/deploy/forgejo/forgejo.yaml b/deploy/forgejo/forgejo.yaml index 47574782..f074aaf5 100644 --- a/deploy/forgejo/forgejo.yaml +++ b/deploy/forgejo/forgejo.yaml @@ -97,7 +97,7 @@ data: gitea forgejo-cli actions register --secret "$(cat "$s")" --name forgejo-runner --keep-labels >/dev/null || echo "Runner の登録に失敗した。Forgejo はこのまま上げる" else - echo "Infisical の /forgejo/forgejo-runner に runner-secret が無いので Runner の登録は飛ばす" + echo "Secret forgejo-runner に runner-secret が無いので Runner の登録は飛ばす" fi start.sh: | #!/bin/sh @@ -206,8 +206,7 @@ spec: name: forgejo-setup - name: tmp emptyDir: {} - # Infisical の operator はボリュームの Secret も見るので、変わると Pod が入れ替わって登録し直す。 - # 環境変数にしないのは、operator が init の env を見ないため + # Reloader はボリュームの Secret も見るので、変わると Pod が入れ替わって登録し直す。 - name: runner-secret secret: secretName: forgejo-runner diff --git a/deploy/forgejo/runner.yaml b/deploy/forgejo/runner.yaml index 0c09255c..5a4dfd69 100644 --- a/deploy/forgejo/runner.yaml +++ b/deploy/forgejo/runner.yaml @@ -6,7 +6,7 @@ # - Runner 側: 下の start.sh が同じ秘密から uuid / token を出す。uuid は秘密の先頭 16 文字のバイト列を # そのまま UUID にしたもの、token は秘密そのもの(Forgejo の models/actions/forgejo.go の RegisterRunner、 # runner の create-runner-file.go の uuidFromSecret と同じ。create-runner-file は v13 で非推奨) -# 秘密は Infisical の /forgejo/forgejo-runner に runner-secret で入れる(forgejo-secrets.yaml)。 +# 秘密は secrets.sops.yaml の forgejo-runner に runner-secret で入れる。 # 入るまで runner コンテナは「待っている」とログに出して止まっている(落ちない)。 # # 設定ファイルは起動のたびに作る。Pod は状態を持たない(.runner ファイルも PVC も要らない)。 @@ -43,7 +43,7 @@ data: #!/bin/sh set -eu while [ -z "${RUNNER_SECRET:-}" ]; do - echo "Infisical の /forgejo/forgejo-runner に runner-secret が入るのを待っている" + echo "Secret forgejo-runner に runner-secret が入るのを待っている" sleep 60 done # 末尾の改行は Forgejo 側($(cat))と同じく落とす。形が違うと Forgejo 側の登録も失敗する。 diff --git a/deploy/headlamp/application.yaml b/deploy/headlamp/application.yaml index e819314a..f13dd02b 100644 --- a/deploy/headlamp/application.yaml +++ b/deploy/headlamp/application.yaml @@ -42,7 +42,7 @@ spec: inCluster: true inClusterContextName: main oidc: - # 値は headlamp-secrets.yaml が作る Secret から envFrom で入る。 + # 値は secrets.sops.yaml から sops-secrets-operator が作る Secret から envFrom で入る。 # chart 側で作らせないこと。 secret: create: false diff --git a/deploy/infisical/httproute.yaml b/deploy/infisical/httproute.yaml deleted file mode 100644 index 219b843d..00000000 --- a/deploy/infisical/httproute.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# 公開経路。証明書は Gateway(deploy/gateway/)のリスナーに付いているので指定しない。 -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: infisical - namespace: infisical -spec: - parentRefs: - - name: doany - namespace: kube-system - sectionName: https - hostnames: ["il.doany.io"] - rules: - - matches: - - path: - type: PathPrefix - value: / - backendRefs: - - name: infisical - port: 8080 diff --git a/deploy/infisical/operator-rbac.yaml b/deploy/infisical/operator-rbac.yaml deleted file mode 100644 index 6ccad1dd..00000000 --- a/deploy/infisical/operator-rbac.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# InfisicalSecret が Infisical にログインするための SA。operator がこの SA の -# トークンを作って提示し、Infisical が TokenReview で本物か確かめる -# (長命の資格情報はどこにも無い)。マシンアイデンティティ側の許可リストは -# (SA: infisical-auth, namespace: infisical)。 -apiVersion: v1 -kind: ServiceAccount -metadata: - name: infisical-auth - namespace: infisical ---- -# TokenReview は提示されたトークン自身の権限で呼ばれるので system:auth-delegator が要る -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: infisical-auth-delegator -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: system:auth-delegator -subjects: - - kind: ServiceAccount - name: infisical-auth - namespace: infisical diff --git a/deploy/infisical/operator.yaml b/deploy/infisical/operator.yaml deleted file mode 100644 index 6a9f08d3..00000000 --- a/deploy/infisical/operator.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# Infisical 純正 Kubernetes operator。InfisicalSecret CR を見て Infisical から値を取り、 -# 普通の Secret を作る。`secrets.infisical.com/auto-reload: "true"` 注釈のある Deployment は -# Secret 更新時に自動でローリング再起動される。 -# https://infisical.com/docs/integrations/platforms/kubernetes/overview -# -# リリース名は変えられない。chart は名前を 15 文字で切って -# `infisical-secre-controller-manager` を作るので、変えると別物として作り直しになる。 -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: infisical-operator - namespace: argocd - finalizers: - - resources-finalizer.argocd.argoproj.io -spec: - project: default - destination: - server: https://kubernetes.default.svc - namespace: infisical-operator - source: - repoURL: https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/ - chart: secrets-operator - targetRevision: v0.11.8 - helm: - releaseName: infisical-secrets-operator - syncPolicy: - automated: - prune: true - selfHeal: true - # CRD より先に同期しにいくと既定の 5 回で諦める。15s から倍々・上限 5 分・10 回(約 33 分)で自力で戻る。 - retry: - limit: 10 - backoff: - duration: 15s - factor: 2 - maxDuration: 5m - syncOptions: - - CreateNamespace=true - # CRD が 7 本入っていて、client-side apply だと last-applied 注釈が - # メタデータの上限に当たる。 - - ServerSideApply=true diff --git a/deploy/infisical/push-bridge.yaml b/deploy/infisical/push-bridge.yaml deleted file mode 100644 index ed54a942..00000000 --- a/deploy/infisical/push-bridge.yaml +++ /dev/null @@ -1,49 +0,0 @@ -# Infisical の Webhook を即時リコンサイルに変えるブリッジ(自作・公開)。 -# https://github.com/danything/infisical-push-bridge -# 無料版は operator のポーリング同期しか無いので、保存 → 数秒反映はこれが担う。 -# 落ちていても operator の resyncInterval に落ちるだけで壊れない。 -# -# セットアップ: Infisical のフォルダ /infisical-push-bridge/infisical-push-bridge に -# `webhook-secret` を置き、プロジェクトの Project Settings → Webhooks で -# URL http://infisical-push-bridge.infisical-push-bridge.svc.cluster.local、 -# Secret Path は必ず `/`(既定の `/` はルートにしかマッチしない)、 -# 同じ secret key の Webhook を作る(README 参照)。 -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: infisical-push-bridge - namespace: argocd - finalizers: - - resources-finalizer.argocd.argoproj.io -spec: - project: default - destination: - server: https://kubernetes.default.svc - namespace: infisical-push-bridge - source: - repoURL: ghcr.io/danything/charts - chart: infisical-push-bridge - targetRevision: 0.1.38 - helm: - valuesObject: - infisicalSecret: - enabled: true - hostAPI: http://infisical.infisical.svc:8080/api - identityId: f2bc3416-f196-4626-9bf3-c11693b32cc6 - serviceAccountName: infisical-auth - serviceAccountNamespace: infisical - projectSlug: doa - secretsPath: /infisical-push-bridge/infisical-push-bridge - syncPolicy: - automated: - prune: true - selfHeal: true - # CRD より先に同期しにいくと既定の 5 回で諦める。15s から倍々・上限 5 分・10 回(約 33 分)で自力で戻る。 - retry: - limit: 10 - backoff: - duration: 15s - factor: 2 - maxDuration: 5m - syncOptions: - - CreateNamespace=true diff --git a/deploy/infisical/smtp-secret.yaml b/deploy/infisical/smtp-secret.yaml deleted file mode 100644 index 4568703a..00000000 --- a/deploy/infisical/smtp-secret.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# Infisical の /infisical/infisical-smtp を operator が Secret にする(UI で書き換えれば数秒で同期)。 -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisical-smtp - namespace: infisical - annotations: - # bootstrap/README.md「`InfisicalSecret` には sync-options を 2 つ付けること」 -spec: - hostAPI: http://infisical.infisical.svc:8080/api - authentication: - kubernetesAuth: - identityId: f2bc3416-f196-4626-9bf3-c11693b32cc6 - autoCreateServiceAccountToken: true - serviceAccountRef: - name: infisical-auth - namespace: infisical - secretsScope: - projectSlug: doa - envSlug: prod - secretsPath: /infisical/infisical-smtp - managedKubeSecretReferences: - - secretName: infisical-smtp - secretNamespace: infisical diff --git a/deploy/infisical/volsync.yaml b/deploy/infisical/volsync.yaml deleted file mode 100644 index cff6e8ab..00000000 --- a/deploy/infisical/volsync.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# この namespace の PVC を VolSync で R2 に取る(リポジトリは PVC ごとに `/volsync/infisical/`)。 -# 取り方・時刻・Secret の組み方は ../volsync/application.yaml。アプリと一緒に置くので、アプリを消せばこれも消える -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: volsync - namespace: infisical -spec: - hostAPI: http://infisical.infisical.svc:8080/api - authentication: - kubernetesAuth: - identityId: f2bc3416-f196-4626-9bf3-c11693b32cc6 - autoCreateServiceAccountToken: true - serviceAccountRef: - name: infisical-auth - namespace: infisical - secretsScope: - projectSlug: doa - envSlug: prod - secretsPath: /backup/backup - managedKubeSecretReferences: - - secretName: volsync-data-postgresql-0 - secretNamespace: infisical - template: - data: - RESTIC_REPOSITORY: "s3:{{ .endpoint.Value }}/{{ .bucket.Value }}/volsync/infisical/data-postgresql-0" - RESTIC_PASSWORD: "{{ .repoPassword.Value }}" - AWS_ACCESS_KEY_ID: "{{ .accessKeyId.Value }}" - AWS_SECRET_ACCESS_KEY: "{{ .secretAccessKey.Value }}" - AWS_DEFAULT_REGION: auto ---- -apiVersion: volsync.backube/v1alpha1 -kind: ReplicationSource -metadata: - name: data-postgresql-0 - namespace: infisical -spec: - sourcePVC: data-postgresql-0 - trigger: - schedule: "0 19 * * *" - restic: - repository: volsync-data-postgresql-0 - copyMethod: Snapshot - retain: {daily: 7, weekly: 4, monthly: 6} - moverSecurityContext: {runAsUser: 0, runAsGroup: 0} diff --git a/deploy/matrix/README.md b/deploy/matrix/README.md index f5be32c6..e8d687ae 100644 --- a/deploy/matrix/README.md +++ b/deploy/matrix/README.md @@ -50,24 +50,23 @@ Entra だけ(Tuwunel の `[[global.identity_provider]]`)。パスワードのロ - **暗号化していない。** hookshot は暗号化したルームに書けない。暗号化を後から入れると戻せないので、入れないこと (Element は非公開のルームを既定で暗号化するので、作るときに切る) - webhook は [hookshot.yaml](hookshot.yaml) の `connections`。URL は `https://m.doany.io/webhook/` で、ID は - Infisical `/matrix/matrix` の `hook-<部屋>`(公開のリポジトリなので git に置かない) + [secrets.sops.yaml](secrets.sops.yaml) の `matrix` の `hook-<部屋>`(公開のリポジトリなので平文では置かない) - **投稿は全部 m.notice(bot の発言)**。通知と音は Element の設定で決める: グループの会話は「音あり」、bot のメッセージは「オン」(音無しで通知)、要らない部屋は部屋ごとに「メンションのみ」。 | ルーム | 送り元 | URL の在処 | | --- | --- | --- | -| `server` | バックアップの通知([../backup/notify.yaml](../backup/notify.yaml)) | Infisical `/backup/backup` の `mattermostWebhook`(名前は据え置き) | -| 〃 | Infisical(秘密の変更) | Infisical のプロジェクトの webhook(Project Settings → Webhooks。環境 prod、パス `/`) | +| `server` | バックアップの通知([../backup/notify.yaml](../backup/notify.yaml)) | [../backup/secrets.sops.yaml](../backup/secrets.sops.yaml) の `mattermostWebhook`(名前は据え置き) | | 〃 | Cloudflare(証明書・DDoS・オリジン不達など) | Cloudflare の通知の webhook の送り先([../../pulumi/notifications.ts](../../pulumi/notifications.ts)) | | `argocd` | Argo CD | `bootstrap/argocd-values.sops.yaml`(SOPS)の `service.webhook.mattermost` | -| `ashi` | ashi | Infisical `/ashi/ashi-secrets` の `notify-webhook-url` | -| `todoroku` | todoroku | Infisical `/todoroku/todoroku-secrets` の `mattermost-webhook-url` | -| `worklog` | worklog | Infisical `/worklog/worklog-secrets` の `mattermost-webhook-url` | +| `ashi` | ashi | ashi のリポジトリの `deploy/secrets.sops.yaml`(`ashi-secrets` の `notify-webhook-url`) | +| `todoroku` | todoroku | todoroku のリポジトリの `deploy/secrets.sops.yaml`(`todoroku-secrets` の `mattermost-webhook-url`) | +| `worklog` | worklog | worklog のリポジトリの `deploy/secrets.sops.yaml`(`worklog-secrets` の `mattermost-webhook-url`) | | `denpa` | denpa | denpa の DB(`webhooks` テーブル。画面の設定) | | `forgejo` | Forgejo | Forgejo の組織 `doa` の webhook(種類は Slack。DB の `webhook` id 2) | -| `contact` | doany.io のお問い合わせフォーム(danything/corp) | Infisical `/corp/corp-secrets` の `contact-webhook-url` | +| `contact` | doany.io のお問い合わせフォーム(danything/corp) | corp のリポジトリの `deploy/secrets.sops.yaml`(`corp-secrets` の `contact-webhook-url`) | -足すとき:ルームを暗号化なしで作り、hookshot を招待して権限を上げる → Infisical に `hook-<部屋>` を足す → +足すとき:ルームを暗号化なしで作り、hookshot を招待して権限を上げる → secrets.sops.yaml に `hook-<部屋>` を足す(`sops edit`) → `connections` と render の env に足す。 ## GitHub 連携(hookshot) @@ -118,7 +117,7 @@ NAT ループバック(ヘアピン)が要る。家の中だけつながらな ### 秘密 -Infisical `/matrix/matrix` の `livekit-secret`。API キーの名前は `matrix`(秘密ではない)。 +[secrets.sops.yaml](secrets.sops.yaml) の `matrix` の `livekit-secret`。API キーの名前は `matrix`(秘密ではない)。 ### やっていないこと diff --git a/deploy/matrix/hookshot.yaml b/deploy/matrix/hookshot.yaml index 5c56ca59..c9657adf 100644 --- a/deploy/matrix/hookshot.yaml +++ b/deploy/matrix/hookshot.yaml @@ -56,7 +56,7 @@ data: - service: "*" level: admin # 通知のルームごとの webhook。 - # stateKey は推測されない乱数で、Infisical /matrix/matrix の hook-<部屋>(公開のリポジトリなので git に置かない)。 + # stateKey は推測されない乱数で、secrets.sops.yaml の matrix の hook-<部屋>(公開のリポジトリなので平文では置かない)。 # 下の render が HOOK_ で始まる変数(stateKey)を差し替える。 # # 投稿は全部 m.notice(bot の発言)。スマホに通知するかと音は、Element の「bot のメッセージ」の設定と @@ -134,28 +134,6 @@ data: state: name: contact transformationFunction: *slack - # 部屋 server: Infisical の秘密の変更(プロジェクトの webhook。種類は general) - - connectionType: uk.half-shot.matrix-hookshot.generic.hook - stateKey: ${HOOK_INFISICAL} - roomId: "!ieWtDZbr64u72aU4QqKfmZlmHSVvh8HEgR2zF9zhtxw" - state: - name: infisical - transformationFunction: | - // Infisical の webhook(種類は general)。どの環境・フォルダの秘密が、誰によって変わったかを 1 行に - const p = data.project ?? {}; - const what = { - "secrets.modified": "🔑 秘密が変わった", - "secrets.rotation-failed": "🚨 秘密のローテーションに失敗", - "secrets.change-request.modified": "📝 変更の申請が動いた", - "secrets.access-request.modified": "📝 アクセスの申請が動いた", - "honey-token.triggered": "🚨 ハニートークンが使われた", - }[data.event] ?? `Infisical: ${data.event ?? "?"}`; - const where = [p.environment, p.secretPath].filter(Boolean).join(" "); - const who = [p.changedByActorType, p.changedBy].filter(Boolean).join(" "); - const by = who ? `(${who})` : ""; - result = data.event - ? { version: "v2", plain: `${what}: \`${where}\`${by}`, msgtype: "m.notice" } - : { version: "v2", empty: true }; # 部屋 server: Cloudflare の通知(証明書・DDoS・オリジン不達など。pulumi/notifications.ts)。本文は text に来る - connectionType: uk.half-shot.matrix-hookshot.generic.hook stateKey: ${HOOK_CLOUDFLARE} @@ -235,8 +213,6 @@ spec: valueFrom: {secretKeyRef: {name: matrix, key: hookshot-hs-token}} - name: PASSKEY_B64 valueFrom: {secretKeyRef: {name: matrix, key: hookshot-passkey-b64}} - - name: HOOK_INFISICAL - valueFrom: {secretKeyRef: {name: matrix, key: hook-infisical}} - name: HOOK_CLOUDFLARE valueFrom: {secretKeyRef: {name: matrix, key: hook-cloudflare}} - name: HOOK_SERVER diff --git a/deploy/matrix/secrets.sops.yaml b/deploy/matrix/secrets.sops.yaml index eb3319ec..57e3f146 100644 --- a/deploy/matrix/secrets.sops.yaml +++ b/deploy/matrix/secrets.sops.yaml @@ -23,7 +23,6 @@ spec: hook-contact: ENC[AES256_GCM,data:yKDtxoeU3rI7r0IVr6hA3Wz8l3Evyd3QdwgjZj4Spo3cKMYj4FkXJlHvUyf6X0dR,iv:GZL/f5vd9s3w/lCNQcHFJa7MsbUUgue4885zBBrVe7o=,tag:gE+ftwBS4tLAFn6OvGHpgQ==,type:str] hook-denpa: ENC[AES256_GCM,data:fc1dRNG4eQPjhvK2p06fNT6eyFvHj5sxeywePNIwmPZWR7AjNpwV7DVvCNWxD0TY,iv:PZU/jZnyb65fVQnVZmTKtxcZ6zd3zHXoQ4D8oGGdT+g=,tag:SHpZk2VLhTTeUPpWYVExAA==,type:str] hook-forgejo: ENC[AES256_GCM,data:lBa0/9wmzkfUb+iSWu98dZwco651QhsjD6C86Xw3QLRVOHxFPA70Tpe92pm24d3s,iv:wGFlhHA5T/px2mEgU8HFQcLI/PJ3pNI3SWLogNdJhz4=,tag:39uLuwGPeNXH49FQpmbFaw==,type:str] - hook-infisical: ENC[AES256_GCM,data:4m3EODaGOZuSeNqLqJfAtRGaAhUTQIdgM0CKWwFWg43cysphVE1E9+lYWdF2GJIs,iv:5NKyULEGteZmC0H9/aIXrhDIrltd4p6plIh3uxNCpVY=,tag:YaB6r7mU46N6SNc5EhWNNA==,type:str] hook-server: ENC[AES256_GCM,data:GFXv4n1A7CujipdIlCWNNcIG2FWOGC1lFkhH1hLQt58YwLW+hPpR12H9ahFELNDn,iv:wSK/q4Cu/uoW91P2ScFR9pXVsHHAHmak75zCPBMaf4Y=,tag:0HZUTVIOgoeH8Oi6iIIBzg==,type:str] hook-todoroku: ENC[AES256_GCM,data:/t935bBlWPgAxZZeQ1km0M/D/1nCRhqndUy7N76QccloQIY6dvroQiVY5XE9jHhu,iv:lFXUD3uUR/+PDrBfviQuigF+b5TI6QqOzS/TwcJFWq4=,tag:ZIzYECtkA2b+1CeZjYBk/g==,type:str] hook-worklog: ENC[AES256_GCM,data:A1+BgRJKs/KRwDnPvO3MQ3KyU5Dk3tPWoqZDfUiNeha5MF+R9GtfjiExQmItOfus,iv:DwSRBzQOZttSPUSGBJzuS+TTyRJl2nuHD+xbIfWa1rs=,tag:eFDVAkqMVExgZLTchq2Ulg==,type:str] @@ -60,7 +59,7 @@ sops: -----END AGE ENCRYPTED FILE----- recipient: age1z36xmmvjmwg4sus6k8nx6wgp8qyew7tj76077rvv5sagcgudd4tszrmwml encrypted_regex: ^(data|stringData)$ - lastmodified: "2026-11-02T06:02:10Z" - mac: ENC[AES256_GCM,data:dbq34fQLrLaTlSiqDchFuGANS6FmmoaHaSmMbOzRqchewybjb/4igAUXThIzgdZ2xkN1w35CMwHLnAr5fwruiDx48mqej3AgJEdsCae9aLFhmXrZAQFeakA9Bd5PWSMF1Vm0UnF60PVjisYfANbv8rnyeVPxTXi62EOtrk0Xblg=,iv:wsyeNFusodqnsvKWtwP30+E0/ATKQe/TcKlczfoFEpA=,tag:8FrHWmLjjgXEtiPLvdY93Q==,type:str] + lastmodified: "2026-11-02T07:27:03Z" + mac: ENC[AES256_GCM,data:JgaGj5qMWeQqG63uk2gjzBBS9yrzBTf0nF1R6s5xpjGb/go53Zx4q++XX4sOZKHvH3ow1/Wr2mRnl1iySH6yNJczqwQkdqfML0WGTdtY5mAJCXEsuOA/6lqmSc+1yJx/BGmCxHt933JyRJjlx0Tdv4AgprNv2UUlshPUdYz7T4I=,iv:aEljV22VyF2C5wDstLCb1AHDFz/uZfOcW7AgGinJuzc=,tag:KyMKyWMOkXnFRzIByzQt1w==,type:str] mac_only_encrypted: true version: 3.13.3 diff --git a/deploy/renovate/README.md b/deploy/renovate/README.md index 818e2f6d..1730444d 100644 --- a/deploy/renovate/README.md +++ b/deploy/renovate/README.md @@ -19,7 +19,7 @@ tools/t.ps1 --admin kubectl -n renovate create job --from=cronjob/renovate renov ## 秘密 -Infisical の `/renovate/renovate-secrets`。**無いあいだは、それを使う Pod は何もせずに終わる。** +[secrets.sops.yaml](secrets.sops.yaml) の `renovate-secrets`(`tools/t sops edit`)。**無いあいだは、それを使う Pod は何もせずに終わる。** | キー | 中身 | 使う Pod | | --- | --- | --- | diff --git a/deploy/renovate/renovate.yaml b/deploy/renovate/renovate.yaml index 41a3972c..a05de314 100644 --- a/deploy/renovate/renovate.yaml +++ b/deploy/renovate/renovate.yaml @@ -37,7 +37,7 @@ data: export RENOVATE_GIT_AUTHOR='Renovate Bot ' # Forgejo のユーザー renovate のトークン(チーム bots = 全リポジトリに書き込み) if [ -z "${FORGEJO_TOKEN:-}" ]; then - echo "renovate-secrets に forgejo-token が無い。Infisical に入れるまで何もしない(README.md)" >&2 + echo "renovate-secrets に forgejo-token が無い。secrets.sops.yaml に入れるまで何もしない(README.md)" >&2 exit 0 fi RENOVATE_TOKEN=$FORGEJO_TOKEN @@ -61,7 +61,7 @@ data: # github.com のトークンは Forgejo のときだけ使う(GitHub ではトークンそのものが github.com のもの) unset RENOVATE_GITHUB_COM_TOKEN if [ -z "${APP_PRIVATE_KEY:-}" ]; then - echo "renovate-secrets に github-app-private-key が無い。Infisical に入れるまで何もしない(README.md)" >&2 + echo "renovate-secrets に github-app-private-key が無い。secrets.sops.yaml に入れるまで何もしない(README.md)" >&2 exit 0 fi if ! RENOVATE_TOKEN=$(node /scripts/github-app-token.mjs); then diff --git a/deploy/rybbit/README.md b/deploy/rybbit/README.md index acfb466f..5dc07d35 100644 --- a/deploy/rybbit/README.md +++ b/deploy/rybbit/README.md @@ -12,14 +12,14 @@ | [clickhouse.yaml](clickhouse.yaml) | イベントの倉庫。上流の設定 4 本を ConfigMap に、メモリの上限だけこの箱向けに下げた(limit 3 Gi、1 クエリ 2 GB) | | [postgres.yaml](postgres.yaml) | ユーザー・サイト・設定。forgejo と同じ 17 系 | | [redis.yaml](redis.yaml) | セッション追跡のカウンタ。PVC は持たない | -| [rybbit-secrets.yaml](rybbit-secrets.yaml) | Infisical `/rybbit/rybbit` の 4 キー → Secret `rybbit` | +| [secrets.sops.yaml](secrets.sops.yaml) | 秘密(SOPS)。Secret `rybbit` の 4 キーと VolSync の分 | | [httproute.yaml](httproute.yaml) | `rt.doany.io`。`/api` は backend、ほかは client。上流の Caddyfile にある `/.well-known/oauth-*`(MCP 向け)は使わないので置かない | バックアップは [volsync.yaml](volsync.yaml)(VolSync。ClickHouse と PostgreSQL の PVC をスナップショットから)。 ## 使い始め -1. Infisical(il.doany.io)に フォルダ `/rybbit/rybbit` を作り、`clickhouse-password` / `postgres-password` / +1. [secrets.sops.yaml](secrets.sops.yaml) の Secret `rybbit` に `clickhouse-password` / `postgres-password` / `redis-password` / `better-auth-secret` を英数字のランダム(32 文字くらい)で入れる。 入れるまで Pod は `CreateContainerConfigError` で待つ(壊れてはいない) 2. を開き、最初のアカウントを作る(これが管理者) diff --git a/deploy/smtp-relay/README.md b/deploy/smtp-relay/README.md index 1666a27a..7385d6de 100644 --- a/deploy/smtp-relay/README.md +++ b/deploy/smtp-relay/README.md @@ -7,13 +7,13 @@ Exchange Online の SMTP AUTH の Basic 認証(ユーザー名とパスワード)は、2026-12 末に既定で無効、2027 年後半に廃止日が発表される。 `info@doany.io` は **Exchange Online Kiosk** で、Kiosk は SMTP の OAuth にも対応していないとされるので、 -SMTP AUTH をやめて **Graph の `sendMail`** で送る。Infisical のように OAuth を話せないアプリもあるので、アプリごとに直さず中継を 1 つ置く。 +SMTP AUTH をやめて **Graph の `sendMail`** で送る。OAuth を話せないアプリもある(当時の Infisical など)ので、アプリごとに直さず中継を 1 つ置く。 ## 仕組み - 中継は**秘密を持たない**。アプリが SMTP AUTH で渡す - ユーザー名: `22ac977d-b140-4dc8-8150-90d645d3475b@<クライアント ID>`(テナント ID @ Entra のアプリのクライアント ID) - - パスワード: そのアプリのクライアントシークレット(Infisical) + - パスワード: そのアプリのクライアントシークレット(送る側の SopsSecret) で中継がトークンを取り、**差出人(From)のメールボックス**から `sendMail` する - Entra のアプリ「smtp-relay」は Graph の `Mail.Send` をテナント全体には持たない。**Exchange の RBAC for Applications で、 @@ -26,12 +26,12 @@ SMTP AUTH をやめて **Graph の `sendMail`** で送る。Infisical のよう | | 値 | | --- | --- | | ホスト・ポート | `smtp-relay.smtp-relay.svc` / `587` | -| TLS | なし(STARTTLS もしない。Infisical は `SMTP_REQUIRE_TLS=false`、todoroku は `SMTP_STARTTLS=false`) | +| TLS | なし(STARTTLS もしない。todoroku は `SMTP_STARTTLS=false`) | | ユーザー名 | `22ac977d-b140-4dc8-8150-90d645d3475b@fd45a316-97cc-4480-8da8-487109403520` | -| パスワード | Infisical の `/infisical/infisical-smtp` の `SMTP_PASSWORD`(Infisical と todoroku が使う)、`/yosegaki/yosegaki-secrets` の `smtp-password` | +| パスワード | 送る側のリポジトリの `deploy/secrets.sops.yaml`(todoroku は `todoroku-smtp` の `SMTP_PASSWORD`、yosegaki は `yosegaki-secrets` の `smtp-password`) | | 差出人 | `info@doany.io`(ほかのアドレスは Exchange が拒む) | -使っているのは Infisical(`bootstrap/infisical-values.sops.yaml`)、yosegaki(blog のリポジトリ)、todoroku(Forgejo の doa/todoroku)。 +使っているのは yosegaki(blog のリポジトリ)と todoroku(Forgejo の doa/todoroku)。Infisical も使っていた(2026-11-02 にやめた)。 **送る側を足すときは network-policy.yaml にその namespace を足す。** ## Entra のアプリと Exchange の絞り込み(1 回だけ。済み) @@ -51,7 +51,7 @@ SMTP AUTH をやめて **Graph の `sendMail`** で送る。Infisical のよう Test-ServicePrincipalAuthorization -Identity fd45a316-97cc-4480-8da8-487109403520 -Resource info@doany.io # InScope が True ``` -3. **クライアントシークレットは Entra の画面で作り**(期限 24 か月)、値を Infisical の上の 2 か所に入れる。あとから取り出せない。 +3. **クライアントシークレットは Entra の画面で作り**(期限 24 か月)、値を上の 2 か所(送る側の secrets.sops.yaml)に入れる。あとから取り出せない。 期限は `pulumi.yml` の expiry が他のアプリ登録と一緒に知らせる。作り直したら 2 か所を書き換え、送る側を rollout restart する ## 確かめ方 diff --git a/deploy/smtp-relay/network-policy.yaml b/deploy/smtp-relay/network-policy.yaml index bfdbe902..74605483 100644 --- a/deploy/smtp-relay/network-policy.yaml +++ b/deploy/smtp-relay/network-policy.yaml @@ -11,8 +11,6 @@ spec: app: smtp-relay ingress: - fromEndpoints: - - matchLabels: - k8s:io.kubernetes.pod.namespace: infisical - matchLabels: k8s:io.kubernetes.pod.namespace: blog - matchLabels: diff --git a/deploy/sops-secrets-operator/application.yaml b/deploy/sops-secrets-operator/application.yaml index 957e36bc..4b8a70df 100644 --- a/deploy/sops-secrets-operator/application.yaml +++ b/deploy/sops-secrets-operator/application.yaml @@ -22,7 +22,7 @@ spec: targetRevision: 0.28.1 helm: valuesObject: - # 今ある Secret(Infisical の operator が作ったもの)も引き取る。復元したあとに持ち主の印が古いまま残ったものも + # 今ある Secret(移したときは Infisical の operator が作ったもの)も引き取る。復元したあとに持ち主の印が古いまま残ったものも defaultEnforceOwnership: true secretsAsFiles: - name: sops-age-key-file diff --git a/deploy/volsync/application.yaml b/deploy/volsync/application.yaml index c6158dc9..36e5dea2 100644 --- a/deploy/volsync/application.yaml +++ b/deploy/volsync/application.yaml @@ -13,7 +13,7 @@ # dataSourceRef は作るときしか書けないので、Argo CD は今ある PVC の dataSourceRef の差分を見ない(bootstrap/argocd-repos.yaml) # - **mover は root**(PVC の中の持ち主がアプリごとに違う)。namespace に `volsync.backube/privileged-movers: "true"` が要る。 # Argo CD が作る namespace は bootstrap/argocd-repos.yaml の managedNamespaceMetadata、自分で Namespace を書いているところはそのファイル、 -# bootstrap 層(infisical など)は bootstrap/render.sh が付ける。PVC を足したら、その namespace に注釈があるかを見る +# bootstrap 層の namespace は bootstrap/render.sh が付ける。PVC を足したら、その namespace に注釈があるかを見る # - **時刻は全部 19:00 UTC(04:00 JST)。** VolSync は並べて流し、22 本で 5 分ほどで終わる(2026-11-02 に全部を同時に取って確かめた)。 # 保持は日次 7・週次 4・月次 6(録画だけ 1 世代)、prune は VolSync の既定の 7 日ごと apiVersion: argoproj.io/v1alpha1 diff --git a/docs/cloudflare-tokens.md b/docs/cloudflare-tokens.md index b32301a6..029fb3ab 100644 --- a/docs/cloudflare-tokens.md +++ b/docs/cloudflare-tokens.md @@ -1,16 +1,15 @@ # Cloudflare の API トークン -アプリが使う Cloudflare のトークンは **Pulumi で作らず、画面(か `cf`)で作って手で Infisical に入れる**。 -Pulumi で作ると値を Infisical に書く鍵が CI に要る。その鍵は prod 全体の読み書き・削除ができる Member になる -(Infisical の最小権限のカスタムロール・追加権限は Enterprise 限定)。漏れたときの範囲に見合わない。 +アプリが使う Cloudflare のトークンは **Pulumi で作らず、画面(か `cf`)で作って手で SOPS のファイルに入れる** +(`tools/t.ps1 sops edit <ファイル>` してコミット)。Pulumi で作ると、値を書き込む先(git の SOPS のファイル)まで CI に触らせることになる。 ## 一覧 | トークン | 種類 | 権限 | 置き場所 | 使うもの | | --- | --- | --- | --- | --- | -| `external-dns (doany.io DNS write)` | アカウント | `doany.io` の Zone Read + DNS Write | Infisical `/external-dns/external-dns` の `cloudflare-api-token` | deploy/external-dns | -| `doany-restic` | アカウント | R2 バケット `doany-restic` だけの Object Read & Write | Infisical `/backup/backup`、`recovery/env.age` | VolSync・etcd のスナップショット、復元(手元の restic) | -| cloudflare-ddns | ユーザー(アカウントの一覧に無い) | 画面で確かめる(DNS Write が要る) | Infisical `/cloudflare-ddns/cloudflare-ddns-secrets` の `CLOUDFLARE_API_TOKEN` | deploy/cloudflare-ddns | +| `external-dns (doany.io DNS write)` | アカウント | `doany.io` の Zone Read + DNS Write | `deploy/external-dns/secrets.sops.yaml` の `cloudflare-api-token` | deploy/external-dns | +| `doany-restic` | アカウント | R2 バケット `doany-restic` だけの Object Read & Write | `deploy/backup/secrets.sops.yaml`、各 namespace の `secrets.sops.yaml` の `volsync-`、`recovery/env.age` | VolSync・etcd のスナップショット、復元(手元の restic) | +| cloudflare-ddns | ユーザー(アカウントの一覧に無い) | 画面で確かめる(DNS Write が要る) | `deploy/cloudflare-ddns/secrets.sops.yaml` の `CLOUDFLARE_API_TOKEN` | deploy/cloudflare-ddns | | cert-manager | ユーザー(アカウントの一覧に無い) | 画面で確かめる(DNS Write が要る) | SOPS の `bootstrap/cert-manager-values.sops.yaml`(2 つめのドキュメント) | cert-manager(DNS-01) | | `todoroku-pulumi (state)` | アカウント | R2 バケット `todoroku-pulumi` だけの Object Read & Write | Forgejo `doa/todoroku` の secrets `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`(ID と値の SHA-256) | todoroku の `infra/`(Pulumi の state) | | `gitops pulumi apply` / `preview` | アカウント | pulumi/README.md「鍵と state」 | GitHub の secrets | gitops の Pulumi | @@ -22,11 +21,11 @@ Pulumi で作ると値を Infisical に書く鍵が CI に要る。その鍵は 権限は上の表のとおりに、**ゾーンやバケットを 1 つに絞って**作る。作ったら古いものを消す。 -- **external-dns / cloudflare-ddns:** Infisical の値を差し替える。operator が Secret を書き換え、 - push-bridge が同期させる。Deployment の `secrets.infisical.com/auto-reload` 注釈で入れ替わる。 +- **external-dns / cloudflare-ddns:** `secrets.sops.yaml` の値を差し替えてコミットする。Argo CD が当てると sops-secrets-operator が Secret を書き換え、 + external-dns は Reloader の注釈で入れ替わる。cloudflare-ddns は注釈が無いので `kubectl -n cloudflare-ddns rollout restart deploy` する。 - **todoroku-pulumi (state):** Forgejo の 2 つの secrets を差し替える(S3 の鍵の導き方は下の doany-restic と同じ)。 - **cert-manager:** `tools/t.ps1 sops edit bootstrap/cert-manager-values.sops.yaml` で書き換えてコミットする。 - **doany-restic(R2):** S3 の鍵はトークンから導く。アクセスキー ID はトークンの ID、シークレットはトークンの値の - SHA-256(16 進)。**2 か所を両方変える**。Infisical が古いとバックアップが落ち(バックアップの通知で気づく)、`env.age` が古いと建て直しで Infisical を戻せない(こちらは誰も気づかない)。 - 1. Infisical `/backup/backup` の `accessKeyId` / `secretAccessKey` + SHA-256(16 進)。**全部の置き場を変える**。SOPS のほうが古いとバックアップが落ち(バックアップの通知で気づく)、`env.age` が古いと手元の restic で R2 を読めない(こちらは誰も気づかない)。 + 1. `deploy/backup/secrets.sops.yaml` の `accessKeyId` / `secretAccessKey` と、各 namespace の `volsync-`(ほかのリポジトリの `deploy/secrets.sops.yaml` も) 2. `recovery/env.age`(作り直しは recovery/README.md) diff --git a/docs/decisions.md b/docs/decisions.md index 6136917e..21c769a6 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -9,7 +9,7 @@ (手順は [../recovery/README.md](../recovery/README.md))。**ツール選定は PV データの部分にしか効かない。** 変えないと決めてあるもの: restic のリポジトリ形式・パスフレーズ・バケット、StorageClass の名前 `local-path` -(中身は Z440 で OpenEBS LVM に替えた。名前は各リポジトリの PVC が名指ししている)、Infisical のデータと `ENCRYPTION_KEY`、ArgoCD + ApplicationSet + 各 repo の `deploy/argocd.yaml`、 +(中身は Z440 で OpenEBS LVM に替えた。名前は各リポジトリの PVC が名指ししている)、ArgoCD + ApplicationSet + 各 repo の `deploy/argocd.yaml`、 **復元手順は公開で秘密を含まない**という性質。 ## bond0 の MAC を固定する @@ -22,7 +22,7 @@ 判断基準は、(1) in-cluster で動かせること、(2) **手元からも同じリポジトリを同じコマンドで読める**こと、 (3) 復元の依存物が「バイナリ 1 つ + 秘密 1 組」で済むこと、の 3 つ。 -**結論: restic。** 手動でも `restic restore` 1 本で戻せ、手元の restic CLI で同じリポジトリを読める(Infisical はそうやって戻す)。 +**結論: restic。** 手動でも `restic restore` 1 本で戻せ、手元の restic CLI で同じリポジトリを読める。 kopia は Velero 経由だと独自のレイアウトになり、Velero は git からの再構築と役割が被り、borg などは in-cluster に持っていけない。 取るのは VolSync(PVC ごとにスナップショットから。下の「ストレージを OpenEBS LVM LocalPV に、バックアップを VolSync にする」。2026-11 までは k8up)。 @@ -57,7 +57,7 @@ diceware 6〜7 語相当を使う。**同期ストレージ(OneDrive など)を | | 何を置くか | 理由 | | --- | --- | --- | -| **パスワードマネージャ**(正本) | パスフレーズ + **何を開ける鍵かと復元手順の URL** | 携帯から取れる。**クラスタの Entra とは独立した認証**であること(Infisical は循環するので不可) | +| **パスワードマネージャ**(正本) | パスフレーズ + **何を開ける鍵かと復元手順の URL** | 携帯から取れる。**クラスタの Entra とは独立した認証**であること(クラスタの中のものは循環するので不可) | | **紙、2 か所**(1 つは家の外) | 同上 + `recovery/*.age` を base64 で印刷(19 行) | 火事・盗難・端末紛失、GitHub が読めない場合に耐える | | **記憶** | パスフレーズ | 出先で全部が閉じている状況を消す。ただし単独では信用しない | @@ -76,23 +76,24 @@ diceware 6〜7 語相当を使う。**同期ストレージ(OneDrive など)を ## Talos の起動順序をどう組むか Talos には helm-controller が無く、inlineManifests は Helm を実行できないので、手元の `render.sh` が `helm template` して載せる。 -**ArgoCD は Infisical 無しで起動する。** `$argocd-oidc:client-secret` のような参照は Secret が無いと警告して文字列のまま返す +**ArgoCD はアプリの秘密無しで起動する。** `$argocd-oidc:client-secret` のような参照は Secret が無いと警告して文字列のまま返す (`util/settings/settings.go`)ので、SSO が効かないだけで同期は動く。順序は -**Cilium → ArgoCD → ArgoCD が Infisical を入れる → operator が Secret を作る → SSO が効く**。 +**Cilium → ArgoCD → ArgoCD が sops-secrets-operator を入れる → operator が SopsSecret から Secret を作る → SSO が効く** +(operator の age の鍵は machine config が入れる。下の「秘密は SOPS で git に置く」)。 その窓の間は `admin.enabled: false` なので UI に誰も入れない(要るなら一時的に戻す。`kubectl` で見る)。 ### 層の分け方 | 層 | 中身 | 資格情報 | | --- | --- | --- | -| **machine config** | **`bootstrap/` の全部**(`argocd-repos.yaml` を除く)。inline が 10(cilium / lvm-localpv / `storage.yaml` / `apiserver-rbac.yaml` / argocd / cert-manager / infisical / SOPS の Secret 3 つ)、URL で 10 個(CRD が gateway-api / argocd ×3 / cert-manager / VolumeSnapshot ×3、snapshot-controller ×2) | 入力(`bootstrap/talos-secrets.sops.yaml` など)が SOPS 済みで、生成物は git に置かない | -| **ArgoCD** | `deploy/`。`ClusterIssuer`・Gateway・auth・`InfisicalSecret` など、本体が machine config に居る部品の平文のマニフェストもここ(2026-10 までは `bootstrap/` に混ぜて Application `bootstrap` が当てていた)。**infisical の本体は来ない**(下の「infisical だけは ArgoCD に移せない」) | — | +| **machine config** | **`bootstrap/` の全部**(`argocd-repos.yaml` を除く)。inline が 8(cilium / lvm-localpv / `storage.yaml` / `apiserver-rbac.yaml` / argocd / cert-manager / SOPS の Secret 2 つ)、URL で 10 個(CRD が gateway-api / argocd ×3 / cert-manager / VolumeSnapshot ×3、snapshot-controller ×2) | 入力(`bootstrap/talos-secrets.sops.yaml` など)が SOPS 済みで、生成物は git に置かない | +| **ArgoCD** | `deploy/`。`ClusterIssuer`・Gateway・auth・`SopsSecret` など、本体が machine config に居る部品の平文(と SOPS 済み)のマニフェストもここ(2026-10 までは `bootstrap/` に混ぜて Application `bootstrap` が当てていた) | — | - **CRD だけ URL で渡すのは大きさの都合。** 埋めると machine config が 341 KB → 4 MB を超える。 `KubeExternalManifestConfig` は Talos がそのために用意している入口([bootstrap/render.sh](../bootstrap/render.sh)) - **平文の残りは ArgoCD。** 鶏と卵にならない ── ArgoCD が動くのに要るもの(本体・repo の資格情報・入口の `apps`)は全部 machine config にあり、この層に頼っていない。 - この層が要る CRD も machine config が入れる(`InfisicalSecret` の CRD だけ `deploy/` から来るので、最初の何回かは retry)。 + この層が要る CRD も machine config が入れる(`SopsSecret` の CRD だけ `deploy/` から来るので、最初の何回かは retry)。 - **SOPS の Secret を machine config に入れる。** 復号鍵はクラスタにも CI にも渡さないので、ArgoCD からは当てられない。 machine config の入力(`bootstrap/talos-secrets.sops.yaml`)もクラスタ CA の秘密鍵を含む SOPS なので信頼水準は変わらず、起動時点で存在する - **Helm を実行する場所はクラスタの外(手元の生成時)だけ。** Cilium は CNI なので inlineManifests 以外に置きようがなく、 @@ -110,9 +111,9 @@ Talos には helm-controller が無く、inlineManifests は Helm を実行で bootstrap のあとに `helm install` する案は、再構築のたびに「CNI が無いので何も動かない」状態での手作業が増えるので採らない。 **描いた結果はコミットしない。** 描き出しを git に置いて Renovate に描き直させる案は採らない: -- **infisical の描き出しは秘密を含む**(下の「infisical だけは ArgoCD に移せない」)。ここだけは手元で描くしかなく、描く道具は結局残る +- **描き出しは秘密を含む**(クラスタの CA と SOPS の Secret 2 つ)。ここだけは手元で描くしかなく、描く道具は結局残る - 描き出しは machine config で 980 行・310 KB(1 行が長い)。版を上げるたびにその差分が積もる -- 代わりに **描くたびに変わる値を潰した**(Hubble の証明書・infisical の `postgres-password`・`updatedAt`)。 +- 代わりに **描くたびに変わる値を潰した**(Hubble の証明書)。 同じ入力なら生成物はバイト単位で同じなので、`apply-config --dry-run` の差分がそのまま「何が変わるか」になる **起動に要らないものは ArgoCD に置く**(metrics-server は `deploy/cluster/metrics-server.yaml`。版上げが `upgrade-k8s` 無しで済む)。cert-manager は移さない ── CRD を machine config から外すと Talos の prune が CRD を消しうる(消えれば @@ -193,20 +194,21 @@ Gateway API の session persistence は experimental チャネル限定、Cilium - リソース名に時刻や乱数を入れない(GitOps は同期のたびに別物として作り直す) - CRD は `templates/` ではなく `crds/` に置く(`templates/` だとアンインストールで CR が全部巻き添えになる) -- 全ワークロードに `podAnnotations` と `resources` を出す(注釈は Infisical・ArgoCD が振る舞いを足す口) +- 全ワークロードに `podAnnotations` と `resources` を出す(注釈は Reloader・ArgoCD が振る舞いを足す口) - patch で中身を入れ替えない(chart の patch 更新で Dragonfly が Valkey に替わり、values のチューニングが黙って無効になった) - 入口(`Ingress` / `httpRoute`)を既定で作らない **別のリリース名で入れ直すと別物として作り直しになる**(名前を切り詰める chart がある)。入れ方を替えるときは `helm template` と live を先に突き合わせる。 -### infisical だけは ArgoCD に移せない +## 秘密は SOPS で git に置く(2026-11-02 に Infisical から替えた) -**理由は鶏卵ではなく chart の作り。** `infisical-standalone` 1.10.0 は `DB_CONNECTION_URI` と `REDIS_URL` を -`.Values.*.auth.password` から **Deployment の平文 env に焼き込む**(`templates/_helpers.tpl`)ので、平文の Application に値を書けない。 -`postgresql.useExistingPostgresSecret` は同梱の Postgres と併用すると URI が 2 回描かれて平文が勝ち、Redis には逃げ道が無い。 -`*.auth.existingSecret` は subchart にしか効かず、本体は既定値で URI を組んで繋がらなくなる(`helm template` で確認)。 -**なので `bootstrap/` に残し、machine config の `inlineManifests` に載せる。** `render.sh` が SOPS 済みの値を復号して `helm template` に渡すので、 -平文は machine config の中にしか出ない。 +**アプリの秘密は各 `deploy/<アプリ>/secrets.sops.yaml`(`SopsSecret`)に暗号化して置き、クラスタの中の +[sops-secrets-operator](../deploy/sops-secrets-operator/) が復号して `Secret` にする。** Pod の入れ替えは [Reloader](../deploy/reloader/)。 +暗号化はクラスタの鍵と管理者の鍵の 2 つ([../.sops.yaml](../.sops.yaml))。クラスタの鍵は `bootstrap/sops-age.sops.yaml` で machine config が入れる。 + +それまではセルフホストの Infisical(chart は machine config、operator は `deploy/infisical/`)が持っていた。やめた理由: +秘密の置き場がクラスタの中にあるので、建て直しでは Infisical の DB と `ENCRYPTION_KEY`・Kubernetes auth の CA を先に手で戻す必要があり、 +戻るまで他の全部の Secret(VolSync のものも)が来なかった。SOPS なら秘密は git にあり、復元で秘密のために何もしなくてよい。 ## PT3 チューナー @@ -224,7 +226,7 @@ extension は `earth_pt3` を blacklist して配るので、`bootstrap/talos.so 共有プリセット `5ym/repo-config` で、**パッチとマイナーは 1 つの PR にまとめて自動マージ、メジャーと Helm chart は人が見る** (chart はパッチ更新でも中身が入れ替わることがある)。 -PostgreSQL は 17 の線に固定(メジャーでデータディレクトリの互換が切れ、dump / restore が要る)、argo-cd と infisical-standalone の chart は自動マージしない。 +PostgreSQL は 17 の線に固定(メジャーでデータディレクトリの互換が切れ、dump / restore が要る)、argo-cd の chart は自動マージしない。 理由はそれぞれ [../renovate.json](../renovate.json) の `description`。Renovate 自体は [deploy/renovate/](../deploy/renovate/) の CronJob で動く。 ## PVC を守る仕掛けをやめた @@ -301,7 +303,7 @@ StorageClass を変えた PVC を作り直すだけで中身が埋まるので - **snapshot-controller と VolumeSnapshot の CRD は chart の同梱を使わず**、external-snapshotter のタグの URL で入れる(版は `talos.sops.yaml` の先頭)。 クラスタに 1 つのもので、VolSync も使う - **talos-boot が確かめていること**: VG と thin プール(VG の 9 割)、PVC が xfs で bind、VolumeSnapshot が readyToUse、 - スナップショットから作った PVC に中身が戻る、再起動のあとも PV と infisical の Postgres が戻る + スナップショットから作った PVC に中身が戻る、再起動のあとも PV が戻る local-path から変わったこと: **PVC の requests が本当の上限になった**(local-path はディスクの残り全部が使えた。 中身が PVC より大きいと populator は埋められない)、**新しい PVC の直下が root の 0755**(local-path は 0777。root 以外で書くアプリは `fsGroup` が要る)。 @@ -358,7 +360,7 @@ ExternalDNS は HTTPRoute の注釈だけで決まり、自分が作ったレコ SMTP AUTH の Basic 認証は 2026-12 末に既定で無効になる。移る先の候補は OAuth(XOAUTH2)・Graph の `sendMail`・IP で許すコネクタ。 - **XOAUTH2 は使えない。** `info@doany.io` は Exchange Online Kiosk で、Kiosk は SMTP の OAuth に対応していないとされる。 - Infisical はそもそも SMTP のユーザー名とパスワードしか話せない + Infisical(2026-11-02 にやめた)はそもそも SMTP のユーザー名とパスワードしか話せなかった - **コネクタ(IP で許す)は家の IP が固定でないので筋が悪い** - したがって **Graph の `sendMail`**。アプリごとに Graph を話させず、普通の SMTP を受けて Graph に渡す中継 ([smtp-oauth-relay](https://github.com/JustinIven/smtp-oauth-relay)、`deploy/smtp-relay/`)を 1 つ置き、3 つのアプリは向け先だけ替えた diff --git a/pulumi/README.md b/pulumi/README.md index 87cff100..da1d5062 100644 --- a/pulumi/README.md +++ b/pulumi/README.md @@ -73,7 +73,7 @@ state は R2 の別バケット **`doany-pulumi`**(APAC)にあり、パスフレ [notifications.ts](notifications.ts)。証明書・DDoS・オリジン不達・不正利用の報告・Security Insights を、Matrix の 部屋 `server`(スペース「通知」。hookshot の受け口 `cloudflare`)に流す。送り先の URL は hookshot の ID を含むので Actions secrets の -`CLOUDFLARE_NOTIFY_WEBHOOK_URL`(Infisical `/matrix/matrix` の `hook-cloudflare` から作る)。 +`CLOUDFLARE_NOTIFY_WEBHOOK_URL`(`deploy/matrix/secrets.sops.yaml` の `hook-cloudflare` から作る)。 ## Entra diff --git a/pulumi/entra-smtp.ts b/pulumi/entra-smtp.ts index 3a0a3b0e..02c3df2b 100644 --- a/pulumi/entra-smtp.ts +++ b/pulumi/entra-smtp.ts @@ -5,7 +5,7 @@ // 代わりに Exchange の RBAC for Applications で、送れるメールボックスを info@doany.io だけに絞る(deploy/smtp-relay/README.md)。 // それは Exchange の PowerShell でしかできないので Pulumi の外。 // -// クライアントシークレットも持たない(手元から state を読めず、Infisical に渡す道が無い)。画面で作って Infisical に入れる。 +// クライアントシークレットも持たない(手元から state を読めず、送る側に渡す道が無い)。画面で作って送る側の SOPS のファイルに入れる。 // 期限は pulumi.yml の expiry が知らせる。 import * as azuread from "@pulumi/azuread"; diff --git a/pulumi/notifications.ts b/pulumi/notifications.ts index 8f62f8c1..b2d14967 100644 --- a/pulumi/notifications.ts +++ b/pulumi/notifications.ts @@ -1,5 +1,5 @@ // Cloudflare の通知を Matrix の部屋 server に流す(hookshot。deploy/matrix/hookshot.yaml)。 -// 受け口の URL は ID を含むので secrets(値は Infisical /matrix/matrix の hook-cloudflare から作る)。 +// 受け口の URL は ID を含むので secrets(値は deploy/matrix/secrets.sops.yaml の hook-cloudflare から作る)。 import * as cloudflare from "@pulumi/cloudflare"; import * as pulumi from "@pulumi/pulumi"; diff --git a/recovery/README.md b/recovery/README.md index eea87ed9..0bc3e6aa 100644 --- a/recovery/README.md +++ b/recovery/README.md @@ -7,11 +7,13 @@ | | | | --- | --- | | `env.age` | バックアップ先の資格情報(R2 の鍵、restic のパスワード)。age のパスフレーズで暗号化 | -| `sops-age.key.age` | `bootstrap/` の SOPS ファイルを復号する age 秘密鍵。**同じパスフレーズ**で暗号化 | -| [backup-all.sh](backup-all.sh) | 入れ直す前に全部の PVC を今すぐ取る道具(戻すのは populator。下の 8) | -| [infisical.sh](infisical.sh) / [infisical-ca.mjs](infisical-ca.mjs) | Infisical を restic から直接戻す道具(下の 6・7) | +| `sops-age.key.age` | `bootstrap/` の SOPS ファイルを復号する age 秘密鍵(管理者の鍵。`deploy/` の SopsSecret も開ける)。**同じパスフレーズ**で暗号化 | +| [backup-all.sh](backup-all.sh) | 入れ直す前に全部の PVC を今すぐ取る道具(戻すのは populator。下の 6) | -**populator で戻す形(8)は、この形ではまだ入れ直して流していない。** 次の建て直しで最初に確かめる(VolSync の populator が OpenEBS LVM のスナップショットから PVC を作れることは、machine config の PR ごとの QEMU の起動テストで見ている)。 +**秘密のために復元ですることは無い。** アプリの秘密は git の SOPS(各 `deploy/` の `secrets.sops.yaml`)にあり、 +クラスタの中で復号する鍵は machine config([../bootstrap/sops-age.sops.yaml](../bootstrap/sops-age.sops.yaml))が入れる。 + +**populator で戻す形(6)は、この形ではまだ入れ直して流していない。** 次の建て直しで最初に確かめる(VolSync の populator が OpenEBS LVM のスナップショットから PVC を作れることは、machine config の PR ごとの QEMU の起動テストで見ている)。 **手動のリハーサルは回さない。** 中身は月次の [restore-check](../deploy/backup/restore-check.yaml)、machine config は [talos-boot](../.github/workflows/talos-boot.yml) が確かめる。残るのはこの手順書の通し(道具そのもの)だけ。 @@ -27,7 +29,8 @@ base64 recovery/env.age recovery/sops-age.key.age # 印刷して封筒へ `env.age` の中身は `KEY=値` の env ファイル(`RESTIC_REPOSITORY=s3:https://.r2.cloudflarestorage.com/`・ `RESTIC_PASSWORD`・`AWS_ACCESS_KEY_ID`・`AWS_SECRET_ACCESS_KEY`・`AWS_DEFAULT_REGION=auto`・`MATTERMOST_WEBHOOK`)。 -値を入れ替えたら作り直してコミットする(`age -p -o recovery/env.age `。同じ値は Infisical の `/backup/backup` にもある)。 +値を入れ替えたら作り直してコミットする(`age -p -o recovery/env.age `。同じ値は [../deploy/backup/secrets.sops.yaml](../deploy/backup/secrets.sops.yaml) にもある)。 +復元の手順そのものには要らない(PVC は VolSync が SopsSecret の資格情報で戻す)。手元の restic で R2 を直接読むときに使う。 ## 何で戻すか @@ -35,8 +38,8 @@ base64 recovery/env.age recovery/sops-age.key.age # 印刷して封筒へ | --- | --- | | ホストの設定 | **machine config**。`bootstrap/render.sh` が repo から描く(`bootstrap/talos-secrets.sops.yaml` が同じなら同じクラスタ CA) | | k8s オブジェクト | **git から Argo CD で再構築**。`bootstrap/` は machine config が当てる | -| Infisical の DB | **VolSync の `ReplicationDestination`、Secret は手元の restic.env から**(ほかの Secret が Infisical から来るので。[infisical.sh](infisical.sh)) | -| ほかの PV データ | **VolSync の populator**(PVC の `dataSourceRef` → 各アプリの `-dst`。Argo CD が PVC を作ると埋まる) | +| 秘密 | **git の SOPS**(`SopsSecret`)。クラスタの鍵は machine config が入れ、sops-secrets-operator(Argo CD が入れる)が Secret にする | +| PV データ | **VolSync の populator**(PVC の `dataSourceRef` → 各アプリの `-dst`。Argo CD が PVC を作ると埋まる) | etcd のスナップショット(リポジトリ `/etcd`、[../deploy/backup/etcd-snapshot.yaml](../deploy/backup/etcd-snapshot.yaml))は git に無い状態まで一度に戻したいときの近道で、主経路ではない([../bootstrap/talos.md](../bootstrap/talos.md)「etcd スナップショットから戻す(近道)」)。 @@ -63,8 +66,7 @@ mkdir -p ~/.config/sops/age && age -d -o ~/.config/sops/age/keys.txt recovery/so ./bootstrap/render.sh /dev/shm/c && rm -rf /dev/shm/c # 描けるかだけ見る。`valid for metal mode` まで出ること ``` -- **`bootstrap/talos-secrets.sops.yaml` を作り直さない。** 作り直すと別のクラスタ CA になり、`bootstrap/apiserver-ca.crt`・ - Infisical の Kubernetes auth(7)が合わなくなる +- **`bootstrap/talos-secrets.sops.yaml` を作り直さない。** 作り直すと別のクラスタ CA になり、`bootstrap/apiserver-ca.crt` が合わなくなる - **描いたものは手元に置かない**(秘密が平文で入る)。当てるときは 3 の `apply.sh` がその場で描いて消す - 版は [../bootstrap/talos.sops.yaml](../bootstrap/talos.sops.yaml) の先頭のコメント。**上げるなら建て直しの日ではない日に** @@ -108,7 +110,7 @@ talosctl get volumestatus # EPHEMERAL は 64GiB 上限。違ったら入れ ```shell talosctl bootstrap # 単一ノードなので 1 回だけ kubectl get nodes # Ready / control-plane / talos.sops.yaml の先頭の kubernetes の版 -kubectl get pods -A # cilium・coredns・lvm-thinpool・lvm-localpv・snapshot-controller・argocd・cert-manager・infisical が Running +kubectl get pods -A # cilium・coredns・lvm-thinpool・lvm-localpv・snapshot-controller・argocd・cert-manager が Running kubectl -n kube-system logs ds/lvm-thinpool -c pool # 最後に data_thinpool(VG の 90%)が出る。出なければ PVC が 1 つも bind しない kubectl get gatewayclass # cilium が Accepted(無いと Gateway が `Waiting for controller` のまま) ``` @@ -118,55 +120,28 @@ kubectl get gatewayclass # cilium が Accepted(無いと Gateway が `Waiting ## 5. Gateway・auth などが入るのを見る(ArgoCD) 手ですることは無い。ArgoCD の `gitops` の Application([../bootstrap/argocd-repos.yaml](../bootstrap/argocd-repos.yaml) の ApplicationSet が作る)が `deploy/` の -`ClusterIssuer`・Gateway・auth・`InfisicalSecret` を当てる。 +`ClusterIssuer`・Gateway・auth・sops-secrets-operator と各 namespace の `SopsSecret` を当てる。 ```shell -kubectl -n argocd get application gitops # InfisicalSecret の CRD が来るまでは何度か落ちて retry で戻る +kubectl -n argocd get application gitops # SopsSecret の CRD が来るまでは何度か落ちて retry で戻る +kubectl get sopssecrets -A # STATUS が全部 Healthy(Secret ができた) kubectl get certificate -A # Gateway に証明書が付く ``` -Argo CD はこの間に `deploy/` の残りと各リポジトリの `deploy/` も同期しはじめる。**中身はまだ空**なので、 -Infisical の Secret を待つアプリと、Forgejo から来る非公開アプリ(`repos-forgejo` が `ErrorOccurred`)は上がらない。想定どおり。 - -## 6. Infisical の DB を戻す - -```shell -age -d -o ~/.config/restic.env recovery/env.age # パスフレーズ。終わったら消す(10) -sh recovery/infisical.sh db # users が 0 なら止まる -``` - -Infisical の Postgres の PVC(`data-postgresql-0`)を、VolSync のリポジトリ(`/volsync/infisical/data-postgresql-0`)の -**このクラスタができる前の最新**のスナップショットで丸ごと置き換える(ReplicationDestination を Direct で。ここだけ populator を使えない)。 -VolSync の Secret はほかの namespace では Infisical から来るが、ここだけは戻すのが Infisical そのものなので、`restic.env` から作る。 -古いものを選ぶときは `RESTORE_BEFORE=2026-10-30T00:00:00Z sh recovery/infisical.sh db`。 - -## 7. Infisical の Kubernetes auth の CA を差し替える - -**DB を戻すと、operator の Machine Identity(`f2bc3416…`、許可 SA `infisical/infisical-auth`)の CA も -古いクラスタのものに戻る。** InfisicalSecret が全部 `SSL certificate verification failed … self-signed certificate` で止まる。 - -管理者でログインしておく。ブラウザのログインは t.ps1 が中継するので、シェルの中ではなく手元で -`tools/t.ps1 infisical login --domain https://il.doany.io/api`(トークンは `.home/` に残る)。 - -```shell -sh recovery/infisical.sh ca # クラスタと Infisical の CA の頭を並べて見る -sh recovery/infisical.sh ca apply # 差し替えて、operator を蹴り直す(失敗が続くと間を空けるため) -kubectl get infisicalsecret -A # 全部 Ready になる。backup/backup と各 namespace の volsync もここで入り、VolSync の Secret ができる -``` - -`talos-secrets.sops.yaml` を作り直していなければ CA は前と同じなので、`ca` で頭が一致していれば `apply` は要らない。 +Argo CD はこの間に `deploy/` の残りと各リポジトリの `deploy/` も同期しはじめる。**PVC の中身は 6 で埋まる**まで、 +PVC を待つアプリと、Forgejo から来る非公開アプリ(`repos-forgejo` が `ErrorOccurred`)は上がらない。想定どおり。 -## 8. PV データを戻す(VolSync の populator。待つだけ) +## 6. PV データを戻す(VolSync の populator。待つだけ) **何も打たない。** 各アプリの PVC は `dataSourceRef` で ReplicationDestination(各アプリの `volsync.yaml` の `-dst`)を指している。 Argo CD が PVC を作ると、VolSync(volume populator)が R2 の `/volsync//` の最新を一時的なボリュームに戻し、 そのスナップショットから PVC を作ってから渡す。PVC は埋まるまで `Pending` で、アプリの Pod は空の PVC では起きない。 -- **順番は勝手にそろう。** RD が使う Secret(`volsync-`)は Infisical から来るので、6・7 が済むまで RD は失敗してやり直す(PVC は `Pending` のまま)。 +- **順番は勝手にそろう。** RD が使う Secret(`volsync-`)は SopsSecret から来るので、sops-secrets-operator が作るまで RD は失敗してやり直す(PVC は `Pending` のまま)。 Forgejo の PVC は gitops にあるので先に埋まり、非公開アプリ(マニフェストもイメージも Forgejo)はそのあとに出てきて埋まる - **空の PVC を取ることはない。** ReplicationSource は PVC が埋まるまでスナップショットを取れないので、戻す前の空の中身が「いちばん新しい」にならない - **Postgres・SQLite・Tuwunel(RocksDB)もファイルのまま戻る。** スナップショットから取っているので crash-consistent で、起きるときに自分で立ち直る -- **infisical の PVC だけは別**(6 で `recovery/infisical.sh` が戻す。Infisical が無いと Secret が無いので)。denpa-raw は取っていないので空で作る +- denpa-raw は取っていないので空で作る - **バックアップがまだ無い PVC**(足したばかりのアプリ)は、リポジトリが空なので空のまま作る - 見るところ: `kubectl get pvc -A`(`Pending` が残っていないか)と `kubectl get replicationdestination -A`(`-dst` の `latestMoverStatus`) @@ -189,7 +164,7 @@ kubectl -n argocd scale sts/argocd-application-controller --replicas=1 # PVC - **PVC の requests が本当の上限**(OpenEBS LVM)。中身が PVC より大きくなっていると埋められない。PVC を大きくしてから作り直す - **k8up の頃のリポジトリ**(バケットの根)は 2026-11-01 に消した。2026-11-01 より前のものは戻せない -## 9. 確かめる +## 7. 確かめる 公開している名前を全部叩く(作業用 PC のコンテナから。ノードの上からは 80/443 に届かない)。 @@ -207,7 +182,7 @@ kubectl get httproute,grpcroute -A -o jsonpath='{range .items[*]}{range .spec.ho | **401** | `dp` ── **これが正しい。** 公開側から来た通信を denpa が弾いている。200 なら X-Forwarded-For が壊れていて住所判定が効いていない | | ERR | `dp.l.doany.io` ── 宅内専用の名前。AdGuard を引く端末からしか通らない | -- [ ] `kubectl -n argocd get applications` が全部 Synced / Healthy、`kubectl get infisicalsecret -A` が全部 Ready +- [ ] `kubectl -n argocd get applications` が全部 Synced / Healthy、`kubectl get sopssecrets -A` が全部 Healthy - [ ] `talosctl get links bond0` の MAC が **`e6:2c:1b:1a:a0:39`**(ルーターの IPv6 ホスト公開がこの MAC を指す。[../docs/router.md](../docs/router.md)) - [ ] **別の回線の IPv6 だけで 443 に届く。** J:COM 側の PC から `curl -6 -so /dev/null -w "%{http_code} %{remote_ip}\n" https://fj.doany.io/` で `200 240f:6d:842b:1::2`(アドレス直打ちは SNI が無く Gateway が切るので駄目) @@ -219,10 +194,10 @@ kubectl get httproute,grpcroute -A -o jsonpath='{range .items[*]}{range .spec.ho - [ ] Renovate の CronJob が次の回で動く(`kubectl -n renovate get jobs`)、バックアップの通知(deploy/backup/notify.yaml)が翌日 OK ✅ - [ ] etcd のスナップショットを 1 回手で取る(`kubectl -n backup create job --from=cronjob/etcd-snapshot etcd-snapshot-first`) -## 10. 後始末 +## 8. 後始末 ```shell -rm -f ~/.config/restic.env ~/infisical-load.log +rm -f ~/.config/restic.env # env.age を開いて手元の restic を使ったとき ``` ## 詰まったときに見るところ @@ -232,7 +207,7 @@ rm -f ~/.config/restic.env ~/infisical-load.log | ノードが上がらない | AMT の SOL(インストール済みのノードのカーネルのコンソール)。ダッシュボード(版と健全性)は Arc につないだモニタ | | Pod が Pending | **PSA のラベル**([../bootstrap/talos.md](../bootstrap/talos.md)「PSA のラベル」)か、control-plane の taint | | PVC が Bound しない | `kubectl -n kube-system logs ds/lvm-thinpool -c pool`(VG `data` が無い = `talosctl get volumestatus` の `r-lvm`)、ノードに `doany.io/lvm-thinpool=ready` が付いて `lvm-localpv-node` が動いているか、`kubectl -n kube-system logs ds/lvm-localpv-node -c openebs-lvm-plugin` | -| 非公開イメージが引けない | `bootstrap/talos.sops.yaml` の fj.doany.io の `password` が machine config に入っているか、Forgejo が戻っているか(8) | +| 非公開イメージが引けない | `bootstrap/talos.sops.yaml` の fj.doany.io の `password` が machine config に入っているか、Forgejo が戻っているか(6) | | cilium-drift が TLS で落ちる | `talos-secrets.sops.yaml` を作り直した。`bootstrap/apiserver-ca.crt` を新しい `talos-secrets.sops.yaml` の Kubernetes の CA(`certs.k8s.crt` を base64 で戻したもの)にする | | Application が `OutOfSync / Missing` のまま | `retry`(約 33 分)で自力で戻る。30 分以上動かなければ `kubectl -n argocd patch application <名前> --type=merge -p '{"operation":{"sync":{"revision":"HEAD"},"initiatedBy":{"username":"me"}}}'`。**chart のものは `HEAD` ではなく `spec.source.targetRevision` の値** | | inlineManifest を直したのに効かない | `apply.sh` → **`upgrade-k8s`**([../bootstrap/talos.md](../bootstrap/talos.md)「当て方」) | diff --git a/recovery/infisical-ca.mjs b/recovery/infisical-ca.mjs deleted file mode 100644 index 63bc08c5..00000000 --- a/recovery/infisical-ca.mjs +++ /dev/null @@ -1,22 +0,0 @@ -// Infisical の Kubernetes auth(operator の Machine Identity)の CA を見る / 差し替える。 -// infisical.sh ca から呼ぶ(port-forward 18080 と TOKEN・CA の env はあちらが用意する)。トークンは表示しない。 -// identity は bootstrap/README.md「Infisical の中の構成」の infisical-operator(InfisicalSecret の identityId)。 -const ID = "f2bc3416-f196-4626-9bf3-c11693b32cc6"; -const url = `http://127.0.0.1:18080/api/v1/auth/kubernetes-auth/identities/${ID}`; -const ca = Buffer.from(process.env.CA, "base64").toString(); -const h = { Authorization: `Bearer ${process.env.TOKEN}`, "Content-Type": "application/json" }; -// 比べやすいように、CA は先頭 1 行(BEGIN の次)の頭だけ出す -const head = (pem) => (pem ?? "").split("\n")[1]?.slice(0, 20); -const show = (j) => { - const a = j.identityKubernetesAuth; - return a - ? { kubernetesHost: a.kubernetesHost, tokenReviewMode: a.tokenReviewMode, allowedNamespaces: a.allowedNamespaces, - allowedNames: a.allowedNames, caHead: head(a.caCert) } - : { status: j.statusCode, message: j.message }; -}; -console.log("cluster caHead:", head(ca)); -console.log("infisical:", show(await (await fetch(url, { headers: h })).json())); -if (process.argv[2] === "apply") { - const r = await fetch(url, { method: "PATCH", headers: h, body: JSON.stringify({ caCert: ca }) }); - console.log("patched:", r.status, show(await r.json())); -} diff --git a/recovery/infisical.sh b/recovery/infisical.sh deleted file mode 100755 index c88fc448..00000000 --- a/recovery/infisical.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/bin/sh -# Infisical を戻す道具。手順は recovery/README.md の 6・7。2026-10-17 の建て直しで使ったもの。 -# -# tools/t.ps1 --admin sh recovery/infisical.sh db DB の PVC を VolSync のスナップショットから戻して Infisical を起こす -# tools/t.ps1 --admin sh recovery/infisical.sh ca [apply] Kubernetes auth の CA を見る / 新しいクラスタのものにする -# -# **Infisical だけは populator で戻せない。** VolSync の Secret が Infisical から来るので、Secret を -# 手元の ~/.config/restic.env(README の 6)から作って戻す。 -set -eu -K=kubectl -# Infisical の chart の Postgres(bitnami)。資格情報は Pod の env にある -PSQL='export PGPASSWORD="$POSTGRES_PASSWORD"; psql -U "$POSTGRES_USER" -d "$POSTGRES_DATABASE"' -pg() { $K -n infisical exec postgresql-0 -- sh -c "$PSQL $*"; } - -case ${1:-} in - db) - # **PVC(data-postgresql-0)を丸ごと戻す**(VolSync のスナップショット)。ほかの PVC は populator で作るときに埋まるが、 - # ここは bootstrap の chart の StatefulSet が作る PVC で dataSourceRef を書けないので、ReplicationDestination(Direct)で上から書く。 - # Secret は、ほかの namespace と違って InfisicalSecret から作れない(戻すのが Infisical そのもの)ので、 - # 手元の restic.env から作る。あとで deploy/infisical/volsync.yaml の InfisicalSecret が同じ名前で上書きする(中身は同じ) - set -a; . "$HOME/.config/restic.env"; set +a - repo=${RESTIC_REPOSITORY%/}/volsync/infisical/data-postgresql-0 - $K -n infisical create secret generic volsync-data-postgresql-0 \ - --from-literal=RESTIC_REPOSITORY="$repo" --from-literal=RESTIC_PASSWORD="$RESTIC_PASSWORD" \ - --from-literal=AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" --from-literal=AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" \ - --from-literal=AWS_DEFAULT_REGION=auto --dry-run=client -o yaml | $K apply -f - >/dev/null - # 書いている最中に Postgres が起きないよう止める(ほかは Postgres を待つだけなので止めなくてよい) - $K -n infisical scale sts/postgresql --replicas=0 >/dev/null - $K -n infisical wait --for=delete pod/postgresql-0 --timeout=120s >/dev/null 2>&1 || true - # **このクラスタができる前の最新**を戻す。ReplicationSource は Argo CD が作るとすぐ時刻を待ちはじめ、戻す前の空の PVC を - # 取ったものが「いちばん新しい」になりうる。境目は kube-system の namespace ができた時刻(RESTORE_BEFORE で上書きできる) - before=${RESTORE_BEFORE:-$($K get ns kube-system -o jsonpath='{.metadata.creationTimestamp}')} - t=restore-$(date -u +%Y%m%d%H%M%S) - $K -n infisical delete replicationdestination restore-db --ignore-not-found >/dev/null - $K create -f - >/dev/null <&2; exit 1; } - $K -n infisical delete replicationdestination restore-db --wait=false >/dev/null - $K -n infisical scale sts/postgresql --replicas=1 >/dev/null - $K -n infisical rollout status sts/postgresql --timeout=300s - users=$(pg "-Atc 'select count(*) from public.users'" 2>/dev/null || echo 0) - echo "users: $users / projects: $(pg "-Atc 'select count(*) from public.projects'" 2>/dev/null || echo 0)" - [ "${users:-0}" -gt 0 ] || { echo "users が 0。戻したスナップショットを見直す(RESTORE_BEFORE)" >&2; exit 1; } - $K -n infisical rollout restart deploy/infisical >/dev/null - $K -n infisical rollout status deploy/infisical --timeout=300s ;; - ca) - # operator の Machine Identity は Kubernetes auth で、Infisical が TokenReview を呼ぶときに - # **古いクラスタの CA を持ったまま**になる(DB を戻すと設定も戻るため)。InfisicalSecret が - # `SSL certificate verification failed … self-signed certificate` で全部止まる。 - # 管理者のトークン(`infisical login` 済みであること)で CA だけ差し替える。 - CA=$($K config view --raw --minify -o jsonpath='{.clusters[0].cluster.certificate-authority-data}') - [ -n "$CA" ] || { echo "kubeconfig から CA を読めなかった" >&2; exit 1; } - TOKEN=$(infisical user get token --plain 2>/dev/null || true) - [ -n "$TOKEN" ] || { echo "infisical のトークンが無い。tools/t.ps1 infisical login --domain https://il.doany.io/api" >&2; exit 1; } - # 18080 はこのコンテナの中だけのポート(t.ps1 は実行ごとに新しいコンテナ)なので、ほかとは取り合わない - $K -n infisical port-forward svc/infisical 18080:8080 > /tmp/pf.log 2>&1 & - PF=$! - trap 'kill $PF 2>/dev/null || true' EXIT - for _ in $(seq 1 30); do curl -fsS -o /dev/null http://127.0.0.1:18080/api/status 2>/dev/null && break; sleep 1; done - curl -fsS -o /dev/null http://127.0.0.1:18080/api/status || { cat /tmp/pf.log >&2; echo "port-forward が繋がらない" >&2; exit 1; } - CA="$CA" TOKEN="$TOKEN" bun "$(dirname "$0")/infisical-ca.mjs" "${2:-}" - if [ "${2:-}" = apply ]; then - # operator は失敗が続くと間を空けるので、蹴り直さないとしばらく戻らない - $K -n infisical-operator rollout restart deploy/infisical-secre-controller-manager - $K -n infisical-operator rollout status deploy/infisical-secre-controller-manager --timeout=120s - fi ;; - *) echo "usage: infisical.sh db | ca [apply]" >&2; exit 1 ;; -esac diff --git a/renovate.json b/renovate.json index 96ec07f4..67940aa7 100644 --- a/renovate.json +++ b/renovate.json @@ -160,10 +160,9 @@ ] }, { - "description": "argo-cd と infisical の chart と、その値で上書きしているイメージは自動マージしない(壊れると直す道具ごと止まる)。当てるのは render.sh → apply-config → upgrade-k8s。", + "description": "argo-cd の chart と、その値で上書きしているイメージは自動マージしない(壊れると直す道具ごと止まる)。当てるのは render.sh → apply-config → upgrade-k8s。", "matchFileNames": [ - "bootstrap/argocd-values.sops.yaml", - "bootstrap/infisical-values.sops.yaml" + "bootstrap/argocd-values.sops.yaml" ], "groupName": "bootstrap charts", "groupSlug": "bootstrap-charts", diff --git a/tools/Dockerfile b/tools/Dockerfile index 52b8a4ed..2f5e5006 100644 --- a/tools/Dockerfile +++ b/tools/Dockerfile @@ -17,8 +17,6 @@ ARG TALOSCTL_VERSION=v1.15.0-alpha.2 ARG HELM_VERSION=v4.3.1 # renovate: datasource=github-releases depName=getsops/sops ARG SOPS_VERSION=v3.13.3 -# renovate: datasource=github-releases depName=Infisical/cli -ARG INFISICAL_VERSION=v0.44.11 # renovate: datasource=github-releases depName=restic/restic ARG RESTIC_VERSION=v0.19.1 # renovate: datasource=github-releases depName=pulumi/pulumi @@ -45,8 +43,6 @@ RUN a=$TARGETARCH; p=$a; [ "$a" = amd64 ] && p=x64; \ ok "https://get.helm.sh/helm-${HELM_VERSION}-linux-$a.tar.gz.sha256sum" "helm-${HELM_VERSION}-linux-$a.tar.gz"; \ get "$gh/getsops/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.linux.$a"; \ ok "$gh/getsops/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.checksums.txt" "sops-${SOPS_VERSION}.linux.$a"; \ - get "$gh/Infisical/cli/releases/download/${INFISICAL_VERSION}/cli_${INFISICAL_VERSION#v}_linux_$a.tar.gz"; \ - ok "$gh/Infisical/cli/releases/download/${INFISICAL_VERSION}/checksums.txt" "cli_${INFISICAL_VERSION#v}_linux_$a.tar.gz"; \ get "$gh/restic/restic/releases/download/${RESTIC_VERSION}/restic_${RESTIC_VERSION#v}_linux_$a.bz2"; \ ok "$gh/restic/restic/releases/download/${RESTIC_VERSION}/SHA256SUMS" "restic_${RESTIC_VERSION#v}_linux_$a.bz2"; \ get "$gh/pulumi/pulumi/releases/download/${PULUMI_VERSION}/pulumi-${PULUMI_VERSION}-linux-$p.tar.gz"; \ @@ -57,7 +53,6 @@ RUN a=$TARGETARCH; p=$a; [ "$a" = amd64 ] && p=x64; \ install -m 755 "talosctl-linux-$a" /usr/local/libexec/gitops/talosctl; \ tar -xzf "helm-${HELM_VERSION}-linux-$a.tar.gz" --strip-components=1 -C /usr/local/libexec/gitops "linux-$a/helm"; \ install -m 755 "sops-${SOPS_VERSION}.linux.$a" /usr/local/bin/sops; \ - tar -xzf "cli_${INFISICAL_VERSION#v}_linux_$a.tar.gz" -C /usr/local/bin infisical; \ bunzip2 -c "restic_${RESTIC_VERSION#v}_linux_$a.bz2" > /usr/local/bin/restic; \ # Pulumi は本体と bun・Node.js の言語ホストだけ(bun が無いと pulumi install で SDK を作れない) tar -xzf "pulumi-${PULUMI_VERSION}-linux-$p.tar.gz" --strip-components=1 -C /usr/local/bin pulumi/pulumi pulumi/pulumi-language-bun pulumi/pulumi-language-nodejs; \ diff --git a/tools/README.md b/tools/README.md index bd9b21d7..9adeb6f2 100644 --- a/tools/README.md +++ b/tools/README.md @@ -5,8 +5,7 @@ ```powershell tools/t.ps1 cf r2 buckets list -tools/t.ps1 sops -d bootstrap/infisical-values.sops.yaml -$env:INFISICAL_PROFILE = 'info@doany.io--doa'; tools/t.ps1 infisical secrets --env prod --path /matrix/matrix +tools/t.ps1 sops edit deploy/matrix/secrets.sops.yaml tools/t.ps1 kubectl get pods -A # クラスタは読み取り用で繋ぐ tools/t.ps1 --admin kubectl apply -f x.yaml # 書くときだけ管理者(下の「クラスタの資格情報」) tools/t.ps1 # 引数なしならシェルに入る @@ -19,8 +18,7 @@ tools/t.ps1 # 引数なしならシェルに入る | 入っているもの | 用途 | | --- | --- | | cf | Cloudflare(R2 など) | -| sops / age | `bootstrap/` の暗号化したファイル | -| infisical | 秘密の読み書き | +| sops / age | 暗号化したファイル(`*.sops.yaml`。秘密の読み書き) | | kubectl / talosctl / helm | クラスタ(資格情報は下の「クラスタの資格情報」) | | restic | R2 のバックアップの中身を見る・戻す | | pulumi / bun | `pulumi/`(ふだんは CI が流す) | @@ -32,8 +30,8 @@ tools/t.ps1 # 引数なしならシェルに入る - [../compose.yaml](../compose.yaml) の `tools` を `wslc-compose run` で動かす(`winget` で入る wslc-compose)。リポジトリは `/repo` に置く - **ログインと鍵はリポジトリの `.home/` に置く**(コンテナの中の `HOME`。`.gitignore` 済み)。手元のホームは汚さない。 - cf のログイン(`.config/cloudflare`)、sops の age 鍵(`.config/sops/age/keys.txt`)、infisical のログイン - (`.infisical` と `infisical-keyring`)、pulumi(`.pulumi`)がここに入る。 + cf のログイン(`.config/cloudflare`)、sops の age 鍵(`.config/sops/age/keys.txt`)、 + pulumi(`.pulumi`)がここに入る。 クラスタの資格情報はここにも置かない(下の「クラスタの資格情報」)。 `.home/` は本人だけが読める ACL にする - **`git clean -fdx` で消える**(無視しているファイルも消すため)。age 鍵は `recovery/sops-age.key.age` から戻せる @@ -77,8 +75,7 @@ tools/t.ps1 # 引数なしならシェルに入る ## wslc の制限 - **wslc は host ネットワークに対応していない。** ブラウザのログインは手元の `localhost:<ポート>` に結果を送ってくるので、 - `infisical login` のときだけ t.ps1 が同じポートで待ち受けて、`wslc exec` の curl でコンテナの中の CLI に渡す。 - `cf auth login` には対応していない + その方式のログイン(`cf auth login` など)はコンテナの中では使えない - uid は指定しない(Windows のファイルに持ち主の uid は無い) - **標準入力が NUL のところ(Claude Code のツールなど)では、`wslc-compose run` がつなぐ標準入力が無効なハンドルになる。** 1 秒ほど以上かかるコマンドの出力が落ちて `ERROR_INVALID_HANDLE` で終わるので、`t.ps1` はそのときだけ空のパイプを渡す。 @@ -88,10 +85,10 @@ tools/t.ps1 # 引数なしならシェルに入る ## 入れるものの確かめ方 -**全部、公式が出しているチェックサムと照らし合わせてから入れる**。sops は age 鍵を、infisical は秘密を扱うので、 +**全部、公式が出しているチェックサムと照らし合わせてから入れる**。sops は age 鍵と秘密を扱うので、 取得元やミラーで差し替えられても気づけるようにする。一致しなければ組み立てごと止まる。 -- kubectl・talosctl・helm・sops・infisical・restic・pulumi: 各リリースのチェックサムのファイル +- kubectl・talosctl・helm・sops・restic・pulumi: 各リリースのチェックサムのファイル - age: チェックサムのファイルを出していない(sigsum の証明だけ)ので、Go のモジュールとして `go install` し、 Go の公開のチェックサムの台帳(sum.golang.org)で確かめる - cf: npm の integrity diff --git a/tools/t.ps1 b/tools/t.ps1 index 48bf3e0d..ba4834a2 100644 --- a/tools/t.ps1 +++ b/tools/t.ps1 @@ -1,12 +1,12 @@ # 運用のコマンドを wslc (WSL コンテナ) で動かす。中身は compose.yaml の tools を wslc-compose run する。例: -# tools/t.ps1 infisical login --domain https://il.doany.io/api -# tools/t.ps1 sops -d bootstrap/infisical-values.sops.yaml +# tools/t.ps1 sops edit deploy/matrix/secrets.sops.yaml +# tools/t.ps1 sops -d bootstrap/cert-manager-values.sops.yaml # tools/t.ps1 kubectl get pods -A # クラスタは読み取り用で繋ぐ # tools/t.ps1 --admin kubectl apply -f x # 書くときだけ管理者 (tools/README.md「クラスタの資格情報」) # 引数が無ければシェルに入る。Dockerfile を変えたら次の実行で作り直す (変わっていなければキャッシュで一瞬)。 # -# wslc はネットワークを host にできないので、infisical login のときだけブラウザの戻りを中継する (Start-LoginRelay)。 -# cf auth login は未対応。uid は指定しない (Windows のファイルに持ち主の uid は無い)。 +# wslc はネットワークを host にできないので、ブラウザから戻ってくる方式のログイン (cf auth login など) は使えない。 +# uid は指定しない (Windows のファイルに持ち主の uid は無い)。 # 5.1 でも読めるように BOM 付きの UTF-8 にする (無いと Shift_JIS と読まれ構文が壊れる)。文字列は ASCII だけ $ErrorActionPreference = 'Stop' $root = Split-Path $PSScriptRoot @@ -22,89 +22,6 @@ if (-not (Test-Path $home_)) { $build = wslc image build --tag $image --progress quiet (Join-Path $root 'tools') 2>&1 if ($LASTEXITCODE -ne 0) { $build | Write-Host; throw "tools: image build failed ($LASTEXITCODE)" } -# infisical login (ブラウザ方式) の中継。コンテナの中の CLI が待ち受けるポートを手元でも受け、wslc exec の curl で渡す。 -# 受け付けるのはログイン先 ($origin) からの POST / OPTIONS だけ。リクエストの値はコマンドに埋め込まない -function Start-LoginRelay($name, $origin) { - Start-Job -ArgumentList $name, $origin -ScriptBlock { - param($name, $origin) - $port = $null - for ($i = 0; $i -lt 240 -and -not $port; $i++) { - Start-Sleep -Milliseconds 500 - # /proc/net/tcp: 127.0.0.1 (0100007F) で LISTEN (0A) しているポート - foreach ($l in (wslc exec $name cat /proc/net/tcp 2>$null)) { - $f = -split $l - if ($f.Count -gt 3 -and $f[1] -like '0100007F:*' -and $f[3] -eq '0A') { $port = [Convert]::ToInt32($f[1].Split(':')[1], 16) } - } - } - if (-not $port) { 'relay: the login callback port was not found in the container'; return } - try { - $listener = New-Object Net.Sockets.TcpListener ([Net.IPAddress]::Loopback), $port - $listener.Start() - } catch { "relay: cannot listen on 127.0.0.1:${port}: $_"; return } - while ($true) { - $client = $listener.AcceptTcpClient() - # 投機的な空の接続で止まらないように - $client.ReceiveTimeout = 10000 - try { - $s = $client.GetStream() - $buf = New-Object byte[] 65536 - $data = New-Object IO.MemoryStream - $headerEnd = -1 - while ($headerEnd -lt 0) { - $n = $s.Read($buf, 0, $buf.Length) - if ($n -le 0) { break } - $data.Write($buf, 0, $n) - $headerEnd = [Text.Encoding]::ASCII.GetString($data.ToArray()).IndexOf("`r`n`r`n") - } - if ($headerEnd -lt 0) { continue } - $head = [Text.Encoding]::ASCII.GetString($data.ToArray(), 0, $headerEnd) - $lines = $head -split "`r`n" - $method = ($lines[0] -split ' ')[0] - $h = @{} - foreach ($l in $lines[1..($lines.Count - 1)]) { $k, $v = $l -split ':\s*', 2; $h[$k.ToLower()] = $v } - $len = if ($h['content-length']) { [int]$h['content-length'] } else { 0 } - while ($data.Length - $headerEnd - 4 -lt $len) { - $n = $s.Read($buf, 0, $buf.Length) - if ($n -le 0) { break } - $data.Write($buf, 0, $n) - } - $body = New-Object byte[] $len - [Array]::Copy($data.ToArray(), $headerEnd + 4, $body, 0, $len) - $cors = "Access-Control-Allow-Origin: $origin`r`nAccess-Control-Allow-Credentials: true`r`n" + - "Access-Control-Allow-Methods: POST, OPTIONS`r`nAccess-Control-Allow-Headers: Content-Type`r`n" + - "Access-Control-Allow-Private-Network: true`r`nVary: Origin`r`n" - if ($h['origin'] -ne $origin) { - $out = [Text.Encoding]::ASCII.GetBytes("HTTP/1.1 403 Forbidden`r`nContent-Length: 0`r`nConnection: close`r`n`r`n") - } elseif ($method -eq 'OPTIONS') { - $resp = "HTTP/1.1 204 No Content`r`n$cors" + "Content-Length: 0`r`nConnection: close`r`n`r`n" - $out = [Text.Encoding]::ASCII.GetBytes($resp) - } elseif ($method -ne 'POST') { - $out = [Text.Encoding]::ASCII.GetBytes("HTTP/1.1 405 Method Not Allowed`r`nContent-Length: 0`r`nConnection: close`r`n`r`n") - } else { - # 埋め込むのは自分で決めた $name / $origin / $port だけ - $psi = New-Object Diagnostics.ProcessStartInfo 'wslc', ("exec --interactive $name curl -s -o /dev/null -w %{http_code} " + - "-X POST -H `"Content-Type: application/json`" -H `"Origin: $origin`" --data-binary @- http://127.0.0.1:$port/") - $psi.UseShellExecute = $false - $psi.RedirectStandardInput = $true - $psi.RedirectStandardOutput = $true - $p = [Diagnostics.Process]::Start($psi) - $p.StandardInput.BaseStream.Write($body, 0, $body.Length) - $p.StandardInput.Close() - $code = $p.StandardOutput.ReadToEnd().Trim() - $p.WaitForExit() - if ($code -notmatch '^\d{3}$') { $code = '502' } - $resp = "HTTP/1.1 $code Relayed`r`n$cors" + "Content-Length: 0`r`nConnection: close`r`n`r`n" - $out = [Text.Encoding]::ASCII.GetBytes($resp) - } - $s.Write($out, 0, $out.Length) - } catch { - } finally { - $client.Close() - } - } - } -} - # 設定は compose.yaml の tools。パイプやリダイレクトのときは TTY を付けない (-T)。 # パイプしたものは標準入力でなく $input に来るので下で流し直す。 # Test-NulStdin: 標準入力が NUL (FILE_TYPE_CHAR) か。ファイルやパイプからの入力は NUL と扱わない @@ -129,43 +46,19 @@ if ($rest.Count -and $rest[0] -eq '--admin') { # 1 つのときも配列にする (でないと $cmd[0] が 1 文字目になる) $cmd = @(if ($rest.Count) { $rest } else { 'bash' }) -$relay = $null -$browserLogin = $cmd.Count -ge 2 -and $cmd[0] -eq 'infisical' -and $cmd[1] -eq 'login' -and - -not ($cmd | Where-Object { $_ -match '^(-i|--interactive|--method)' }) -if ($browserLogin) { - # ブラウザのリクエストの Origin はログイン先 (--domain から /api を除いたもの) - $domain = 'https://app.infisical.com' - for ($i = 0; $i -lt $cmd.Count; $i++) { - if ($cmd[$i] -eq '--domain' -and $i + 1 -lt $cmd.Count) { $domain = $cmd[$i + 1] } - elseif ($cmd[$i] -like '--domain=*') { $domain = $cmd[$i].Substring(9) } - } - $origin = ([Uri]$domain).GetLeftPart([UriPartial]::Authority) - $name = "gitops-tools-login-$PID" - $opts += '--name', $name - $relay = Start-LoginRelay $name $origin -} -try { - $compose = Join-Path $root 'compose.yaml' - if ($piped) { - # ネイティブに流すと CRLF になり 5.1 では日本語が化けるので、LF の UTF-8 を base64 で渡す - $text = (@($input) -join "`n") + "`n" - $b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($text)) - # 7.3 より前 (5.1 も) は引数の中の " をネイティブのコマンドに渡すときにエスケープしないので、自分でする - $wrap = 'base64 -d -i | "$@"' - if ($PSVersionTable.PSVersion -lt [version]'7.3') { $wrap = $wrap.Replace('"', '\"') } - $b64 | wslc-compose --file $compose run @opts tools sh -c $wrap sh @cmd - } elseif (Test-NulStdin) { - # 標準入力が NUL だと wslc-compose run の出力が落ちて ERROR_INVALID_HANDLE で終わる。空のパイプに置き換える - @() | wslc-compose --file $compose run @opts tools @cmd - } else { - wslc-compose --file $compose run @opts tools @cmd - } - $code = $LASTEXITCODE -} finally { - if ($relay) { - Stop-Job $relay - Receive-Job $relay -ErrorAction SilentlyContinue | ForEach-Object { Write-Warning $_ } - Remove-Job $relay -Force - } +$compose = Join-Path $root 'compose.yaml' +if ($piped) { + # ネイティブに流すと CRLF になり 5.1 では日本語が化けるので、LF の UTF-8 を base64 で渡す + $text = (@($input) -join "`n") + "`n" + $b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($text)) + # 7.3 より前 (5.1 も) は引数の中の " をネイティブのコマンドに渡すときにエスケープしないので、自分でする + $wrap = 'base64 -d -i | "$@"' + if ($PSVersionTable.PSVersion -lt [version]'7.3') { $wrap = $wrap.Replace('"', '\"') } + $b64 | wslc-compose --file $compose run @opts tools sh -c $wrap sh @cmd +} elseif (Test-NulStdin) { + # 標準入力が NUL だと wslc-compose run の出力が落ちて ERROR_INVALID_HANDLE で終わる。空のパイプに置き換える + @() | wslc-compose --file $compose run @opts tools @cmd +} else { + wslc-compose --file $compose run @opts tools @cmd } -exit $code +exit $LASTEXITCODE