Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ talos/secrets.plain.yaml
# **平文のまま置かないこと。** talos/registries.yaml は SOPS 済みのものをコミットする
# (talos/README.md「fj.doany.io の資格情報」)。書きかけの平文はこの名前に逃がす。
talos/registries.plain.yaml
# 平文の kubeconfig / talosconfig も置かない。talos/kubeconfig.yaml は SOPS 済みのものをコミットする
# (tools/README.md「クラスタの資格情報」)。talosctl kubeconfig と gen config がこの名前で書き出す
kubeconfig
talosconfig

# pulumi/ の道具(bun.lock で固定してある)
pulumi/node_modules/
Expand Down
5 changes: 5 additions & 0 deletions .sops.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ creation_rules:
# 選り分ける意味が無い(k3s 期の /etc/rancher/k3s/registries.yaml の移し先)。
- path_regex: ^talos/registries\.yaml$
age: age1sgnnkn67wpt4wgjsff4c36w3cmjkk557ng504e4aczx2lzls9y6ssy0x9r
# talos/kubeconfig.yaml(管理者。`talosctl kubeconfig` の出力)も丸ごと。
# 中身は証明書と鍵がほとんどで、選り分ける意味が無い。tools/t.ps1 の kubectl がその場で復号して使う
# (tools/README.md「クラスタの資格情報」)。
- path_regex: ^talos/kubeconfig\.yaml$
age: age1sgnnkn67wpt4wgjsff4c36w3cmjkk557ng504e4aczx2lzls9y6ssy0x9r
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Argo CD が git を読む GitHub App 鍵、cert-manager の Cloudflare トーク
```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/secrets.yaml | tools/t.ps1 kubectl apply -f -
tools/t.ps1 sops -d bootstrap/infisical/secrets.yaml | tools/t.ps1 --admin kubectl apply -f -
```

## ホスト名の付け方
Expand Down
2 changes: 1 addition & 1 deletion apps/renovate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ GitHub の schedule は 15〜40 分遅れ、丸ごと飛ばされることもあ
tools/t.ps1 kubectl -n renovate get jobs
tools/t.ps1 kubectl -n renovate logs job/<名前>
# 待たずに 1 回回す
tools/t.ps1 kubectl -n renovate create job --from=cronjob/renovate-5ym renovate-5ym-manual
tools/t.ps1 --admin kubectl -n renovate create job --from=cronjob/renovate-5ym renovate-5ym-manual
```

## 秘密
Expand Down
3 changes: 3 additions & 0 deletions talos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ talosctl -n 10.0.0.2 -e 10.0.0.2 --talosconfig /tmp/talos-config/talosconfig boo
talosctl -n 10.0.0.2 -e 10.0.0.2 --talosconfig /tmp/talos-config/talosconfig kubeconfig
```

ふだんの kubectl / talosctl は `tools/t.ps1` が資格情報を用意する。管理者の kubeconfig は SOPS で
`talos/kubeconfig.yaml` に置く(手順は [../tools/README.md](../tools/README.md)「クラスタの資格情報」)。

`clusterconfig/` と平文の秘密は `.gitignore` 済み。

**作り直すとき**(まっさらから組む場合だけ):
Expand Down
16 changes: 11 additions & 5 deletions tools/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ ARG PULUMI_VERSION=v3.270.0
ARG CF_VERSION=1.0.0-beta.18

RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl bzip2 git openssh-client less \
&& apt-get install -y --no-install-recommends ca-certificates curl bzip2 git openssh-client less openssl \
&& rm -rf /var/lib/apt/lists/*

# **入れるものは全部、公式が出しているチェックサムと照らし合わせる**(2026-10-07)。sops は age 鍵を、infisical は
Expand All @@ -55,16 +55,18 @@ RUN a=$TARGETARCH; p=$a; [ "$a" = amd64 ] && p=x64; \
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"; \
ok "$gh/pulumi/pulumi/releases/download/${PULUMI_VERSION}/pulumi-${PULUMI_VERSION#v}-checksums.txt" "pulumi-${PULUMI_VERSION}-linux-$p.tar.gz"; \
install -m 755 kubectl /usr/local/bin/kubectl; \
install -m 755 "talosctl-linux-$a" /usr/local/bin/talosctl; \
tar -xzf "helm-${HELM_VERSION}-linux-$a.tar.gz" --strip-components=1 -C /usr/local/bin "linux-$a/helm"; \
# kubectl・talosctl・helm の本物は libexec に置く。/usr/local/bin のほうは creds.sh(下)
mkdir -p /usr/local/libexec/gitops; \
install -m 755 kubectl /usr/local/libexec/gitops/kubectl; \
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 の言語ホストだけ。ほかの言語のぶんは 200MB 近いので入れない。
# bun の言語ホストが無いと runtime: bun のプロジェクトで SDK を作れない (pulumi install。2026-10-07 まで抜けていた)
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; \
chmod +x /usr/local/bin/*; \
chmod +x /usr/local/bin/* /usr/local/libexec/gitops/*; \
cd / && rm -r /tmp/dl

# age はチェックサムのファイルを出していない(sigsum の証明だけ)。Go のモジュールとして入れ、Go の公開の
Expand All @@ -74,5 +76,9 @@ COPY --from=age /go/bin/age /go/bin/age-keygen /usr/local/bin/
COPY --from=bun /usr/local/bin/bun /usr/local/bin/bun
RUN npm install -g "cf@${CF_VERSION}" && npm cache clean --force

# kubectl・talosctl・helm はクラスタに繋ぐときだけ、暗号化した資格情報を tmpfs に戻して渡す(creds.sh)
COPY --chmod=755 creds.sh /usr/local/bin/creds.sh
RUN for t in kubectl talosctl helm; do ln -s creds.sh "/usr/local/bin/$t"; done

# HOME はリポジトリの .home/(compose.yaml)
WORKDIR /repo
46 changes: 42 additions & 4 deletions tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Linux 用の `tools/t`(docker compose)は、手元が Windows に移ったので
tools/t.ps1 cf r2 buckets list
tools/t.ps1 sops -d bootstrap/infisical/secrets.yaml
$env:INFISICAL_PROFILE = 'info@doany.io--doa'; tools/t.ps1 infisical secrets --env prod --path /matrix/matrix
tools/t.ps1 kubectl get pods -A # クラスタは読み取り用で繋ぐ
tools/t.ps1 --admin kubectl apply -f x.yaml # 書くときだけ管理者(下の「クラスタの資格情報」)
tools/t.ps1 # 引数なしならシェルに入る
```

Expand All @@ -17,7 +19,7 @@ tools/t.ps1 # 引数なしならシェルに入る
| cf | Cloudflare(R2 など) |
| sops / age | `bootstrap/` と `talos/` の暗号化したファイル |
| infisical | 秘密の読み書き |
| kubectl / talosctl / helm | クラスタ(Talos の移行後は手元から直接) |
| kubectl / talosctl / helm | クラスタ(資格情報は下の「クラスタの資格情報」) |
| restic | R2 のバックアップの中身を見る・戻す |
| pulumi / bun | `pulumi/`(ふだんは CI が流す) |

Expand All @@ -29,20 +31,56 @@ 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`)、kubectl / talosctl(`.kube` / `.talos`)がここに入る。
kubeconfig と talosconfig は今は無い(k3s の間はノードに ssh して kubectl を使うため。Talos に移ったら置く)。
(`.infisical` と `infisical-keyring`)、pulumi(`.pulumi`)がここに入る。
クラスタの資格情報はここにも置かない(下の「クラスタの資格情報」)。
`.home/` は本人だけが読める ACL にする
- **`git clean -fdx` で消える**(無視しているファイルも消すため)。age 鍵は `recovery/sops-age.key.age` から戻せる
(README「Infisical より下の層」)、ほかはログインし直せば戻る
- Dockerfile を変えると、次の `tools/t.ps1` でイメージを作り直す(変わっていなければキャッシュで一瞬)
- パイプやリダイレクトのときは TTY を付けない(`tools/t.ps1` が見分ける)
- PowerShell でパイプしたもの(`tools/t.ps1 sops -d … | tools/t.ps1 kubectl apply -f -`)は、`t.ps1` がコンテナの標準入力に流し直す。
- PowerShell でパイプしたもの(`tools/t.ps1 sops -d … | tools/t.ps1 --admin kubectl apply -f -`)は、`t.ps1` がコンテナの標準入力に流し直す。
行を LF でつないだ UTF-8 にして渡す(PowerShell のまま流すと CRLF になり、5.1 では日本語が `?` に化けるため、
base64 で包んでコンテナの中で戻す)。PowerShell のパイプは行(文字列)単位なので、バイナリは通らない。
バイナリはファイルに書いてリポジトリの中から読ませる
- `t.ps1` は **BOM 付きの UTF-8** にしてある。BOM が無いと Windows PowerShell 5.1 が Shift_JIS として読み、
日本語のコメントで構文が壊れる(2026-10-07 まで 5.1 では動かなかった)

## クラスタの資格情報

**kubectl・talosctl・helm は、クラスタに繋ぐときだけ資格情報をコンテナの中で作るか復号して渡す**(2026-10-17。
[creds.sh](creds.sh))。置き場はコンテナの `/dev/shm`(tmpfs)で、コンテナと一緒に消える。
**平文は手元のディスクにも `.home/` にも残らない。** age 鍵(上の「仕組み」)が無いと止まる。

**既定は読み取り用。管理者は `tools/t.ps1 --admin …` と書いたときだけ。** 書く操作(`apply`・`delete`・
`create job` など)は `--admin` を付ける。`--admin` はシェルにも効く(`tools/t.ps1 --admin`)。
**誤操作を防ぐための分け方で、権限の境界ではない**(どちらも同じ age 鍵から作れる)。

| | 読み取り用(既定) | 管理者(`--admin`) |
| --- | --- | --- |
| kubectl / helm | その場で作る。`talos/secrets.yaml` の Kubernetes の CA で、グループ `entra:viewer` の証明書(1 日で切れる) | `talos/kubeconfig.yaml`(SOPS 済み)を復号する |
| talosctl | その場で作る。Talos の CA で `os:reader` の証明書(1 日で切れる) | 同じく `os:admin` |

- 読み取り用の権限は [`../bootstrap/apiserver/rbac.yaml`](../bootstrap/apiserver/rbac.yaml) の `entra:viewer`
(組み込みの `view` と `readonly-extra`。ログは読めて、Secret は読めない)。Headlamp で Entra の viewer が持つものと同じ
- **helm はリリースの情報を Secret に持つ**ので、`helm list` や `helm status` も `--admin` を付ける
- **talosconfig は保存しない**([../docs/decisions.md](../docs/decisions.md)「手で持つもの」)。`secrets.yaml` から毎回作る
- 管理者の kubeconfig だけはファイルで持つ。Talos が出す正式なもの(`talosctl kubeconfig`)を使い、証明書は 1 年で切れる。
中身は `secrets.yaml` の CA で作れるものなので、置いても開けられる範囲は今と変わらない
- 行き先はどれも `10.0.0.2`(`talos/render.sh` と同じ)。`KUBECONFIG` / `TALOSCONFIG` や `--kubeconfig` / `--talosconfig` /
`--server` を自分で渡したときは何もしない。繋がないもの(`kubectl kustomize`、`helm template`、`talosctl gen`、`--client` など)も同じ

**管理者の kubeconfig の入れ方**(作り直すときも同じ。切れる前に):

```powershell
tools/t.ps1 --admin sh -c 'talosctl kubeconfig /dev/shm/k && sops -e --filename-override talos/kubeconfig.yaml /dev/shm/k > talos/kubeconfig.yaml'
tools/t.ps1 --admin kubectl get nodes # 確かめる
git add talos/kubeconfig.yaml # 暗号化済み(`sops:` がある)ことを見てからコミットする
```

`talosctl kubeconfig` が書き出す先も暗号化の入力も `/dev/shm` なので、平文は一度もディスクに出ない。
規則は [`../.sops.yaml`](../.sops.yaml)。平文の `kubeconfig` / `talosconfig` という名前のファイルは `.gitignore` 済みで、
`creds.sh` も暗号化されていない `talos/kubeconfig.yaml` は使わずに止まる。

## wslc の制限

- **wslc は host ネットワークに対応していない。** ブラウザのログインは手元の `localhost:<ポート>` に結果を送ってくるので、
Expand Down
117 changes: 117 additions & 0 deletions tools/creds.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/bin/sh
# kubectl / talosctl / helm の入口(tools/README.md「クラスタの資格情報」)。本物は /usr/local/libexec/gitops/ にあり、
# このスクリプトがその名前で呼ばれる。クラスタに繋ぐときだけ資格情報をコンテナの /dev/shm(tmpfs)に作り、
# KUBECONFIG / TALOSCONFIG で渡す。**平文はディスクに書かない。** /dev/shm はコンテナと一緒に消える(t.ps1 は --rm)。
#
# 既定は読み取り用。管理者は `tools/t.ps1 --admin …`(GITOPS_ADMIN=1)のときだけ。
# kubectl / helm 読み取り用は talos/secrets.yaml の CA でその場で証明書を作る(1 日で切れる)。
# 管理者は talos/kubeconfig.yaml(SOPS 済み。talosctl kubeconfig の出力)を復号する
# talosctl 同じく CA でその場で作る(os:reader / os:admin)。talosconfig は保存しない
# (docs/decisions.md「手で持つもの」)
set -eu
tool=${0##*/}
real=/usr/local/libexec/gitops/$tool
shm=/dev/shm/gitops

die() { echo "$tool: $*" >&2; exit 1; }

# 最初の引数らしいもの(フラグを飛ばす)。`kubectl -n x get` だと x を拾うが、そのときは「繋ぐ」側に倒れるだけ
# (helm も「繋がないと分かっているもの」以外は繋ぐ側にしてある)。`--` より後ろはコマンドの引数なので見ない
sub=
for a in "$@"; do case $a in --) break ;; -*) ;; *) sub=$a; break ;; esac; done

# 繋がないもの(版を見る・描くだけ)と、資格情報を自分で渡しているものはそのまま通す。CI(tools.yml)もここを通る
pass() { exec "$real" "$@"; }
for a in "$@"; do
case $a in
--) break ;;
-h | --help | --client | --kubeconfig | --kubeconfig=* | --talosconfig | --talosconfig=* | -s | --server | --server=*) pass "$@" ;;
esac
done
case $tool in
kubectl)
[ -z "${KUBECONFIG:-}" ] || pass "$@"
case $sub in '' | kustomize | completion | help | plugin | options) pass "$@" ;; esac
;;
helm)
[ -z "${KUBECONFIG:-}" ] || pass "$@"
case $sub in '' | template | lint | package | create | show | inspect | pull | fetch | push | repo | registry | search | dependency | dep | plugin | env | version | completion | verify | help) pass "$@" ;; esac
;;
talosctl)
[ -z "${TALOSCONFIG:-}" ] || pass "$@"
case $sub in '' | gen | completion | help | docs | validate) pass "$@" ;; esac
;;
esac

if [ "${GITOPS_ADMIN:-}" = 1 ]; then role=admin; else role=readonly; fi
mkdir -p -m 700 "$shm"
cd /repo
nokey() { [ -s ~/.config/sops/age/keys.txt ] || die "age の鍵が無い(README「Infisical より下の層」の 1 行目で .home に置く)"; }

# talos/secrets.yaml の CA($1 = os か k8s)で、件名 $2 のクライアント証明書をその場で作り、$3 に置く。
# 鍵 $4(ed25519 / EC)。1 日で切れる。ノードの時計が少し遅れていても通るように 5 分前から有効にする。
# 作業中は CA の鍵も $w(tmpfs)に出るので、作り終えたらすぐ消す。結果は $3.crt / $3.key / $3.ca
mint() {
nokey
w=$(mktemp -d "$shm/w.XXXXXX")
# `( … ) || …` だと中の set -e が効かないので、終了コードを受けてから見る。パイプは最後の終了コードしか
# 返さないので、復号したものは一度ファイルに出す
set +e
(
set -e
umask 077
for k in crt key; do
sops -d --extract "[\"certs\"][\"$1\"][\"$k\"]" talos/secrets.yaml >"$w/ca.$k.b64"
base64 -d "$w/ca.$k.b64" >"$w/ca.$k"
done
# Talos は ed25519 の鍵に独自の見出しを付けている。中身は PKCS#8 なので見出しだけ直せば openssl が読める
sed -i 's/ED25519 PRIVATE KEY/PRIVATE KEY/' "$w/ca.key"
case $4 in
ed25519) openssl genpkey -algorithm ed25519 -out "$w/c.key" ;;
*) openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out "$w/c.key" ;;
esac
printf 'keyUsage=critical,digitalSignature\nextendedKeyUsage=clientAuth\n' >"$w/ext"
openssl req -new -key "$w/c.key" -subj "$2" -out "$w/c.csr" 2>/dev/null
openssl x509 -req -in "$w/c.csr" -CA "$w/ca.crt" -CAkey "$w/ca.key" -extfile "$w/ext" \
-not_before "$(date -u -d '-5 min' +%Y%m%d%H%M%SZ)" -days 1 -out "$w/c.crt" 2>/dev/null
for k in crt key; do base64 -w0 "$w/c.$k" >"$3.$k"; done
base64 -w0 "$w/ca.crt" >"$3.ca"
[ -s "$3.crt" ] && [ -s "$3.key" ] && [ -s "$3.ca" ]
)
rc=$?
set -e
rm -rf "$w"
[ $rc = 0 ] || { rm -f "$3".*; die "証明書を作れなかった(talos/secrets.yaml を age の鍵で開けるか)"; }
}

# 名前と行き先は talos/render.sh の gen config と同じ(doany、https://10.0.0.2:6443)
if [ "$tool" = talosctl ]; then
out=$shm/talosconfig-$role
if [ ! -s "$out" ]; then
if [ $role = admin ]; then os=os:admin; else os=os:reader; fi
mint os "/O=$os" "$out.c.$$" ed25519
(umask 077 && printf 'context: doany\ncontexts:\n doany:\n endpoints: [10.0.0.2]\n nodes: [10.0.0.2]\n ca: %s\n crt: %s\n key: %s\n' \
"$(cat "$out.c.$$.ca")" "$(cat "$out.c.$$.crt")" "$(cat "$out.c.$$.key")" >"$out.$$" && mv "$out.$$" "$out")
rm -f "$out.c.$$".*
fi
export TALOSCONFIG="$out"
exec "$real" "$@"
fi

out=$shm/kubeconfig-$role
if [ ! -s "$out" ] && [ $role = readonly ]; then
# 読み取り用は bootstrap/apiserver/rbac.yaml の entra:viewer(組み込みの view + readonly-extra。Secret は読めない)。
# Headlamp で Entra の viewer が持つ権限と同じにするため、同じグループ名を使う
mint k8s "/O=entra:viewer/CN=tools-readonly" "$out.c.$$" ec
(umask 077 && printf 'apiVersion: v1\nkind: Config\nclusters:\n- name: doany\n cluster:\n server: https://10.0.0.2:6443\n certificate-authority-data: %s\ncontexts:\n- name: readonly@doany\n context:\n cluster: doany\n user: readonly\ncurrent-context: readonly@doany\nusers:\n- name: readonly\n user:\n client-certificate-data: %s\n client-key-data: %s\n' \
"$(cat "$out.c.$$.ca")" "$(cat "$out.c.$$.crt")" "$(cat "$out.c.$$.key")" >"$out.$$" && mv "$out.$$" "$out")
rm -f "$out.c.$$".*
elif [ ! -s "$out" ]; then
f=talos/kubeconfig.yaml
[ -f $f ] || die "$f が無い。作り方は tools/README.md「クラスタの資格情報」"
grep -q '^sops:' $f || die "$f が暗号化されていない。git に入れる前に tools/README.md の手順で暗号化する"
nokey
(umask 077 && sops -d $f >"$out.$$" && mv "$out.$$" "$out") || { rm -f "$out.$$"; die "$f を復号できなかった"; }
fi
export KUBECONFIG="$out"
exec "$real" "$@"
Loading
Loading