Author SHA1 Message Date
panxiao81 57b13387cb ci: 输出 kind 节点准备调试日志
kind-on-kata-smoke / smoke (push) Failing after 23m50s
2026-09-14 16:16:36 +00:00
panxiao81 1deb024621 ci: 移除错误的容器内磁盘检查
kind-on-kata-smoke / smoke (push) Failing after 13m13s
2026-09-14 16:02:13 +00:00
panxiao81 311a506982 ci: 记录 kind guest 临时盘容量
kind-on-kata-smoke / smoke (push) Failing after 8s
2026-09-14 16:00:51 +00:00
panxiao81 a0d38fc9ab ci: 验证 kind 使用 guest-local overlay2
kind-on-kata-smoke / smoke (push) Failing after 10m59s
2026-09-14 15:54:28 +00:00
panxiao81 fa293ffc80 ci: 重试 kind 工具下载
kind-on-kata-smoke / smoke (push) Failing after 16m43s
2026-09-14 14:53:46 +00:00
panxiao81 77df686981 ci: 向 kind node 提供 guest kmsg
kind-on-kata-smoke / smoke (push) Failing after 28s
2026-09-14 14:51:37 +00:00
panxiao81 9e6515406f ci: 将 guest Docker 暴露给 kind job
kind-on-kata-smoke / smoke (push) Failing after 5m45s
2026-09-14 14:42:10 +00:00
panxiao81 f6c216ce93 ci: 验证 Kata 内运行 kind 集群
kind-on-kata-smoke / smoke (push) Failing after 15s
2026-09-14 14:38:14 +00:00
panxiao81 e0e8213b8f Merge pull request '部署 zot:接入 SeaweedFS、SPIRE 并统一 S3 凭据来源' (#55) from feat/zot-spire-s3 into main
yaml / yaml (push) Successful in 11s
ansible / collection-test (push) Successful in 50s
ansible / lint (push) Successful in 11m30s
Reviewed-on: #55
2026-09-14 13:38:17 +00:00
panxiao81 5c2b575a4f feat(zot): 接入 SeaweedFS 与 SPIRE 并统一 S3 凭据来源
yaml / yaml (pull_request) Successful in 20s
ansible / collection-test (pull_request) Successful in 59s
ansible / lint (pull_request) Successful in 10m32s
2026-09-14 13:26:54 +00:00
panxiao81 67dfe1ddcb Merge pull request 54: 记录 SPIRE 与 OpenBao workload identity 用法 2026-09-14 10:33:14 +00:00
panxiao81 ed86738c4b 文档:记录 SPIRE 与 OpenBao workload identity 用法 2026-09-14 10:31:36 +00:00
panxiao81 8c27a7286b Merge pull request 53: 配置 SPIRE JWT-SVID 登录 OpenBao
terraform / validate (push) Successful in 1m5s
2026-09-13 16:01:52 +00:00
panxiao81 c9987122e6 配置 SPIRE JWT-SVID 登录 OpenBao
terraform / validate (pull_request) Successful in 1m12s
2026-09-13 15:56:08 +00:00
panxiao81 9139f6f1f1 Merge pull request 52: 暴露 SPIRE OIDC discovery endpoint
yaml / yaml (push) Successful in 14s
ansible / collection-test (push) Successful in 1m8s
ansible / lint (push) Successful in 2m10s
2026-09-13 15:50:21 +00:00
panxiao81 b6ad65d768 暴露 SPIRE OIDC discovery endpoint
ansible / collection-test (pull_request) Successful in 2m17s
yaml / yaml (pull_request) Successful in 28s
ansible / lint (pull_request) Successful in 4m21s
2026-09-13 15:44:18 +00:00
panxiao81 d256682d11 Merge pull request 49: 建立 DNS source of truth
yaml / yaml (push) Successful in 20s
ansible / collection-test (push) Successful in 2m3s
ansible / lint (push) Successful in 3m17s
2026-09-13 15:42:12 +00:00
panxiao81 bdacc03e74 Merge pull request 51: 部署 SPIRE workload identity 基础设施
yaml / yaml (push) Successful in 18s
2026-09-13 15:35:53 +00:00
panxiao81 22192a3c68 部署 SPIRE workload identity 基础设施
yaml / yaml (pull_request) Successful in 13s
2026-09-13 15:33:20 +00:00
panxiao81 e2be2ca045 fix(dns): 声明公网隧道 CNAME 目标
yaml / yaml (pull_request) Successful in 18s
ansible / collection-test (pull_request) Successful in 54s
ansible / lint (pull_request) Successful in 5m13s
2026-09-10 18:04:02 +00:00
panxiao81 b390565146 ci(ansible): 并行测试本地 collection
ansible / lint (pull_request) Successful in 2m40s
yaml / yaml (pull_request) Successful in 16s
ansible / collection-test (pull_request) Successful in 54s
2026-09-10 17:48:19 +00:00
panxiao81 97021109ac feat(dns): 建立统一清单并接管 Samba 静态记录
yaml / yaml (pull_request) Successful in 39s
ansible / lint (pull_request) Failing after 3m5s
2026-09-10 17:24:49 +00:00
panxiao81 422a640c0f Merge pull request '修复 Grafana 在 ZFS RWO PVC 上的升级卡死' (#48) from fix/grafana-zfs-recreate-strategy into main
yaml / yaml (push) Successful in 11s
Reviewed-on: #48
2026-09-10 16:34:55 +00:00
panxiao81 d09f62c063 fix(grafana): 使用 Recreate 挂载 ZFS PVC
yaml / yaml (pull_request) Successful in 11s
2026-09-10 15:55:28 +00:00
panxiao81 20a60b77d0 Merge pull request '激活剩余可观测性栈 Flux 接管' (#44) from feat/observability-remainder-adoption-activate into main
yaml / yaml (push) Successful in 13s
Reviewed-on: #44
2026-09-10 15:47:00 +00:00
panxiao81 15444b9eb3 feat(flux): 激活剩余可观测性栈接管
yaml / yaml (pull_request) Successful in 12s
2026-09-10 12:26:09 +00:00
panxiao81 065f96d432 Merge pull request '统一接管剩余可观测性栈' (#43) from feat/observability-remainder-adoption-stage into main
yaml / yaml (push) Successful in 11s
Reviewed-on: #43
2026-09-10 12:23:22 +00:00
panxiao81 92820fc631 feat(flux): 统一接管剩余可观测性栈
yaml / yaml (pull_request) Successful in 10s
2026-09-10 11:53:06 +00:00
panxiao81 6a54afede3 Merge pull request '激活 VictoriaMetrics Operator Flux 接管' (#42) from feat/vm-operator-adoption-activate into main
yaml / yaml (push) Successful in 11s
Reviewed-on: #42
2026-09-10 11:45:44 +00:00
panxiao81 1ea4604dfc feat(flux): 激活 VictoriaMetrics Operator 接管
yaml / yaml (pull_request) Successful in 12s
2026-09-10 11:35:25 +00:00
panxiao81 47c8133034 Merge pull request '分阶段接管 VictoriaMetrics Operator' (#41) from feat/vm-operator-adoption-stage into main
yaml / yaml (push) Successful in 10s
Reviewed-on: #41
2026-09-10 11:32:45 +00:00
panxiao81 b9645be107 feat(flux): 分阶段接管 VictoriaMetrics Operator
yaml / yaml (pull_request) Successful in 10s
2026-09-10 11:23:35 +00:00
panxiao81 ffe21da281 Merge pull request '激活 OpenEBS Flux HelmRelease 接管' (#40) from feat/openebs-adoption-activate into main
yaml / yaml (push) Successful in 10s
Reviewed-on: #40
2026-09-10 11:12:48 +00:00
panxiao81 b631bae5b3 feat(flux): 激活 OpenEBS 接管
yaml / yaml (pull_request) Successful in 11s
2026-09-10 11:06:29 +00:00
panxiao81 addd103b2c Merge pull request '分阶段接管 OpenEBS Helm release' (#39) from feat/openebs-adoption-stage into main
yaml / yaml (push) Successful in 21s
Reviewed-on: #39
2026-09-10 11:03:56 +00:00
panxiao81 f000301864 feat(flux): 分阶段接管 OpenEBS
yaml / yaml (pull_request) Successful in 30s
2026-09-10 11:01:21 +00:00
panxiao81 abf3be0eae Merge pull request #38: 激活 Envoy Gateway Flux 接管
yaml / yaml (push) Successful in 21s
2026-09-10 09:49:01 +00:00
panxiao81 4070b75436 feat(flux): 激活 Envoy Gateway 接管
yaml / yaml (pull_request) Successful in 10s
2026-09-10 09:45:25 +00:00
panxiao81 7d72e12d60 Merge pull request '分阶段接管 Envoy Gateway Helm release' (#37) from feat/envoy-gateway-adoption-stage into main
yaml / yaml (push) Successful in 10s
Reviewed-on: #37
2026-09-10 09:29:37 +00:00
panxiao81 c6e3abbfc0 feat(flux): 分阶段接管 Envoy Gateway
yaml / yaml (pull_request) Successful in 10s
2026-09-10 09:26:54 +00:00
panxiao81 392df6b6e6 Merge pull request '激活 cert-manager Flux HelmRelease 接管' (#36) from feat/cert-manager-adoption-activate into main
yaml / yaml (push) Successful in 29s
Reviewed-on: #36
2026-09-10 09:21:21 +00:00
panxiao81 d8754100f1 feat(flux): 激活 cert-manager 接管
yaml / yaml (pull_request) Successful in 37s
2026-09-10 09:20:03 +00:00
panxiao81 8c18d9daf3 Merge pull request '分阶段接管 cert-manager Helm release' (#35) from feat/cert-manager-adoption-stage into main
yaml / yaml (push) Successful in 34s
Reviewed-on: #35
2026-09-10 09:17:55 +00:00
panxiao81 2a1d1e095e feat(flux): 分阶段接管 cert-manager
yaml / yaml (pull_request) Successful in 10s
2026-09-10 09:15:26 +00:00
panxiao81 039ed46c8e Merge pull request '激活 Flux 对 External Secrets 的接管' (#33) from feat/external-secrets-adoption-activate into main
yaml / yaml (push) Successful in 11s
2026-09-10 08:44:37 +00:00
panxiao81 c47922855f feat(flux): 激活 External Secrets 接管
yaml / yaml (pull_request) Successful in 10s
2026-09-10 08:43:35 +00:00
panxiao81 9a581a2097 Merge pull request '分阶段接管 External Secrets Helm release' (#32) from feat/external-secrets-adoption-stage into main
yaml / yaml (push) Successful in 10s
Reviewed-on: #32
2026-09-10 08:40:01 +00:00
panxiao81 c4b425b079 feat(flux): 分阶段接管 External Secrets
yaml / yaml (pull_request) Successful in 9s
2026-09-10 08:39:22 +00:00
panxiao81 02fd9a932a Merge pull request '按路径拆分静态 CI 并消除重复运行' (#31) from ci/path-filtered-workflows into main
yaml / yaml (push) Successful in 23s
terraform / validate (push) Successful in 23s
ansible / lint (push) Successful in 3m57s
Reviewed-on: #31
2026-09-10 08:34:42 +00:00
panxiao81 8f6b08bd20 ci: 按路径拆分静态检查
yaml / yaml (pull_request) Successful in 20s
terraform / validate (pull_request) Successful in 22s
ansible / lint (pull_request) Successful in 4m5s
2026-09-10 08:33:26 +00:00
panxiao81 1b2e551392 Merge pull request '修复 Actions user-defined network 的 MTU' (#30) from fix/gitea-runner-user-network-mtu into main
lint / yaml (push) Successful in 11s
lint / terraform (push) Successful in 19s
lint / ansible (push) Successful in 5m28s
Reviewed-on: #30
2026-09-10 07:56:57 +00:00
panxiao81 4d6274bb8f 修复 Actions 动态网络的 MTU
lint / yaml (push) Successful in 1m6s
lint / terraform (push) Successful in 1m15s
lint / ansible (push) Successful in 5m18s
lint / yaml (pull_request) Successful in 11s
lint / terraform (pull_request) Successful in 1m15s
lint / ansible (pull_request) Successful in 8m6s
2026-09-10 07:55:39 +00:00
panxiao81 def6ba185d Merge pull request '记录 Gitea 后续精简升级策略' (#28) from docs/gitea-upgrade-policy into main
lint / yaml (push) Successful in 9s
lint / terraform (push) Successful in 29s
lint / ansible (push) Successful in 4m17s
Reviewed-on: #28
2026-09-10 07:44:04 +00:00
panxiao81 19cd0a938b docs(gitea): 记录精简升级验收策略
lint / ansible (push) Successful in 3m20s
lint / yaml (push) Successful in 9s
lint / terraform (push) Successful in 32s
lint / yaml (pull_request) Successful in 9s
lint / terraform (pull_request) Successful in 31s
lint / ansible (pull_request) Successful in 4m58s
2026-09-10 07:40:06 +00:00
panxiao81 c4f7046e1c Merge pull request '修复 Gitea runner 访问 GitHub 超时' (#29) from fix/gitea-runner-mtu into main
lint / yaml (push) Successful in 12s
lint / terraform (push) Successful in 30s
lint / ansible (push) Failing after 15m55s
Reviewed-on: #29
2026-09-10 07:34:34 +00:00
panxiao81 33627573c3 修复 Gitea runner 的 DinD MTU
lint / yaml (push) Successful in 18s
lint / terraform (pull_request) Successful in 35s
lint / terraform (push) Successful in 30s
lint / yaml (pull_request) Successful in 17s
lint / ansible (push) Failing after 19m37s
lint / ansible (pull_request) Failing after 19m55s
2026-09-10 07:30:09 +00:00
panxiao81 5539793e10 Merge pull request: 激活 Gitea 1.27.3 升级
lint / yaml (push) Successful in 14s
lint / terraform (push) Successful in 31s
lint / ansible (push) Successful in 9m3s
2026-09-10 07:14:25 +00:00
panxiao81 789872264e feat(gitea): 激活 1.27.3 升级
lint / yaml (pull_request) Successful in 12s
lint / terraform (pull_request) Successful in 33s
lint / yaml (push) Successful in 10s
lint / terraform (push) Successful in 30s
lint / ansible (pull_request) Successful in 6m52s
lint / ansible (push) Successful in 6m55s
2026-09-10 07:04:24 +00:00
panxiao81 48f6e52e61 Merge pull request: 准备 Gitea 1.27.3 suspended 升级
lint / yaml (push) Successful in 11s
lint / terraform (push) Successful in 30s
lint / ansible (push) Successful in 7m9s
2026-09-10 07:01:04 +00:00
panxiao81 7a1c83054d docs(gitea): 记录 1.26.4 OIDC 验收成功
lint / yaml (push) Successful in 13s
lint / ansible (push) Successful in 5m31s
lint / terraform (push) Successful in 30s
lint / yaml (pull_request) Successful in 8s
lint / terraform (pull_request) Successful in 33s
lint / ansible (pull_request) Successful in 6m14s
2026-09-10 06:58:49 +00:00
panxiao81 01cca07f48 feat(gitea): 准备 1.27.3 suspended 升级
lint / yaml (pull_request) Successful in 18s
lint / yaml (push) Successful in 17s
lint / terraform (pull_request) Successful in 31s
lint / terraform (push) Successful in 32s
lint / ansible (pull_request) Successful in 3m51s
lint / ansible (push) Successful in 3m34s
2026-09-10 06:56:57 +00:00
panxiao81 f9bcde53ab Merge pull request: 激活 Gitea 1.26.4 升级
lint / ansible (push) Successful in 4m16s
lint / terraform (push) Successful in 42s
lint / yaml (push) Successful in 10s
2026-09-10 06:38:18 +00:00
panxiao81 1538a8a3c8 feat(gitea): 激活 1.26.4 升级
lint / yaml (pull_request) Successful in 23s
lint / yaml (push) Successful in 23s
lint / terraform (push) Successful in 37s
lint / terraform (pull_request) Successful in 30s
lint / ansible (push) Successful in 4m14s
lint / ansible (pull_request) Successful in 4m29s
2026-09-10 06:37:28 +00:00
panxiao81 6bddb744c9 Merge pull request: 准备 Gitea 1.26.4 suspended 升级
lint / yaml (push) Successful in 12s
lint / terraform (push) Successful in 30s
lint / ansible (push) Successful in 6m35s
2026-09-10 06:01:31 +00:00
panxiao81 bac85b6335 feat(gitea): 准备 1.26.4 suspended 升级
lint / yaml (pull_request) Successful in 14s
lint / yaml (push) Successful in 15s
lint / terraform (push) Successful in 34s
lint / terraform (pull_request) Successful in 33s
lint / ansible (push) Successful in 4m21s
lint / ansible (pull_request) Successful in 5m1s
2026-09-10 05:59:23 +00:00
panxiao81 af23195b04 docs: 规划 Gitea 1.27 分阶段升级
lint / yaml (pull_request) Successful in 15s
lint / yaml (push) Successful in 15s
lint / terraform (pull_request) Successful in 32s
lint / terraform (push) Successful in 33s
lint / ansible (pull_request) Successful in 3m19s
lint / ansible (push) Successful in 3m5s
2026-09-10 05:50:40 +00:00
90 changed files with 2659 additions and 136 deletions
+79
View File
@@ -0,0 +1,79 @@
---
name: ansible
on:
push:
branches: [main]
paths:
- 'infrastructure/**/ansible/**'
- 'infrastructure/dns/**'
- '.ansible-lint'
- '.gitea/workflows/ansible.yml'
pull_request:
paths:
- 'infrastructure/**/ansible/**'
- 'infrastructure/dns/**'
- '.ansible-lint'
- '.gitea/workflows/ansible.yml'
env:
ANSIBLE_COLLECTIONS_PATH: /root/.ansible/collections
jobs:
lint:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Bootstrap uv
run: |
python3 -m pip install --user --break-system-packages \
--index-url https://pypi.org/simple --quiet uv==0.11.7
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Install ansible-lint and collections
run: |
for i in 1 2 3 4 5; do
uv tool install ansible-core --with paramiko --with pywinrm --quiet && break
echo "attempt $i failed"; sleep 10
done
for i in 1 2 3 4 5; do
uv tool install ansible-lint --quiet && break
echo "attempt $i failed"; sleep 10
done
export PATH="$HOME/.local/bin:$PATH"
for p in infrastructure/proxmox infrastructure/samba-ad infrastructure/openbao; do
ansible-galaxy collection install \
-r "$p/ansible/requirements.yml" -p "$ANSIBLE_COLLECTIONS_PATH"
done
- name: ansible-lint
run: |
export PATH="$HOME/.local/bin:$PATH"
export ANSIBLE_COLLECTIONS_PATH="$PWD/infrastructure/samba-ad/ansible/collections:/root/.ansible/collections"
rc=0
for p in infrastructure/openbao infrastructure/samba-ad infrastructure/proxmox; do
echo "::group::$p"
(cd "$p/ansible" && ansible-lint -c ../../../.ansible-lint --nocolor -f pep8 .) || rc=1
echo "::endgroup::"
done
exit $rc
collection-test:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Install ansible-core
run: |
python3 -m pip install --user --break-system-packages \
--index-url https://pypi.org/simple --quiet uv==0.11.7
export PATH="$HOME/.local/bin:$PATH"
uv tool install ansible-core --quiet
- name: Run ansible-test
working-directory: infrastructure/samba-ad/ansible/collections/ansible_collections/ddupan/homelab
run: |
export PATH="$HOME/.local/bin:$PATH"
ansible-test sanity --venv --requirements --python 3.12 --color no
ansible-test units --venv --requirements --python 3.12 --color no
+63
View File
@@ -0,0 +1,63 @@
---
name: kind-on-kata-smoke
on:
push:
branches:
- poc/kind-on-kata
paths:
- .gitea/workflows/kind-on-kata-smoke.yml
workflow_dispatch:
jobs:
smoke:
runs-on: kata-poc
steps:
- name: Create nested kind cluster
shell: sh
env:
KIND_VERSION: v0.27.0
KIND_NODE_IMAGE: kindest/node:v1.32.2@sha256:f226345927d7e348497136874b6d207e0b32cc52154ad8323129352923a3142f
run: |
set -eu
apk add --no-cache ca-certificates curl docker-cli
curl --retry 5 --retry-all-errors --connect-timeout 15 -fsSLo /tmp/kind \
"https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-linux-amd64"
curl --retry 5 --retry-all-errors --connect-timeout 15 -fsSLo /tmp/kind.sha256sum \
"https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-linux-amd64.sha256sum"
expected="$(awk '{print $1}' /tmp/kind.sha256sum)"
printf '%s %s\n' "$expected" /tmp/kind | sha256sum -c -
install -m 0755 /tmp/kind /usr/local/bin/kind
docker info --format 'kernel={{.KernelVersion}} driver={{.Driver}}'
test "$(docker info --format '{{.Driver}}')" = overlay2
cleanup() {
kind delete cluster --name nested >/dev/null 2>&1 || true
}
trap cleanup EXIT
cat >/tmp/kind-config.yaml <<'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: nested
nodes:
- role: control-plane
extraMounts:
- hostPath: /dev/kmsg
containerPath: /dev/kmsg
EOF
kind create cluster -v 9 --retain --config /tmp/kind-config.yaml --image "$KIND_NODE_IMAGE" --wait 5m
docker exec nested-control-plane kubectl \
--kubeconfig=/etc/kubernetes/admin.conf wait \
--for=condition=Ready node/nested-control-plane --timeout=2m
docker exec nested-control-plane kubectl \
--kubeconfig=/etc/kubernetes/admin.conf run smoke \
--image=docker.io/library/busybox:1.37 --restart=Never \
--command -- sh -c 'echo kind-on-kata-ok'
docker exec nested-control-plane kubectl \
--kubeconfig=/etc/kubernetes/admin.conf wait \
--for=jsonpath='{.status.phase}'=Succeeded pod/smoke --timeout=2m
test "$(docker exec nested-control-plane kubectl \
--kubeconfig=/etc/kubernetes/admin.conf logs smoke)" = kind-on-kata-ok
kind delete cluster --name nested
trap - EXIT
+14 -77
View File
@@ -1,20 +1,26 @@
--- ---
# Stage 1 of the infra pipeline: static checks only. No cluster access, no # Stage 1 of the infra pipeline: static checks only. No cluster access, no
# credentials, no mutation — so this is safe to run on every push from day one. # credentials or mutation. It runs only when YAML-related paths change.
# #
# Stages 2 (kubectl --dry-run=server) and 3 (k3d / molecule) come later and DO # Stages 2 (kubectl --dry-run=server) and 3 (k3d / molecule) come later and DO
# need cluster access; keep them in separate workflows so a credential problem # need cluster access; keep them in separate workflows so a credential problem
# there can never block this one. # there can never block this one.
name: lint name: yaml
on: on:
push: push:
branches: [main]
paths:
- '**/*.yaml'
- '**/*.yml'
- '.yamllint.yml'
- '.gitea/workflows/lint.yml'
pull_request: pull_request:
paths:
env: - '**/*.yaml'
# ansible-lint and ansible-core install as separate uv tools. Install Galaxy - '**/*.yml'
# collections into this shared path so both isolated environments can see them. - '.yamllint.yml'
ANSIBLE_COLLECTIONS_PATH: /root/.ansible/collections - '.gitea/workflows/lint.yml'
jobs: jobs:
yaml: yaml:
@@ -23,8 +29,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Bootstrap uv - name: Bootstrap uv
# setup-uv queries api.github.com, which is unreachable from the nested # Pin the tool for reproducibility; PyPI also avoids another setup action.
# job network. Official PyPI is reachable; pin the tool for reproducibility.
run: | run: |
python3 -m pip install --user --break-system-packages \ python3 -m pip install --user --break-system-packages \
--index-url https://pypi.org/simple --quiet uv==0.11.7 --index-url https://pypi.org/simple --quiet uv==0.11.7
@@ -47,71 +52,3 @@ jobs:
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
files=$(git ls-files '*.yaml' '*.yml' | grep -vE '^apps/netboot/') files=$(git ls-files '*.yaml' '*.yml' | grep -vE '^apps/netboot/')
yamllint -c .yamllint.yml --no-warnings -f parsable $files yamllint -c .yamllint.yml --no-warnings -f parsable $files
ansible:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Bootstrap uv
run: |
python3 -m pip install --user --break-system-packages \
--index-url https://pypi.org/simple --quiet uv==0.11.7
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Install ansible-lint and collections
# pywinrm is not optional — without it every ansible.windows.* task dies
# with "No module named 'winrm'" (CLAUDE.md documents this trap).
run: |
for i in 1 2 3 4 5; do
# Do not add the `ansible` meta-package here: it bundles collections
# inside this uv venv, making Galaxy skip the shared path below while
# ansible-lint's separate venv still cannot resolve the modules.
uv tool install ansible-core --with paramiko --with pywinrm --quiet && break
echo "attempt $i failed"; sleep 10
done
for i in 1 2 3 4 5; do
uv tool install ansible-lint --quiet && break
echo "attempt $i failed"; sleep 10
done
export PATH="$HOME/.local/bin:$PATH"
for p in infrastructure/proxmox infrastructure/samba-ad infrastructure/openbao; do
ansible-galaxy collection install \
-r "$p/ansible/requirements.yml" -p "$ANSIBLE_COLLECTIONS_PATH"
done
- name: ansible-lint
# Each project has its own ansible.cfg and relative roles_path, so lint
# must run from inside each one — a single run at the repo root resolves
# roles_path incorrectly and reports spurious missing-role errors.
run: |
export PATH="$HOME/.local/bin:$PATH"
rc=0
for p in infrastructure/openbao infrastructure/samba-ad infrastructure/proxmox; do
echo "::group::$p"
(cd "$p/ansible" && ansible-lint -c ../../../.ansible-lint --nocolor -f pep8 .) || rc=1
echo "::endgroup::"
done
exit $rc
terraform:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: fmt and validate
# -backend=false so validate never touches real state or needs credentials.
# These roots deliberately use different providers AND different interactive
# auth (bao login -method=oidc, az login), which is exactly why they are not
# merged — so validate is as far as static checking can go here.
run: |
rc=0
for d in $(git ls-files '*.tf' | xargs -n1 dirname | sort -u); do
echo "::group::$d"
terraform -chdir="$d" fmt -check -diff || rc=1
terraform -chdir="$d" init -backend=false -input=false || rc=1
terraform -chdir="$d" validate || rc=1
echo "::endgroup::"
done
exit $rc
+35
View File
@@ -0,0 +1,35 @@
---
name: terraform
on:
push:
branches: [main]
paths:
- '**/*.tf'
- '**/.terraform.lock.hcl'
- '.gitea/workflows/terraform.yml'
pull_request:
paths:
- '**/*.tf'
- '**/.terraform.lock.hcl'
- '.gitea/workflows/terraform.yml'
jobs:
validate:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: fmt and validate
# -backend=false so validate never touches real state or needs credentials.
run: |
rc=0
for d in $(git ls-files '*.tf' | xargs -n1 dirname | sort -u); do
echo "::group::$d"
terraform -chdir="$d" fmt -check -diff || rc=1
terraform -chdir="$d" init -backend=false -input=false || rc=1
terraform -chdir="$d" validate || rc=1
echo "::endgroup::"
done
exit $rc
+2
View File
@@ -4,6 +4,8 @@
- `apps/http-echo/` and `archive/traefik/` contain Kubernetes/Gateway API manifests; inspect their parent Gateway references before applying archived or brownfield resources. - `apps/http-echo/` and `archive/traefik/` contain Kubernetes/Gateway API manifests; inspect their parent Gateway references before applying archived or brownfield resources.
- `apps/tailscale/helm.sh` contains live Tailscale OAuth values; do not copy, print, or commit those values anywhere else. - `apps/tailscale/helm.sh` contains live Tailscale OAuth values; do not copy, print, or commit those values anywhere else.
- Preserve the existing README intent in `apps/http-echo/` and `archive/traefik/` when updating manifests. - Preserve the existing README intent in `apps/http-echo/` and `archive/traefik/` when updating manifests.
- `CHANGELOG.md` 是冻结的历史快照,不再更新。持久的服务状态与运维知识写入对应
README/runbook;单次变化由 commit 和 PR 记录,agent 陷阱写入 `CLAUDE.md`。
- 在 homelab 工作中,所有提交到 `git.ddupan.top` 的 commit message、PR、issue - 在 homelab 工作中,所有提交到 `git.ddupan.top` 的 commit message、PR、issue
和项目文档默认优先使用中文。代码标识符、命令、配置键、上游专有名称,以及 和项目文档默认优先使用中文。代码标识符、命令、配置键、上游专有名称,以及
使用英文能避免歧义的技术字段可保留英文。 使用英文能避免歧义的技术字段可保留英文。
+17
View File
@@ -27,6 +27,23 @@ What changed in this homelab, when, and why. Newest first.
| Helm adoption activate | 第一阶段合并后 Flux source 与子 Kustomization 均 Ready,Helm release 仍为 revision 1,runner Pod 未 rollout;再次确认固定 chart 的完整 render 对 live 集群为零差异后,第二阶段移除 `suspend`,允许 Flux 修正 Helm 存储状态并开始 drift detection | | Helm adoption activate | 第一阶段合并后 Flux source 与子 Kustomization 均 Ready,Helm release 仍为 revision 1,runner Pod 未 rollout;再次确认固定 chart 的完整 render 对 live 集群为零差异后,第二阶段移除 `suspend`,允许 Flux 修正 Helm 存储状态并开始 drift detection |
| Gitea adoption stage | 开始用 Flux 接管关键 `gitea` release:固定现有 chart `12.5.3`,以 `suspend: true` 登记 HelmRelease、source、values ConfigMap 和现有 HTTPRoute,子 Kustomization 保持 `prune: false`;现有 OIDC Secret 继续只引用不覆盖,其尚未进入 OpenBao/ESO 的缺口独立跟踪 | | Gitea adoption stage | 开始用 Flux 接管关键 `gitea` release:固定现有 chart `12.5.3`,以 `suspend: true` 登记 HelmRelease、source、values ConfigMap 和现有 HTTPRoute,子 Kustomization 保持 `prune: false`;现有 OIDC Secret 继续只引用不覆盖,其尚未进入 OpenBao/ESO 的缺口独立跟踪 |
| Gitea adoption activate | 第一阶段合并后 source、子 Kustomization 和 HTTPRoute 均 Ready,Helm release 仍为 revision 14,Gitea Pod 未重建或重启;再次确认固定 chart 对 live 业务资源零差异后移除 `suspend`,允许 Flux 修正 Helm 存储状态并启用 drift detection | | Gitea adoption activate | 第一阶段合并后 source、子 Kustomization 和 HTTPRoute 均 Ready,Helm release 仍为 revision 14,Gitea Pod 未重建或重启;再次确认固定 chart 对 live 业务资源零差异后移除 `suspend`,允许 Flux 修正 Helm 存储状态并启用 drift detection |
| Gitea upgrade plan | 规划两跳升级:chart `12.6.0` + 显式 Gitea `1.26.4`,再到 chart `12.7.0` + 显式 Gitea `1.27.3`;每个 minor 都先以 suspended desired state 合并、停机建立 CNPG/PVC 一致回滚点,再用独立 PR 激活。当前 CNPG 无连续备份、local-path PVC 无 snapshot class,因此禁止无备份直接触发数据库 migration |
| Gitea 1.26 preparation | 将第一跳目标写入 Git:chart 固定为 `12.6.0`、rootless 镜像显式固定为 `1.26.4`,同时重新设置 HelmRelease `suspend: true`;该准备 revision 合并后只更新 desired state,不触发 Pod replacement 或数据库 migration |
| Gitea 1.26 backup | 预拉取 `1.26.4-rootless` 后,在 HelmRelease suspended 状态将 Gitea scale 到 0;生成并校验 539355-byte CNPG custom dump、2609188-byte PVC tar 和 2848667-byte GPG encrypted bundle,随后恢复旧版 `1.25.5` 并验证内外 API 与 Flux source。按明确决定不上传 OCI,本阶段接受只有节点本地回滚点的风险 |
| Gitea 1.26 activation | 停机一致备份门槛完成后,激活变更只移除 HelmRelease 的 `suspend`;chart `12.6.0`、显式 `1.26.4-rootless` image、values、数据库与 PVC 均保持已 review 的准备状态 |
| Gitea 1.26 result | Flux 以 Helm revision 16 成功完成 chart `12.6.0` / Gitea `1.26.4-rootless` 的 Recreate upgrade 和 migration 323–330;Pod 内/统一域名 API、临时 branch push/delete、Flux source 及 main/smoke 的全部 CI jobs 均通过,Pod 在约 15 分钟采样中保持零重启,Authelia OIDC init 同步与浏览器交互式管理员登录也已确认成功 |
| Gitea 1.27 preparation | 预拉取 `1.27.3-rootless` 并将第二跳 desired state 原子设置为 chart `12.7.0`、显式 image `1.27.3` 和 `suspend: true`;合并只暂停并登记目标,不执行 migration,激活前必须从当前 1.26.4 数据建立新的配套回滚点 |
| Gitea 1.27 activation | 按明确决定跳过新的 1.26.4 数据库/PVC 备份,激活变更只移除 HelmRelease 的 `suspend`;接受 migration 失败后不能无损回退到 1.26.4 的风险,现有 1.25.5 本地备份仅能作为会丢失第一跳后状态的灾难恢复点 |
| CI runner network | 修复 Actions job 容器访问 GitHub 超时:k3s Pod MTU 为 1450,而 DinD 动态 bridge 默认为 1500;为 Docker daemon 固定 `--mtu=1450`。隔离测试证明相同 curl 镜像在默认 bridge 超时、在 MTU 1450 bridge 下访问 GitHub 与 API 均约 0.1 秒成功 |
### Incident: Gitea 备份后的恢复命令被 stdin 校验阻塞
最初把本地 custom-format dump 通过 `kubectl exec -i` 输送给 CNPG Pod 内的
`pg_restore --list`;远端 stdin 没有正常结束,组合脚本因此停在校验步骤,尚未执行
后面的 scale-up。Deployment 保持预期的 0,没有失败 Pod 或数据写入。发现后终止
会话、先恢复 Gitea,再把 dump 临时复制到 CNPG 可写数据卷完成校验并立即删除。
旧版 Gitea 恢复后内外 API 和 Flux source 均正常;后续 runbook 不再把 stdin 管道与
恢复命令放进同一个 shell transaction。
`Carried forward`: complete the two-stage zero-change `gitea` HelmRelease `Carried forward`: complete the two-stage zero-change `gitea` HelmRelease
adoption, migrate its remaining manual OIDC Secret to OpenBao/ESO, then upgrade adoption, migrate its remaining manual OIDC Secret to OpenBao/ESO, then upgrade
+9 -3
View File
@@ -132,9 +132,10 @@ recovered, so `.vault_pass.gpg` is the authoritative recovery path.
## Working rules ## Working rules
- **Record changes in `CHANGELOG.md`.** One dated section per day, newest first; incidents - **Do not update `CHANGELOG.md`.** It is a frozen historical snapshot; requiring every PR
get their own subsection. Traps and procedures belong *here* in CLAUDE.md, not there — to append to one shared text file caused needless conflicts and duplicated Git/PR history.
the changelog is for humans reading what changed. Put durable service state and operational knowledge in the component README or runbook,
agent-facing traps here, and let commits/PRs record individual changes.
- **Verify, don't assert.** Check the end state (`pvesm status`, `linstor node list`, - **Verify, don't assert.** Check the end state (`pvesm status`, `linstor node list`,
`kubectl get pod`, `show ip route`) rather than trusting that a command "should have" worked. `kubectl get pod`, `show ip route`) rather than trusting that a command "should have" worked.
Several confident diagnoses in this repo's history were wrong until measured. Several confident diagnoses in this repo's history were wrong until measured.
@@ -149,6 +150,11 @@ recovered, so `.vault_pass.gpg` is the authoritative recovery path.
all-clear. Use `git check-ignore --no-index` and `git rm --cached` to actually remove it. all-clear. Use `git check-ignore --no-index` and `git rm --cached` to actually remove it.
- **A `.tfplan` is a zip containing a full `tfstate`.** It walks straight past `*.tfstate` - **A `.tfplan` is a zip containing a full `tfstate`.** It walks straight past `*.tfstate`
ignore rules. Ignore `*.tfplan` everywhere. ignore rules. Ignore `*.tfplan` everywhere.
- **SPIRE CLI JSON can be an array of response blocks.** `spire-agent api fetch jwt
-output json` in 1.15.3 returns blocks containing `svids` and `bundles`. Capture stdout
privately and type-check before extracting fields; `list(response)` prints full tokens
when the response is already an array. Never inspect credential payloads by printing
their containers, and never put fetched JWTs in command arguments or Pod logs.
- **Quoting does not survive two ssh hops.** `ssh pve1 "ssh pve3 'cmd | qm monitor 103'"` - **Quoting does not survive two ssh hops.** `ssh pve1 "ssh pve3 'cmd | qm monitor 103'"`
loses the inner quotes — ssh re-joins argv with spaces, so the pipeline splits and the loses the inner quotes — ssh re-joins argv with spaces, so the pipeline splits and the
tail runs on the **jump host**. It fails silently if you discard stderr: a `screendump` tail runs on the **jump host**. It fails silently if you discard stderr: a `screendump`
+25 -1
View File
@@ -6,7 +6,7 @@ Envoy Gateway HTTPRoute 提供。Helm chart 自带的无 class Ingress 暂时保
## Flux 接管 ## Flux 接管
现有 release 是 `gitea-12.5.3`(Gitea `1.25.5`)。接管分为两个 PR:第一阶段创建 初始接管 release 是 `gitea-12.5.3`(Gitea `1.25.5`)。接管分为两个 PR:第一阶段创建
固定版本且 `suspend: true` 的 HelmRelease,只让 Flux 登记对象;确认 source Ready 固定版本且 `suspend: true` 的 HelmRelease,只让 Flux 登记对象;确认 source Ready
并重新验证完整 chart render 后,第二阶段解除 suspend。第一阶段已经确认 source、 并重新验证完整 chart render 后,第二阶段解除 suspend。第一阶段已经确认 source、
子 Kustomization 和 HTTPRoute 均 Ready,Helm revision 与 Gitea Pod 未变化;第二次 子 Kustomization 和 HTTPRoute 均 Ready,Helm revision 与 Gitea Pod 未变化;第二次
@@ -20,3 +20,27 @@ client secret 仍是历史手工 Secret `gitea-oidc-secret`,本次接管只引
Gitea 是 Flux GitRepository 的上游。升级或重启期间 Git source 暂时不可用不会删除 Gitea 是 Flux GitRepository 的上游。升级或重启期间 Git source 暂时不可用不会删除
已经应用的资源;Gitea 恢复后 Flux 会继续同步。任何会改变 Pod template、数据库迁移 已经应用的资源;Gitea 恢复后 Flux 会继续同步。任何会改变 Pod template、数据库迁移
或 PVC identity 的变更都不得与首次接管合并。 或 PVC identity 的变更都不得与首次接管合并。
跨 minor 的执行顺序、停机一致备份和失败恢复步骤见
[`../../docs/gitea-upgrade-plan.md`](../../docs/gitea-upgrade-plan.md)。
当前 release 是 chart `12.7.0` / Gitea `1.27.3-rootless`。两次跨 minor migration、
API、OIDC、Git/Flux 和 runner 均已验证。第二跳按明确决定跳过了新的 1.26.4 停机
一致回滚点;现有 1.25.5 本地备份没有 OCI 或异机副本,因此只作为会丢失后续状态的
灾难恢复点。
## 后续升级默认策略
已连续验证两次 Flux 驱动的跨 minor Recreate upgrade,后续常规 patch/minor 升级
不再默认执行长时间观察、临时 branch push/delete、重复 CI、逐条 migration 日志审计
或每个 minor 的停机备份。默认只需要:
1. 固定 chart 和实际 `image.tag`,阅读与本配置相关的 breaking/security notes;
2. Helm/Kustomize render 通过 review;
3. 合并后确认 HelmRelease `UpgradeSucceeded`、Pod Ready 且没有 CrashLoop;
4. API 返回目标版本,并简单确认 OIDC 登录和一次正常 Git 操作。
只有变更数据库后端或存储布局、rootless 模式、PVC identity、部署策略、重大 chart
结构,或者 release notes 指出相关 breaking migration 时,才恢复停机一致备份、分阶段
suspend、详细日志审计和扩展验收。出现启动失败或 migration error 时也立即升级为完整
故障流程。
+9 -1
View File
@@ -11,7 +11,7 @@ spec:
sourceRef: sourceRef:
kind: HelmRepository kind: HelmRepository
name: gitea-charts name: gitea-charts
version: 12.5.3 version: 12.7.0
driftDetection: driftDetection:
mode: enabled mode: enabled
install: install:
@@ -26,6 +26,14 @@ spec:
strategy: strategy:
name: RetryOnFailure name: RetryOnFailure
retryInterval: 5m retryInterval: 5m
# Keep the patch release explicit because chart 12.7.0 defaults to 1.27.0.
# This override is in the suspended HelmRelease itself so chart, image and
# suspension are applied atomically; changing the watched values ConfigMap in
# the same revision could otherwise trigger reconciliation first.
values:
image:
rootless: true
tag: "1.27.3"
valuesFrom: valuesFrom:
- kind: ConfigMap - kind: ConfigMap
name: gitea-values name: gitea-values
+22 -2
View File
@@ -11,7 +11,9 @@
| `helm.sh` | Installs or upgrades the SeaweedFS release. | | `helm.sh` | Installs or upgrades the SeaweedFS release. |
**Install** **Install**
1. Set real S3 access and secret keys in `values.yaml`. 1. 在 OpenBao `kv/k8s/seaweedfs-s3` 维护基础 S3 配置;zot 凭据单独以
`kv/k8s/zot-s3` 为唯一来源。ESO 合成为 `seaweedfs-s3-config`,详见下文。
不要把真实 AK/SK 放进 `values.yaml`。
2. Apply the manifests: 2. Apply the manifests:
```bash ```bash
bash ~/services/apps/seaweedfs/helm.sh bash ~/services/apps/seaweedfs/helm.sh
@@ -31,4 +33,22 @@
**Notes** **Notes**
- The chart manages master, volume, filer, S3, and admin components. - The chart manages master, volume, filer, S3, and admin components.
- The chart-managed S3 secret uses the current AK/SK pair for the admin user. - The filer uses the ESO-managed `seaweedfs-s3-config` Secret for static S3 identities.
## zot 制品存储
`zot` bucket 专用于 [zot Registry](../zot/README.md),OCI 数据位于 `registry/`
前缀。静态身份 `zot` 只有该 bucket 的 Read/Write/List/Tagging 权限,凭据唯一来源为
Bao `kv/k8s/zot-s3` 的 `access_key` / `secret_key`,同时供 zot consumer 和
SeaweedFS 服务端使用。
[ExternalSecret 模板](../../platform/external-secrets/externalsecrets.yaml) 保留
`kv/k8s/seaweedfs-s3` 的原有身份及其他配置,再追加 zot 身份与限定 bucket 的权限。
基础配置当前版本不保存 zot AK/SK;旧 KV 版本历史仍保留。新增其他身份时使用
KV compare-and-set 保留已有内容,不覆盖 Terraform 或其他应用的 AK/SK。
不要直接编辑生成的 Kubernetes Secret。该 ExternalSecret 已单独应用到集群,
目前仍未加入 ESO 的 Flux Kustomization,遵循该组件现有 ownership 边界。
运行版本 `4.22` 可在 Secret volume 更新后向 filer/内嵌 S3 的 `weed` 进程发送
SIGHUP,重新加载静态配置,无需重启共享 S3 服务。本次接入没有启用 SeaweedFS
OIDC/STS;SPIRE 认证发生在 zot 的客户端入口。
+123
View File
@@ -0,0 +1,123 @@
# zot OCI Registry
内网入口为 `https://zot.ad.ddupan.top`。使用官方 Helm chart `0.1.124`,运行
zot `v2.1.21`,镜像固定到官方 linux/amd64 digest。
## 存储与凭据
制品、manifest 和 OCI layout 保存在现有 SeaweedFS 的 `zot` bucket,前缀为
`registry/`,S3 endpoint 为 `https://s3.ad.ddupan.top`。**不创建 PVC**;chart 的
`/var/lib/registry` 是 `emptyDir`,仅用于运行时本地工作数据。
首期单副本,关闭跨仓库 dedupe,不额外部署 Redis/DynamoDB 缓存。保留 zot GC,
暂不配置自动删除已发布版本的 retention policy。增加副本、启用 dedupe 或搜索等
扩展前,需要重新检查共享元数据与缓存的持久化要求。
凭据链路:
```text
OpenBao kv/k8s/seaweedfs-s3
→ 原有 S3 身份及基础配置 ─┐
├→ ESO 模板 → seaweedfs/seaweedfs-s3-config
OpenBao kv/k8s/zot-s3 ────┘ → 完整 s3.config
└→ ESO → zot/zot-s3 → zot 的 AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
```
`kv/k8s/zot-s3` 是 zot AK/SK 的唯一维护来源。基础配置保留原有身份及其他字段,
不再保存 zot 凭据副本;[SeaweedFS ExternalSecret](../../platform/external-secrets/externalsecrets.yaml)
使用 ESO v2 模板追加 zot 身份。两个 Kubernetes Secret 都是自动生成的消费副本,
不手工编辑。Bao 的旧版本历史保留,回滚基础配置时模板也会替换其中的旧 zot 身份。
专用 S3 身份只有 `Read:zot`、`Write:zot`、`List:zot`、`Tagging:zot`,不能读取
Terraform 的 `tfstate` bucket。`kv/k8s/zot-s3` 的字段是 `access_key` 和
`secret_key`。AK/SK 不进入 Git、Helm values 或 CI;这里仍是静态 S3 凭据,尚未
接入 SPIRE/STS。
本次归一没有轮换密钥,生成的完整配置与归一前语义一致。当前模板只有一组 zot
凭据,尚未实现新旧密钥重叠轮换。后续轮换只修改 `kv/k8s/zot-s3`,但仍需协调
两个 ExternalSecret 同步:确认 SeaweedFS Secret volume 更新后向 filer 的
`weed` 进程发送 SIGHUP,再确认 zot Secret 更新并重启 zot(环境变量不会热更新)。
两端异步更新期间可能短暂认证失败;需要无中断轮换时先扩展模板支持新旧凭据重叠。
## SPIRE 认证和授权
| 参数 | 值 |
|---|---|
| issuer | `https://spire-oidc.ad.ddupan.top` |
| JWT audience | `zot` |
| subject | `spiffe://ddupan.top/` 下的 workload SPIFFE ID |
| token endpoint | `https://zot.ad.ddupan.top/zot/auth/token` |
| 当前权限 | 受信身份可以读取所有仓库;没有常驻写入或删除授权 |
zot 通过已配置的 issuer discovery/JWKS 验证 JWT-SVID,再以 `sub` 作为授权身份。
不接受任意 issuer,不关闭 TLS/issuer 验证。新的 Kata CI 负责取得并更新自己的
JWT-SVID;确认其身份命名后,再添加针对具体 repository 的 `create`/`update`
授权,不能把整个 trust domain 都授予写权限。
现阶段拉取也需要 JWT-SVID。原定内网匿名拉取尚未启用:zot `v2.1.21` 的
OIDC Bearer middleware 会在授权阶段之前拒绝无 token 请求,单独增加
`anonymousPolicy` 无法解决。匿名读取与 SPIRE 写入共存需后续单独验证方案。
已有 SPIRE 身份的进程可以通过 Workload API 获取 `aud=zot` 的 JWT-SVID,然后
通过 `docker login` 或 `crane auth login` 的 `--password-stdin` 交给 Registry。
用户名可以使用 `zot`,实际权限取自已验证 JWT 的身份。使用独立、权限为 `0700`
的临时 `DOCKER_CONFIG`,结束后删除;不要开启 shell tracing,不要打印 token,
不要把 token 放进命令参数。token 接口不会延长 SVID 有效期。
## 部署与网络
- 官方 chart 管理 Deployment、Service、ConfigMap 和 HTTPRoute。
- `persistence: false`,Service 为 ClusterIP,TLS 由已有 Envoy Gateway 的
`https` listener 与内网通配符证书终止。
- 仅配置 Samba AD 内网 DNS;不创建公网 DNS 或 Cloudflare Tunnel route。
- NetworkPolicy 只允许现有 Envoy Gateway 数据面访问 zot 的 5000 端口。
- HTTPRoute 只暴露 `/v2/` 和 `/zot/auth/token`,不暴露内部健康检查或管理端点。
- namespace 使用 restricted PodSecurity,容器非 root、只读根文件系统。
首次已按用户授权从本地执行 `kubectl apply -k apps/zot`,由集群 Helm controller
安装。`clusters/homelab/apps/zot.yaml` 是 GitOps composition;对应文件合并进入
Flux 跟踪分支后,才由根 Kustomization 持续管理,不能把未提交的本地部署写成
已完成 Git 接管。
检查与渲染:
```bash
helm template zot --repo https://zotregistry.dev/helm-charts \
--version 0.1.124 --namespace zot -f apps/zot/values.yaml --skip-tests
sudo k3s kubectl -n zot get helmrelease,pods,externalsecret,httproute
sudo k3s kubectl -n zot get pvc
```
上游 chart 的 Helm test Pod 不满足本 namespace 的 restricted 策略,也没有
SPIRE 凭据,因此不运行默认 `helm test`;使用下述真实身份验收。
## 验收与恢复
验收使用独立临时 Pod,通过 SPIFFE CSI socket 和真实 Workload API 取得 JWT-SVID,
没有修改现有 runner。仅在初始化 `verification/smoke:spire-s3` 测试镜像时临时
授予该测试身份针对该仓库的写权限;完成后必须撤回 HelmRelease override,并删除
临时 Pod、ServiceAccount 与 ClusterSPIFFEID。
验收项目:有效 SVID + crane pull、manifest digest 一致、错误 audience、错误
signature、过期 token、无凭据写入、跨仓库写入、只读身份写入和删除拒绝;另外检查
Pod 重建后镜像仍可拉取,以及 S3 身份不能访问 `tfstate`。
2026-09-14 已完成上述验收:HelmRelease Ready、HTTPRoute Accepted/ResolvedRefs,
DNS 第二次 Ansible check 为 `changed=0`;一分钟真实 JWT-SVID 到期后返回 401。
临时写权限已移除。测试镜像可供后续 CI 验证拉取:
```text
zot.ad.ddupan.top/verification/smoke:spire-s3
sha256:b8d3b977a1235022759470903dab4a46b7cf8107958624f1f76a323eabe37c5e
```
它是仅含验证文本的 OCI 测试镜像,没有可执行入口,不用于运行服务。
Registry 恢复需要完整的 SeaweedFS bucket 数据、Bao 专用凭据和此目录配置。
zot 的临时目录不是制品备份。独立异机/离线备份尚未在本次部署中建立;不能把同一
SeaweedFS 内的数据副本当作独立灾备。重装 zot 不得删除 `zot` bucket。
参考:[官方 Kubernetes 安装](https://zotregistry.dev/v2.1.21/install-guides/install-guide-k8s/)、
[S3 存储](https://zotregistry.dev/v2.1.21/articles/storage/)、
[OIDC workload identity](https://github.com/project-zot/zot/blob/v2.1.21/examples/README-OIDC-WORKLOAD-IDENTITY.md)。
+22
View File
@@ -0,0 +1,22 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: zot-s3
namespace: zot
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: openbao
target:
name: zot-s3
creationPolicy: Owner
data:
- secretKey: access_key
remoteRef:
key: k8s/zot-s3
property: access_key
- secretKey: secret_key
remoteRef:
key: k8s/zot-s3
property: secret_key
+30
View File
@@ -0,0 +1,30 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: zot
namespace: zot
spec:
chart:
spec:
chart: zot
version: 0.1.124
interval: 1h
sourceRef:
kind: HelmRepository
name: zot
releaseName: zot
interval: 30m
timeout: 5m
driftDetection:
mode: enabled
install:
strategy:
name: RetryOnFailure
retryInterval: 5m
upgrade:
strategy:
name: RetryOnFailure
retryInterval: 5m
valuesFrom:
- kind: ConfigMap
name: zot-values
+8
View File
@@ -0,0 +1,8 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: zot
namespace: zot
spec:
interval: 1h
url: https://zotregistry.dev/helm-charts
+18
View File
@@ -0,0 +1,18 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- serviceaccount.yaml
- external-secret.yaml
- helmrepository.yaml
- helmrelease.yaml
- networkpolicy.yaml
generatorOptions:
disableNameSuffixHash: true
labels:
reconcile.fluxcd.io/watch: Enabled
configMapGenerator:
- name: zot-values
namespace: zot
files:
- values.yaml=values.yaml
+7
View File
@@ -0,0 +1,7 @@
apiVersion: v1
kind: Namespace
metadata:
name: zot
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.36
+22
View File
@@ -0,0 +1,22 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: zot-ingress
namespace: zot
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: zot
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: envoy-gateway-system
podSelector:
matchLabels:
gateway.envoyproxy.io/owning-gateway-name: eg
gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway-system
ports:
- protocol: TCP
port: 5000
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: zot
namespace: zot
automountServiceAccountToken: false
+147
View File
@@ -0,0 +1,147 @@
# 官方 chart 0.1.124 / zot v2.1.21;制品与 manifests 保存在 SeaweedFS S3。
# persistence=false 仅保留 chart 的 emptyDir,不创建 PVC。
# 首期关闭跨仓库 dedupe,不额外引入 Redis/DynamoDB 持久缓存。
replicaCount: 1
image:
repository: ghcr.io/project-zot/zot
tag: v2.1.21@sha256:8258443838e95989c13c891f78a02bc1c391b5a00591ffef24cb8c17cde28038
persistence: false
strategy:
type: Recreate
serviceAccount:
create: false
name: zot
service:
type: ClusterIP
port: 5000
mountConfig: true
mountSecret: false
secretFiles: {}
configFiles:
config.json: |
{
"distSpecVersion": "1.1.1",
"storage": {
"rootDirectory": "/var/lib/registry",
"dedupe": false,
"gc": true,
"gcDelay": "24h",
"gcInterval": "24h",
"storageDriver": {
"name": "s3",
"region": "us-east-1",
"regionendpoint": "https://s3.ad.ddupan.top",
"bucket": "zot",
"rootdirectory": "/registry",
"secure": true,
"skipverify": false,
"forcepathstyle": true
}
},
"http": {
"address": "0.0.0.0",
"port": "5000",
"externalUrl": "https://zot.ad.ddupan.top",
"compat": [
"docker2s2"
],
"auth": {
"bearer": {
"realm": "https://zot.ad.ddupan.top/zot/auth/token",
"service": "zot.ad.ddupan.top",
"oidc": [
{
"issuer": "https://spire-oidc.ad.ddupan.top",
"audiences": [
"zot"
],
"claimMapping": {
"username": "claims.sub",
"validations": [
{
"expression": "claims.sub.startsWith('spiffe://ddupan.top/')",
"message": "SPIFFE trust domain mismatch"
}
]
}
}
]
}
},
"accessControl": {
"repositories": {
"**": {
"defaultPolicy": [
"read"
]
}
}
}
},
"log": {
"level": "info"
}
}
env:
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: zot-s3
key: access_key
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: zot-s3
key: secret_key
- name: AWS_EC2_METADATA_DISABLED
value: 'true'
podSecurityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: '1'
memory: 512Mi
extraVolumes:
- name: tmp
emptyDir:
sizeLimit: 128Mi
extraVolumeMounts:
- name: tmp
mountPath: /tmp
startupProbe:
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 60
httproute:
enabled: true
parentRefs:
- name: eg
namespace: envoy-gateway-system
sectionName: https
hostnames:
- zot.ad.ddupan.top
rules:
- matches:
- path:
type: PathPrefix
value: /v2/
- path:
type: Exact
value: /zot/auth/token
timeouts:
request: 900s
backendRequest: 900s
+10 -3
View File
@@ -43,6 +43,13 @@ sudo k3s kubectl -n flux-system get gitrepositories,kustomizations
- `http-echo` 的专用测试 ConfigMap 已在 `prune: true` 生效后重新纳管,并由下一 - `http-echo` 的专用测试 ConfigMap 已在 `prune: true` 生效后重新纳管,并由下一
revision 自动删除; revision 自动删除;
- `http-echo` 保持 `prune: true`,root 保持 `prune: false`; - `http-echo` 保持 `prune: true`,root 保持 `prune: false`;
- `gitea-actions` 已由 Flux HelmRelease 接管且首次 reconcile 未触发 runner rollout; - `gitea-actions`、`gitea` 与 External Secrets 已由 Flux HelmRelease 接管,Gitea 已升级到 `1.27.3`;
- 下一个接管对象是现有 `gitea` Helm release,先固定 chart `12.5.3` 并分两阶段完成 - cert-manager 已固定现有 `v1.21.0` 并完成分阶段 Flux HelmRelease 接管;
零变化 adoption,再通过独立 PR 升级 Gitea。 - Envoy Gateway 已固定现有 `v1.5.6` 并完成分阶段 Flux HelmRelease 接管;
- OpenEBS 已固定现有 `4.4.0` 并完成分阶段 Flux HelmRelease 接管;
- VictoriaMetrics Operator 已固定现有 chart `0.66.2` 并完成分阶段 Flux HelmRelease
接管;Metrics、Logs、Traces 与 Grafana 也已统一完成 Flux 接管;
- External Secrets Operator 已固定 chart `2.8.0` 并完成分阶段接管;
- SPIRE 已按官方 hardened chart `0.30.2`(SPIRE `1.15.3`)声明,使用共享
PostgreSQL 与独立 signing-key PVC;首次上线和 OpenBao JWT-SVID PoC 尚待合并后验证;
- root Kustomization 与所有 brownfield 子 Kustomization 继续保持 `prune: false`。
+14
View File
@@ -0,0 +1,14 @@
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: cert-manager
namespace: flux-system
spec:
interval: 10m
path: ./platform/cert-manager
prune: false
sourceRef:
kind: GitRepository
name: flux-system
timeout: 3m
wait: false
+14
View File
@@ -0,0 +1,14 @@
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: envoy-gateway
namespace: flux-system
spec:
interval: 10m
path: ./platform/envoy-gateway
prune: false
sourceRef:
kind: GitRepository
name: flux-system
timeout: 3m
wait: false
@@ -0,0 +1,14 @@
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: external-secrets
namespace: flux-system
spec:
interval: 10m
path: ./platform/external-secrets
prune: false
sourceRef:
kind: GitRepository
name: flux-system
timeout: 3m
wait: false
+14
View File
@@ -0,0 +1,14 @@
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: observability
namespace: flux-system
spec:
interval: 10m
path: ./platform/observability
prune: false
sourceRef:
kind: GitRepository
name: flux-system
timeout: 3m
wait: false
+14
View File
@@ -0,0 +1,14 @@
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: openebs
namespace: flux-system
spec:
interval: 10m
path: ./platform/openebs
prune: false
sourceRef:
kind: GitRepository
name: flux-system
timeout: 3m
wait: false
+14
View File
@@ -0,0 +1,14 @@
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: spire
namespace: flux-system
spec:
interval: 10m
path: ./platform/spire
prune: false
sourceRef:
kind: GitRepository
name: flux-system
timeout: 15m
wait: false
+18
View File
@@ -0,0 +1,18 @@
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: zot
namespace: flux-system
spec:
dependsOn:
- name: envoy-gateway
- name: external-secrets
- name: spire
interval: 10m
path: ./apps/zot
prune: false
sourceRef:
kind: GitRepository
name: flux-system
timeout: 5m
wait: true
+7
View File
@@ -3,6 +3,13 @@ kind: Kustomization
resources: resources:
- flux-system - flux-system
- namespaces/gitops-canary.yaml - namespaces/gitops-canary.yaml
- apps/cert-manager.yaml
- apps/envoy-gateway.yaml
- apps/external-secrets.yaml
- apps/gitea.yaml - apps/gitea.yaml
- apps/gitea-actions.yaml - apps/gitea-actions.yaml
- apps/http-echo.yaml - apps/http-echo.yaml
- apps/openebs.yaml
- apps/spire.yaml
- apps/observability.yaml
- apps/zot.yaml
+8 -6
View File
@@ -1,7 +1,7 @@
# CI/CD — what we are building # CI/CD — what we are building
Status: **partly built.** Stage 1 and the Kubernetes runner are live; Flux and Status: **partly built.** Stage 1、Kubernetes runner 与 Flux 已上线;credentialed
credentialed stages are not yet installed. stages 尚未实现。
Started 2026-07-28. Started 2026-07-28.
## Goal ## Goal
@@ -24,18 +24,20 @@ to make drift between this repo and reality visible when it happens.
| | | | | |
|---|---| |---|---|
| git | `homelab-infra` is hosted on the local Gitea; an independent off-site mirror is still missing | | git | `homelab-infra` is hosted on the local Gitea; an independent off-site mirror is still missing |
| stage 1 | Live and green on the Gitea runner — `yamllint`, `ansible-lint`, `terraform fmt`/`validate` | | stage 1 | Gitea runner 上的 `yamllint`、`ansible-lint`、Terraform fmt/validate 已上线;三个 workflow 按路径触发,feature push 不再与 PR 事件重复运行 |
| gitea | 1.25.5, Actions enabled, `DEFAULT_ACTIONS_URL=github`; one instance-scoped Kubernetes runner is deployed with capacity four | | gitea | 1.27.3,Actions 已启用;一个 instance-scoped Kubernetes runner 以 capacity 4 运行 |
| ansible | 33 roles across `infrastructure/proxmox/`, `infrastructure/samba-ad/`, `infrastructure/openbao/` | | ansible | 33 roles across `infrastructure/proxmox/`, `infrastructure/samba-ad/`, `infrastructure/openbao/` |
| terraform | 4 roots, **local state**, each with **different interactive auth** (`bao login -method=oidc`, `az login`) | | terraform | 4 roots, **local state**, each with **different interactive auth** (`bao login -method=oidc`, `az login`) |
| k8s | ~13 Helm releases, all deployed by hand | | k8s | Flux 已接管 Gitea、Gitea Actions 与 http-echo canary;其余 brownfield release 逐项迁移 |
| secrets | 4 config files are gitignored because they embed live secrets, so their contents are **not** version controlled | | secrets | 4 config files are gitignored because they embed live secrets, so their contents are **not** version controlled |
## The four stages ## The four stages
**Stage 1 — static. Built.** **Stage 1 — static. Built.**
`yamllint`, `ansible-lint`, `terraform fmt -check` / `validate -backend=false`. `yamllint`, `ansible-lint`, `terraform fmt -check` / `validate -backend=false`.
No cluster, no credentials, no mutation, so it is safe on every push. It already No cluster, no credentials, no mutation. YAML、Ansible 和 Terraform 各自按相关路径
触发;feature branch 只由 `pull_request` 检查,合并后再由 `main` push 检查,避免
同一 revision 因 branch push 和 PR 各跑一遍。它已经
found a real defect: `infrastructure/proxmox/ansible/` had no `requirements.yml` at all, so a found a real defect: `infrastructure/proxmox/ansible/` had no `requirements.yml` at all, so a
fresh checkout could not reproduce its collections. fresh checkout could not reproduce its collections.
+174
View File
@@ -0,0 +1,174 @@
# Gitea 1.25.5 → 1.27.3 升级计划
## 决策
现有 release 为 chart `12.5.3` / Gitea `1.25.5`,已经由 Flux HelmRelease 接管。
升级分两个独立阶段执行,不跨过中间 minor:
1. chart `12.6.0`,显式设置 `image.tag: 1.26.4`;
2. chart `12.7.0`,显式设置 `image.tag: 1.27.3`。
chart 当前默认 appVersion 分别只是 `1.26.1` 和 `1.27.0`,因此不能依赖默认镜像。
官方 `1.26.4` 修复了 1.26.3 的代码页回归并包含安全修复;`1.27.3` 又修复了
Actions fork PR 审批绕过等安全问题。两个 rootless 镜像标签都已经在官方 registry
验证存在。Gitea Runner `2.3.0` 已高于 1.27 推荐的 `2.0.0`,无需与服务端同时升级。
参考:
- [Gitea 升级说明](https://docs.gitea.com/installation/upgrade-from-gitea/)
- [Gitea 备份一致性说明](https://docs.gitea.com/1.26/administration/backup-and-restore/)
- [Gitea 1.26.4 发布说明](https://blog.gitea.com/release-of-1.26.3-and-1.26.4/)
- [Gitea 1.27.0 与 Runner 兼容说明](https://blog.gitea.com/release-of-1.27.0/)
- [Gitea 1.27.3 发布说明](https://blog.gitea.com/release-of-1.27.3/)
## 当前风险边界
- Gitea 使用外部单实例 CNPG;`gitea` 数据库约 19 MB,CNPG 没有配置连续备份,
`firstRecoverabilityPoint` 为空。
- `/data` 使用 `local-path` RWO PVC,实际数据约 6.2 MB;集群没有
VolumeSnapshotClass,因此不能把 CSI snapshot 当成回滚点。
- Gitea 是 Flux GitRepository 的上游。Gitea 停机时已应用的资源继续运行,但不能
依靠新的 Git commit 解救失败的 Gitea。
- 跨 minor 启动会执行数据库 migration。官方明确说明升级后的数据库不能由旧 minor
安全使用;回退必须同时恢复数据库和 `/data`。
- `gitea-oidc-secret` 仍是手工 Secret。本次只继续引用它,不与版本升级一起迁移。
## 已完成的静态检查
- 使用现有 `gitea-values.yaml` 渲染三个 chart,并从比较中排除 Secret 与 Helm test
hook;`12.5.3 → 12.6.0` 的业务资源变化只有 chart/app 标签和四处 Gitea 镜像。
- `12.6.0 → 12.7.0` 除同类版本变化外,只包含 Service template 文件重命名、字段
排版变化和 test hook 参数排版,没有新增或删除 live 业务对象。
- `docker.gitea.com/gitea:1.26.4-rootless` 与
`docker.gitea.com/gitea:1.27.3-rootless` 的 multi-arch manifests 均存在。
- 升级前没有 deprecation 或 error;启动会报告若干数据库 default 比较及内网明文 SMTP
warning,均为已有状态。HTTPRoute、内部 API、统一域名 API 与 Flux GitRepository
均 Ready。
## 每个 minor 的执行单元
两个阶段各自重复以下流程。1.26 验收完成前不得创建 1.27 的执行 PR。
### 1. 预拉取与 review
1. 在 HelmRelease 中显式设置目标 `image.tag`,同时把 chart 固定到该阶段目标版本,
并重新执行 Helm/Kustomize render。目标 chart、镜像与 `suspend: true` 必须在同一
HelmRelease 对象内原子提交;不得同时修改会触发 watch 的 values ConfigMap。
2. 在节点预拉取目标 rootless 镜像,避免维护窗口受 WAN 波动影响。
3. 创建 **保持 `spec.suspend: true`** 的准备 PR;合并并等 Flux 同步。此时 Git 只记录
目标版本,不执行 Helm action。
4. 从该准备 commit 创建只删除 `suspend` 的激活 PR,提前 push 到 Gitea,但暂不合并。
### 2. 建立一致回滚点
Gitea 官方要求停机备份才能保证数据库、仓库、LFS、附件和 metadata 一致。本环境
数据量很小,因此接受一个短维护窗口,不使用在线 `gitea dump` 冒充一致备份。
1. 确认 HelmRelease 已 suspended,记录 Deployment、ReplicaSet、Pod UID、Helm
revision、数据库大小和 PVC 路径。
2. 手工将 Gitea Deployment scale 到 0 并等待 Pod 消失。此时 runner 会暂时无法上报,
Flux source 会暂时 NotReady,但现有工作负载不受影响。
3. 对 CNPG 的 `gitea` 数据库执行 custom-format `pg_dump`;对 PVC host path 创建保留
owner/xattr 的 tar archive。两个文件写入节点上新建的、权限为 0700 的时间戳目录。
4. 生成 SHA-256,执行 `pg_restore --list` 和 `tar --list`,确认两份文件可读。
5. 用个人 GPG 公钥加密后复制一份到 OCI Object Storage(或另一台物理机);未验证
第二份副本前不得升级。
6. 将 Deployment scale 回 1,确认仍以旧版本启动,并验证 API、OIDC 登录、clone、
push 和 Actions runner online。
手工 scale 只用于取得一致备份;HelmRelease 此时 suspended,不会发生 drift repair。
备份结束后服务恢复,后续激活 PR 仍通过正常 Gitea review/merge,不需要在 Gitea 停机
时修改 Git 或 live HelmRelease。
### 3. 由 Flux 升级
1. review 并合并已经 push 的激活 PR;它唯一的行为变化应是删除 `suspend: true`。
2. 不手工 reconcile,观察 Flux 正常发现 revision、Helm upgrade 和 Gitea migration。
3. `Recreate` strategy 会先终止旧 Pod 再启动新 Pod,避免 RWO PVC 与 leveldb queue
lock 导致双 Pod deadlock。
4. 若 15 分钟内 HelmRelease 未 Ready,停止自动重试并进入回滚判断,不连续修改
values 猜测修复。
### 4. 每阶段验收
- HelmRelease `Ready=True`、`UpgradeSucceeded`,Helm revision 只增加一次;
- Deployment 和 Pod 使用目标 rootless 镜像,PVC UID/volume name 不变;
- Gitea `/api/v1/version` 从 Pod 内和 `https://git.ddupan.top` 返回目标版本;
- Authelia OIDC 管理员登录与 break-glass 本地管理员登录均有效;
- 对现有仓库完成 HTTPS clone、创建临时 branch、push、删除临时 branch;
- 当前仓库 Actions workflow 能入队并由 runner `2.3.0` 完成;
- Flux GitRepository 恢复 Ready 并能拉取升级 revision;
- package registry、Terraform package/state API(启用后)与附件/LFS 按实际使用情况抽查;
- 日志中没有 migration failure、panic、持续数据库错误或配置弃用警告;
- 稳定观察至少 15 分钟,再把该阶段记录为完成。
## 回滚
patch 版本原则上保持数据库结构兼容;本计划的两个步骤都是跨 minor,不能仅回退镜像。
若 migration 后目标版本无法健康启动:
1. 暂停 `Kustomization/gitea` 和 `HelmRelease/gitea`,停止 Gitea Deployment;
2. 保存失败现场的 Pod logs、Helm status 与 migration error;
3. 删除并从同一阶段备份恢复 `gitea` 数据库;
4. 清空并从配套 tar 恢复原 PVC 内容,保持 owner、mode 和 xattr;
5. 将 live HelmRelease 临时恢复到上一阶段 chart/image,并启动验证;
6. Gitea 恢复后,通过 PR 把 Git desired state 恢复到上一阶段,再恢复 Flux
Kustomization。不得让 Flux 在数据库尚未恢复时自动拉回新版本。
数据库 dump 与 PVC archive 是一个不可拆分的回滚点;禁止混用不同时间或不同阶段的
两份备份。
## 1.26.4 阶段执行记录
- 目标 `1.26.4-rootless` 镜像已经预拉取到 k3s 节点;
- Gitea 停写后生成了 custom-format PostgreSQL dump 与保留 owner/ACL/xattr 的 PVC
tar,两者分别通过 `pg_restore --list`、`tar --list` 和 SHA-256 校验;
- 本地回滚点位于
`/var/backups/homelab/gitea/20260910T060400Z-1.25.5/`,权限为 0700;其中另有使用
个人 GPG 公钥加密并通过 packet 检查的 bundle;
- 操作者明确选择不上传 OCI,因此本阶段接受只有节点本地副本的风险例外;
- 备份后旧版 `1.25.5` 已恢复,Pod 内与统一域名 API、Gitea API 和 Flux Git source
均验证正常。
激活后 Helm revision 16 以 chart `12.6.0` 成功部署
`docker.gitea.com/gitea:1.26.4-rootless`。Migration 323–330 完成;Pod 内和统一域名
API、临时 branch push/delete、Flux source,以及 main/smoke 的 YAML、Ansible、
Terraform CI 均通过。Pod 在约 15 分钟观察期内保持 Running、零重启;Authelia OIDC
配置 init 同步及浏览器交互式管理员登录也已确认成功。
## 1.27.3 阶段准备状态
- 目标 `1.27.3-rootless` 镜像已经预拉取到 k3s 节点;
- Git desired state 固定为 chart `12.7.0` 与显式 image `1.27.3`,并重新设置
`suspend: true`;
- 合并准备状态后必须从已经迁移的 1.26.4 数据重新建立一组数据库/PVC 回滚点,禁止
复用 1.25.5 备份作为 1.27 阶段的直接回滚点。
执行时操作者明确选择跳过上述 1.26.4 备份门槛并继续激活。该风险例外意味着 1.27
migration 后若失败,不能无损回到 1.26.4;现有本地 1.25.5 备份只可用于接受丢失
第一跳之后状态的灾难恢复。目标 1.27.3 镜像已预拉取,其余准备检查均已通过。
激活后 Helm revision 17 以 chart `12.7.0` 成功部署实际镜像 `1.27.3-rootless`,
migration 331–342 和全部 init containers 成功;Pod 内/统一域名 API、Git pull、临时
branch push/delete 与 Flux source 均通过,Pod Ready 且零重启。Chart metadata 显示
appVersion `1.27.0`,实际版本以固定 image 和 API 返回的 `1.27.3` 为准。
## 后续升级策略
本次连续两次跨 minor 升级证明现有 chart、外部 CNPG、rootless PVC 和 `Recreate`
组合工作稳定。后续常规 patch/minor 升级采用精简验收:固定 chart/image、审阅相关
release notes、render、确认 `UpgradeSucceeded`/Pod Ready/API 版本,再人工抽查 OIDC
与 Git。长时间观察、临时 branch、重复 CI、逐条 migration 日志和逐 minor 停机备份
不再是默认步骤。
以下任一条件出现时恢复本文的完整流程:数据库或存储变更、rootless/权限模型变化、
PVC identity 或 deployment strategy 变化、重大 chart 结构变化、相关 breaking migration,
以及任何启动失败、CrashLoop 或 migration error。
## 后续但不并入升级
- 将 `gitea-oidc-secret` 等剩余手工 Secret 迁入 OpenBao/ESO;
- Gitea 1.27 稳定后验证最小权限 Actions job token,再实现无集群凭据的 PR render
diff/summary;live `kubectl diff` 仍使用独立、受信任且限权的执行路径;
- 单独评估关闭 chart 生成但无人处理的 Ingress;
- 为 CNPG 和 Gitea `/data` 建立周期性、异机可恢复备份,替代升级前一次性备份。
+4 -5
View File
@@ -213,11 +213,10 @@ offline break-glass path. ESO-generated Secrets are projections, not backups.
4. **Complete:** the reviewed Gitea Runner and Stage 1 CI are live. Flux deploys 4. **Complete:** the reviewed Gitea Runner and Stage 1 CI are live. Flux deploys
`http-echo`; automatic deployment, replica drift repair and scoped deletion `http-echo`; automatic deployment, replica drift repair and scoped deletion
were verified. Root prune remains disabled for brownfield safety. were verified. Root prune remains disabled for brownfield safety.
5. **Next:** adopt the existing `gitea-actions` Helm release with the same chart 5. **In progress:** Gitea Actions and Gitea are managed by Flux. Adopt External
`0.1.1` and repository values before attempting an upgrade. Its live Secrets Operator next with its existing chart `2.8.0` and repository values;
StatefulSet and Git both use regular DinD, while Helm's stored release values register the suspended release first, then activate it in a separate PR after
still describe the failed rootless attempt. The first Flux reconciliation proving the fixed render matches Helm's stored manifest.
must repair this stored-state drift without changing the live Pod spec.
6. Move Tunnel origins to Envoy and consolidate split DNS through Blocky. 6. Move Tunnel origins to Envoy and consolidate split DNS through Blocky.
7. Deploy Backstage read-only with Catalog, Kubernetes, Flux and TechDocs. 7. Deploy Backstage read-only with Catalog, Kubernetes, Flux and TechDocs.
8. Reconstruct the OCI root to a zero-change plan and add libvirt drift reports. 8. Reconstruct the OCI root to a zero-change plan and add libvirt drift reports.
+1 -1
View File
@@ -20,7 +20,7 @@ SUB-SKILL", checkbox task lists) that only ever suited one migration. Design
documents now live directly in `docs/` — see `../cicd.md` — and the split that documents now live directly in `docs/` — see `../cicd.md` — and the split that
matters is: matters is:
- `CHANGELOG.md` — what changed, for humans - `CHANGELOG.md` — frozen historical snapshot; Git commits and PRs now record changes
- `CLAUDE.md` — traps and procedures, for agents - `CLAUDE.md` — traps and procedures, for agents
- `docs/*.md` — design docs for work not yet built - `docs/*.md` — design docs for work not yet built
- `<service>/README.md` — how a service actually works - `<service>/README.md` — how a service actually works
+29
View File
@@ -0,0 +1,29 @@
# DNS 声明与权威边界
`records.yml` 是 homelab DNS 的唯一声明清单,但不是 DNS 服务本身。不同视图仍由最适合
它们的后端提供:
| 视图 | 权威或递归服务 | 配置方式 |
|---|---|---|
| 公网 `ddupan.top` | Cloudflare | Terraform;尚待完整导入已有记录 |
| AD `ad.ddupan.top` | Samba internal DNS | `samba_dns_record` Ansible module |
| LAN split horizon | Blocky | 尚待从 inventory 渲染或校验 |
| Kubernetes Pod split horizon | CoreDNS | 尚待从 inventory 渲染或校验 |
## 安全边界
- Samba module 只管理 `homelab_dns.samba.records` 明确列出的 RRset,不遍历或清理 zone。
- `_ldap`、`_kerberos`、域控制器 locator 等由 Samba 自动维护的记录不进入 inventory。
- 一个受管 RRset 默认使用 `exact: true`:同名同类型的额外值会被删除,但其他名称和类型
不受影响。
- DHCP 切换不属于本阶段。Blocky 仍未成为 LAN 客户端的正式 resolver。
## 分阶段接管
1. 用 Samba module 接管现有静态 A RRset,首次 check mode 应为零变更。
2. 将 Cloudflare 已有 tunnel DNS 记录导入 Terraform state。
3. 让 Blocky 与 CoreDNS 从 `split_horizon.records` 生成配置或执行 CI 一致性检查。
4. 验证公网、LAN、Pod、AD 四个视图后,再单独修改 DHCP。
当前 inventory 已明确暴露一个既有差异:`obj.ddupan.top` 在 Blocky 中存在,但 CoreDNS
尚无对应覆盖。本阶段不会偷偷修复它;后续在两个 resolver 同时接管时统一修复。
+46
View File
@@ -0,0 +1,46 @@
---
# Homelab DNS desired state. This file is the canonical inventory; individual
# backends consume only the views they own.
homelab_dns:
samba:
# Samba remains authoritative for the AD zone. Only these explicitly listed
# RRsets are reconciled; Samba-generated AD/Kerberos records are untouched.
records:
- { zone: ad.ddupan.top, name: bao, type: A, values: [192.168.10.8] }
- { zone: ad.ddupan.top, name: pve1, type: A, values: [192.168.10.4] }
- { zone: ad.ddupan.top, name: pve2, type: A, values: [192.168.10.7] }
- { zone: ad.ddupan.top, name: pve3, type: A, values: [192.168.10.9] }
- { zone: ad.ddupan.top, name: retrolab, type: A, values: [10.60.0.10] }
- { zone: ad.ddupan.top, name: netbox, type: A, values: [192.168.10.127] }
- { zone: ad.ddupan.top, name: s3, type: A, values: [192.168.10.127] }
- { zone: ad.ddupan.top, name: spire-oidc, type: A, values: [192.168.10.127] }
- { zone: ad.ddupan.top, name: zot, type: A, values: [192.168.10.127] }
split_horizon:
# LAN and pod resolvers should eventually render the same set from here.
# Adoption of Blocky/CoreDNS is deliberately a separate change.
records:
- { name: git.ddupan.top, type: A, values: [192.168.10.127] }
- { name: auth.ddupan.top, type: A, values: [192.168.10.127] }
- { name: obj.ddupan.top, type: A, values: [192.168.10.127] }
public:
# Names expected at Cloudflare. Terraform adoption is a separate change;
# complete RRsets here make the current ownership gap explicit.
records:
- name: auth.ddupan.top
type: CNAME
values: [ff392451-b0b1-45bb-964e-6d9372c3a9e3.cfargotunnel.com]
proxied: true
- name: git.ddupan.top
type: CNAME
values: [ff392451-b0b1-45bb-964e-6d9372c3a9e3.cfargotunnel.com]
proxied: true
- name: obj.ddupan.top
type: CNAME
values: [ff392451-b0b1-45bb-964e-6d9372c3a9e3.cfargotunnel.com]
proxied: true
- name: e5renew.ddupan.top
type: CNAME
values: [ff392451-b0b1-45bb-964e-6d9372c3a9e3.cfargotunnel.com]
proxied: true
+6
View File
@@ -36,6 +36,7 @@ configure any secrets engines/auth methods — that is a separate bootstrap play
``` ```
terraform/ # OpenBao's API-level CONFIGURATION (see below) terraform/ # OpenBao's API-level CONFIGURATION (see below)
mounts.tf pki.tf ssh.tf auth.tf policies.tf mounts.tf pki.tf ssh.tf auth.tf policies.tf
auth-spire.tf # SPIFFE JWT-SVID -> short-lived Bao tokens
imports.tf # adopts the already-running instance into state imports.tf # adopts the already-running instance into state
policies/*.hcl # policy bodies, kept diffable policies/*.hcl # policy bodies, kept diffable
``` ```
@@ -205,6 +206,11 @@ after this, use `BAO_ADDR=https://bao.ad.ddupan.top:8200` (no skip-verify), not
## Using it ## Using it
Kubernetes workload 不接收长期 `BAO_TOKEN`:它通过 SPIRE Workload API 获取
JWT-SVID,再经 `auth/jwt-spire/login` 换取短期、最小权限 token。完整接入流程、
manifest、exchange 脚本、安全要求和排障方法见
[`../../platform/spire/RUNBOOK.md`](../../platform/spire/RUNBOOK.md)。
```bash ```bash
# human: log in via Authelia (2FA) # human: log in via Authelia (2FA)
bao login -method=oidc # browser → auth.ddupan.top bao login -method=oidc # browser → auth.ddupan.top
@@ -0,0 +1,27 @@
# Workload authentication via SPIFFE JWT-SVIDs. The discovery document and
# JWKS contain public verification material, so this backend carries no secret.
resource "vault_jwt_auth_backend" "spire" {
path = "jwt-spire"
description = "SPIFFE JWT-SVID workload authentication"
oidc_discovery_url = "https://spire-oidc.ad.ddupan.top"
bound_issuer = "https://spire-oidc.ad.ddupan.top"
}
# First end-to-end identity. Keep the subject exact: this role is deliberately
# not a wildcard escape hatch for every workload in the trust domain.
resource "vault_jwt_auth_backend_role" "spire_poc" {
backend = vault_jwt_auth_backend.spire.path
role_name = "spire-poc"
role_type = "jwt"
user_claim = "sub"
bound_audiences = ["openbao"]
bound_claims = {
sub = "spiffe://ddupan.top/ns/spire-poc/sa/spire-jwt-poc"
}
token_policies = [vault_policy.spire_poc.name]
token_no_default_policy = true
token_ttl = 300
token_max_ttl = 900
}
@@ -14,6 +14,11 @@ resource "vault_policy" "ai_agent_ssh" {
policy = file("${path.module}/policies/ai-agent-ssh.hcl") policy = file("${path.module}/policies/ai-agent-ssh.hcl")
} }
resource "vault_policy" "spire_poc" {
name = "spire-poc"
policy = file("${path.module}/policies/spire-poc.hcl")
}
resource "vault_policy" "snapshot" { resource "vault_policy" "snapshot" {
name = "snapshot" name = "snapshot"
policy = file("${path.module}/policies/snapshot.hcl") policy = file("${path.module}/policies/snapshot.hcl")
@@ -0,0 +1,10 @@
# Intentionally grants no secret access. This policy proves that an exact
# SPIFFE ID can exchange a JWT-SVID for a bounded OpenBao token and inspect or
# revoke only that token.
path "auth/token/lookup-self" {
capabilities = ["read"]
}
path "auth/token/revoke-self" {
capabilities = ["update"]
}
+4
View File
@@ -54,6 +54,7 @@ second run is a no-op there and only reconciles config drift.
|---|---| |---|---|
| `roles/dc_vm/` | creates the DC VM locally via libvirt + cloud-init | | `roles/dc_vm/` | creates the DC VM locally via libvirt + cloud-init |
| `roles/samba_ad_dc/` | provisions the DC (§1) — idempotent | | `roles/samba_ad_dc/` | provisions the DC (§1) — idempotent |
| `ansible/collections/ansible_collections/ddupan/homelab/` | repository-local collection containing the Samba DNS module and its `ansible-test` tests |
| `roles/samba_ad_dc/tasks/verify.yml` | smoke tests (`--tags verify`) | | `roles/samba_ad_dc/tasks/verify.yml` | smoke tests (`--tags verify`) |
| `roles/samba_ad_dc/tasks/legacy.yml` | opt-in retro-client protocols (§ Retro clients) | | `roles/samba_ad_dc/tasks/legacy.yml` | opt-in retro-client protocols (§ Retro clients) |
| `roles/windows_vm/` | unattended-installs the Windows Server 2025 admin box (§2.0) | | `roles/windows_vm/` | unattended-installs the Windows Server 2025 admin box (§2.0) |
@@ -62,6 +63,9 @@ second run is a no-op there and only reconciles config drift.
| `group_vars/all/vault.yml` | admin passwords (gitignored; encrypt with ansible-vault) | | `group_vars/all/vault.yml` | admin passwords (gitignored; encrypt with ansible-vault) |
| `inventory/hosts.yml` | DC + Windows hosts | | `inventory/hosts.yml` | DC + Windows hosts |
Static DNS records are declared once in `../../dns/records.yml`. The role manages
only those RRsets and deliberately leaves Samba-generated AD locator records alone.
--- ---
## 0. Decisions — fill these in before touching anything ## 0. Decisions — fill these in before touching anything
@@ -1,6 +1,7 @@
[defaults] [defaults]
inventory = inventory/hosts.yml inventory = inventory/hosts.yml
roles_path = roles roles_path = roles
collections_paths = collections
host_key_checking = False host_key_checking = False
callback_result_format = yaml callback_result_format = yaml
nocows = True nocows = True
@@ -0,0 +1,4 @@
# ddupan.homelab
Repository-local Ansible collection for homelab infrastructure modules. It is consumed
directly through the `collections_paths` setting and is not published.
@@ -0,0 +1,16 @@
---
namespace: ddupan
name: homelab
version: 0.1.0
readme: README.md
authors:
- panxiao81
description: Ansible plugins used to manage the ddupan.top homelab.
license:
- GPL-3.0-or-later
tags:
- infrastructure
- dns
repository: https://git.ddupan.top/panxiao81/homelab-infra
build_ignore:
- .git
@@ -0,0 +1,159 @@
#!/usr/bin/python
# Copyright: (c) 2026, homelab-infra contributors
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
DOCUMENTATION = r"""
---
module: samba_dns_record
short_description: Reconcile one Samba internal DNS RRset
author:
- Pan Xiao (@panxiao81)
description:
- Reconciles only the named RRset and never deletes an undeclared zone or name.
- Runs C(samba-tool dns) on a Samba AD domain controller using its machine account.
options:
server:
description: DNS server accepted by C(samba-tool dns).
type: str
required: true
zone:
description: Authoritative DNS zone.
type: str
required: true
name:
description: Record owner relative to the zone.
type: str
required: true
type:
description: DNS record type.
type: str
choices: [A, AAAA, CNAME, PTR, TXT]
required: true
values:
description: Complete desired value set for this owner and type.
type: list
elements: str
default: []
state:
description: Whether the desired RRset is present or absent.
type: str
choices: [present, absent]
default: present
exact:
description: Remove live values not present in C(values).
type: bool
default: true
"""
EXAMPLES = r"""
- name: Reconcile an A RRset
samba_dns_record:
server: dc1.ad.ddupan.top
zone: ad.ddupan.top
name: pve1
type: A
values: [192.168.10.4]
exact: true
"""
RETURN = r"""
before:
description: Values observed before reconciliation.
type: list
returned: always
after:
description: Values expected after reconciliation.
type: list
returned: always
"""
import re
from ansible.module_utils.basic import AnsibleModule
ABSENT_ERRORS = (
"WERR_DNS_ERROR_NAME_DOES_NOT_EXIST",
"WERR_DNS_ERROR_RECORD_DOES_NOT_EXIST",
"WERR_DNS_ERROR_NXDOMAIN",
)
def normalize_value(record_type, value):
value = value.strip()
if record_type in ("CNAME", "PTR"):
return value.rstrip(".").lower()
if record_type == "TXT" and len(value) >= 2 and value[0] == value[-1] == '"':
return value[1:-1]
return value.lower() if record_type == "AAAA" else value
def parse_query(stdout, record_type):
values = []
prefix = re.compile(r"^\s*%s:\s*(.*?)\s*(?:\([^)]*\))?\s*$" % re.escape(record_type))
for line in stdout.splitlines():
match = prefix.match(line)
if match:
values.append(normalize_value(record_type, match.group(1)))
return sorted(set(values))
def run(module, args, check_rc=True):
command = [module.get_bin_path("samba-tool", required=True), "dns"] + args + ["-P"]
return module.run_command(command, check_rc=check_rc)
def main():
module = AnsibleModule(
argument_spec=dict(
server=dict(type="str", required=True),
zone=dict(type="str", required=True),
name=dict(type="str", required=True),
type=dict(type="str", required=True, choices=["A", "AAAA", "CNAME", "PTR", "TXT"]),
values=dict(type="list", elements="str", default=[]),
state=dict(type="str", choices=["present", "absent"], default="present"),
exact=dict(type="bool", default=True),
),
supports_check_mode=True,
)
p = module.params
desired = sorted(set(normalize_value(p["type"], value) for value in p["values"]))
if p["state"] == "present" and not desired:
module.fail_json(msg="values must not be empty when state=present")
rc, stdout, stderr = run(
module,
["query", p["server"], p["zone"], p["name"], p["type"]],
check_rc=False,
)
if rc == 0:
current = parse_query(stdout, p["type"])
elif any(marker in stdout + stderr for marker in ABSENT_ERRORS):
current = []
else:
module.fail_json(msg="samba-tool dns query failed", rc=rc, stdout=stdout, stderr=stderr)
target = desired if p["state"] == "present" else []
additions = sorted(set(target) - set(current))
removals = sorted(set(current) - set(target)) if p["exact"] or p["state"] == "absent" else []
changed = bool(additions or removals)
if changed and not module.check_mode:
for value in removals:
run(module, ["delete", p["server"], p["zone"], p["name"], p["type"], value])
for value in additions:
run(module, ["add", p["server"], p["zone"], p["name"], p["type"], value])
after = sorted((set(current) - set(removals)) | set(additions))
module.exit_json(
changed=changed,
before=current,
after=after,
diff={"before": {p["type"]: current}, "after": {p["type"]: after}},
)
if __name__ == "__main__":
main()
@@ -0,0 +1,23 @@
import unittest
from ansible_collections.ddupan.homelab.plugins.modules import samba_dns_record
class ParseQueryTests(unittest.TestCase):
def test_parses_a_records_and_ignores_other_types(self):
output = """ Name=host, Records=3, Children=0
A: 192.168.10.4 (flags=f0, serial=1, ttl=900)
A: 192.168.10.7 (flags=f0, serial=2, ttl=900)
TXT: ignored (flags=f0, serial=3, ttl=900)
"""
self.assertEqual(
samba_dns_record.parse_query(output, "A"),
["192.168.10.4", "192.168.10.7"],
)
def test_normalizes_dns_targets(self):
output = " CNAME: Gateway.AD.DDUPAN.TOP. (flags=f0, serial=1, ttl=900)\n"
self.assertEqual(
samba_dns_record.parse_query(output, "CNAME"),
["gateway.ad.ddupan.top"],
)
@@ -10,29 +10,8 @@ samba_ad_dc_ip: "192.168.10.5"
samba_ad_dns_forwarder: "192.168.10.1" samba_ad_dns_forwarder: "192.168.10.1"
samba_ad_reverse_zone: "10.168.192.in-addr.arpa" # reverse of 192.168.10.0/24 samba_ad_reverse_zone: "10.168.192.in-addr.arpa" # reverse of 192.168.10.0/24
# Extra A records for non-domain hosts published in the AD DNS zone. # Static records now live in ../../dns/records.yml and are reconciled as complete
samba_ad_extra_a_records: # RRsets by the local samba_dns_record module.
- { name: "bao", ip: "192.168.10.8" } # OpenBao (../openbao), not domain-joined
# Proxmox cluster nodes (../proxmox). Not domain-joined; they authenticate
# USERS against this DC rather than being members themselves.
- { name: "pve1", ip: "192.168.10.4" }
- { name: "pve2", ip: "192.168.10.7" }
- { name: "pve3", ip: "192.168.10.9" }
# Lab VMs on the SDN VNets (routed via the VyOS router, see ../../proxmox).
# These are NOT on 192.168.10.0/24, so they have no PTR in the existing
# reverse zone — forward resolution only unless a 0.60.10.in-addr.arpa zone
# is added later.
- { name: "retrolab", ip: "10.60.0.10" }
# k3s services exposed on the LAN through the Envoy gateway (../../../platform/envoy-gateway;
# Contour was retired 2026-07-25). They all point at the k3s node, which is where
# Envoy's LoadBalancer lands; the gateway routes by Host header and serves the
# *.ad.ddupan.top wildcard cert.
# Adding another such service = one more line here + an HTTPRoute, nothing else.
- { name: "netbox", ip: "192.168.10.127" } # NetBox (../../../apps/netbox)
# SeaweedFS S3. Exists so Terraform state does NOT ride the Cloudflare tunnel:
# obj.ddupan.top works, but it hairpins through the WAN, and on 2026-07-28 that
# path was blackholed for hours by a dead VPN tunnel. State must stay on the LAN.
- { name: "s3", ip: "192.168.10.127" } # SeaweedFS S3 (../../../apps/seaweedfs)
# Support legacy clients (Win9x/NT4/2000/XP)? INSECURE — see README "Retro clients". # Support legacy clients (Win9x/NT4/2000/XP)? INSECURE — see README "Retro clients".
samba_ad_legacy_clients: false samba_ad_legacy_clients: false
@@ -6,6 +6,8 @@
hosts: samba_dc hosts: samba_dc
become: true become: true
gather_facts: true gather_facts: true
vars_files:
- ../../dns/records.yml
roles: roles:
- role: samba_ad_dc - role: samba_ad_dc
post_tasks: post_tasks:
@@ -0,0 +1,14 @@
---
- name: Reconcile explicitly managed AD DNS RRsets
ddupan.homelab.samba_dns_record:
server: "{{ samba_ad_dc_ip }}"
zone: "{{ item['zone'] }}"
name: "{{ item['name'] }}"
type: "{{ item['type'] }}"
values: "{{ item['values'] }}"
state: present
exact: true
loop: "{{ homelab_dns.samba.records }}"
loop_control:
label: "{{ item['name'] }}.{{ item['zone'] }} {{ item['type'] }}"
tags: [dns]
@@ -198,20 +198,11 @@
- "'already exists' not in (kms_srv.stderr | default('')) + (kms_srv.stdout | default(''))" - "'already exists' not in (kms_srv.stderr | default('')) + (kms_srv.stdout | default(''))"
no_log: true no_log: true
# --- Extra A records for non-domain hosts (e.g. OpenBao) ----------------------- # --- Directory objects and explicitly managed DNS records ----------------------
- name: Service accounts and RBAC groups - name: Service accounts and RBAC groups
ansible.builtin.import_tasks: directory_objects.yml ansible.builtin.import_tasks: directory_objects.yml
tags: [directory, accounts] tags: [directory, accounts]
- name: Register extra A records in the AD DNS zone - name: Reconcile static AD DNS records
ansible.builtin.command: ansible.builtin.import_tasks: dns_records.yml
cmd: >-
samba-tool dns add {{ samba_ad_dc_ip }} {{ samba_ad_realm | lower }}
{{ item.name }} A {{ item.ip }} -P
loop: "{{ samba_ad_extra_a_records }}"
register: extra_a
changed_when: "'Record added successfully' in (extra_a.stdout | default(''))"
failed_when:
- extra_a.rc != 0
- "'already exists' not in (extra_a.stderr | default('')) + (extra_a.stdout | default(''))"
tags: [dns] tags: [dns]
+12
View File
@@ -51,6 +51,18 @@ the only challenge that can issue a wildcard.
## Deploy ## Deploy
### Flux 接管状态
现有 release 为 chart/app `v1.21.0`,Git 中固定同一版本并已由 Flux HelmRelease
完成接管。第一阶段暂停登记后确认 chart artifact Ready、Helm revision 保持为 1,且
三个 workload Pod 均未被替换;随后通过独立 PR 解除暂停。
Issuer 与 Certificate 清单随本目录的 Kustomization 由 Flux 管理;现有 Cloudflare
token Secret 只被引用,本次接管不改变其所有权。删除保护期间保持 `prune: false`,且
CRD 同时启用 chart 的 `crds.keep` 与 Flux Helm action 的 `CreateReplace`。
以下命令保留为 break-glass 手工恢复流程;正常变更应提交 Git:
```bash ```bash
helm repo add jetstack https://charts.jetstack.io && helm repo update jetstack helm repo add jetstack https://charts.jetstack.io && helm repo update jetstack
helm upgrade --install cert-manager jetstack/cert-manager --version v1.21.0 \ helm upgrade --install cert-manager jetstack/cert-manager --version v1.21.0 \
+33
View File
@@ -0,0 +1,33 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: cert-manager
namespace: cert-manager
spec:
chart:
spec:
chart: cert-manager
interval: 1h
sourceRef:
kind: HelmRepository
name: jetstack
version: v1.21.0
driftDetection:
mode: enabled
install:
crds: CreateReplace
strategy:
name: RetryOnFailure
retryInterval: 5m
interval: 30m
releaseName: cert-manager
targetNamespace: cert-manager
timeout: 10m
upgrade:
crds: CreateReplace
strategy:
name: RetryOnFailure
retryInterval: 5m
valuesFrom:
- kind: ConfigMap
name: cert-manager-values
@@ -0,0 +1,8 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: jetstack
namespace: cert-manager
spec:
interval: 1h
url: https://charts.jetstack.io
+19
View File
@@ -0,0 +1,19 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
generatorOptions:
disableNameSuffixHash: true
labels:
reconcile.fluxcd.io/watch: Enabled
configMapGenerator:
- name: cert-manager-values
namespace: cert-manager
files:
- values.yaml=values.yaml
resources:
- helmrepository.yaml
- helmrelease.yaml
- clusterissuer-letsencrypt.yaml
- clusterissuer-bao-acme.yaml
- certificate-wildcard-ad.yaml
- certificate-auth-ddupan.yaml
- certificate-git-ddupan.yaml
+15
View File
@@ -79,6 +79,21 @@ unauthenticated requests to an app whose auth model is "trust the header".
## Install ## Install
### Flux 接管状态
现有 release 为 OCI chart/app `v1.5.6`,且没有 user-supplied values。Git 中固定
同一版本并已完成分阶段 Flux HelmRelease 接管。第一阶段确认 Kustomization Ready、
Helm revision 保持为 1,controller/data-plane Pod UID 与入口响应均未变化后,再通过
独立 PR 解除暂停。OCI 类型 HelmRepository 是按需 chart generator,本身不产生
artifact 或 Ready condition;解除暂停后由 HelmRelease 请求并验证 chart。
`GatewayClass eg` 与 `Gateway eg` 随本目录 Kustomization 由 Flux 管理。接管期间保持
`prune: false`;Gateway API 与 Envoy Gateway CRD 使用 `CreateReplace`,延续现有 Helm
所有权且绝不通过删除 CRD 迁移。Gateway 的三个 `certificateRefs` 最初属于
`kubectl-client-side-apply`;首次 reconcile 已由 Flux 默认 SSA `Override` 接管这些在
Git 中声明且值相同的字段,没有改变 listener spec。不得用 `force: true`,它用于
不可变字段失败时删除重建资源。以下命令保留为 break-glass 手工恢复流程。
```bash ```bash
# The Gateway API CRDs may already be owned by another tool's field manager (Contour's # The Gateway API CRDs may already be owned by another tool's field manager (Contour's
# quickstart used client-side apply), which makes Helm fail with # quickstart used client-side apply), which makes Helm fail with
+30
View File
@@ -0,0 +1,30 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: envoy-gateway
namespace: envoy-gateway-system
spec:
chart:
spec:
chart: gateway-helm
interval: 1h
sourceRef:
kind: HelmRepository
name: envoy-gateway
version: v1.5.6
driftDetection:
mode: enabled
install:
crds: CreateReplace
strategy:
name: RetryOnFailure
retryInterval: 5m
interval: 30m
releaseName: envoy-gateway
targetNamespace: envoy-gateway-system
timeout: 10m
upgrade:
crds: CreateReplace
strategy:
name: RetryOnFailure
retryInterval: 5m
@@ -0,0 +1,9 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: envoy-gateway
namespace: envoy-gateway-system
spec:
interval: 1h
type: oci
url: oci://docker.io/envoyproxy
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- helmrelease.yaml
- gateway.yaml
+20
View File
@@ -0,0 +1,20 @@
# External Secrets Operator
External Secrets Operator(ESO)把 OpenBao `kv/k8s/*` 下的值投影为 Kubernetes
Secret。`ClusterSecretStore/openbao` 使用 `external-secrets` ServiceAccount 的短期
JWT 登录 OpenBao,不在 Git 中保存长期凭据。
## Flux 接管
现有 release 是 2026-07-28 手工安装的 chart `external-secrets` `2.8.0`,Helm
revision 1。接管前审计确认:本目录 `values.yaml` 与 Helm stored user values 一致;
用固定 chart 生成的 33,528 行 manifest 与 stored manifest 只有末尾空行差异。
接管分两阶段。第一阶段以 `suspend: true` 登记 HelmRepository、values ConfigMap 与
HelmRelease;合并后已确认 source Ready、release 仍为 revision 1,三个 Pod UID 保持
不变且零重启。第二阶段只移除 `suspend`,允许 Flux 修正 Helm stored ownership 并
启用 drift detection;chart、values 和子 Kustomization 的 `prune: false` 均不变。
`clustersecretstore.yaml` 和 `externalsecrets.yaml` 是现有 secret delivery intent,
本阶段故意不把它们加入该 Kustomization,避免在 Helm release 接管时同时扩大 Flux
ownership。Helm 接管稳定后再单独审计、纳管这些对象。
+35 -2
View File
@@ -101,6 +101,7 @@ spec:
# The chart normally GENERATES seaweedfs-s3-secret from s3.credentials. We point # The chart normally GENERATES seaweedfs-s3-secret from s3.credentials. We point
# filer.s3.existingConfigSecret at this one instead, so the chart stops rendering # filer.s3.existingConfigSecret at this one instead, so the chart stops rendering
# credentials from values entirely. # credentials from values entirely.
# zot 的 AK/SK 只保存在 k8s/zot-s3;在此组装服务端配置,不在基础配置中维护副本。
apiVersion: external-secrets.io/v1 apiVersion: external-secrets.io/v1
kind: ExternalSecret kind: ExternalSecret
metadata: metadata:
@@ -114,6 +115,38 @@ spec:
target: target:
name: seaweedfs-s3-config name: seaweedfs-s3-config
creationPolicy: Owner creationPolicy: Owner
dataFrom: template:
- extract: engineVersion: v2
mergePolicy: Replace
data:
seaweedfs_s3_config: |-
{{- $config := mustFromJson .baseConfig -}}
{{- if not (kindIs "slice" $config.identities) -}}
{{- fail "base S3 configuration must contain an identities array" -}}
{{- end -}}
{{- if or (eq .zotAccessKey "") (eq .zotSecretKey "") -}}
{{- fail "zot S3 credentials must not be empty" -}}
{{- end -}}
{{- $identities := list -}}
{{- range $config.identities -}}
{{- if ne .name "zot" -}}
{{- $identities = append $identities . -}}
{{- end -}}
{{- end -}}
{{- $credential := dict "accessKey" .zotAccessKey "secretKey" .zotSecretKey -}}
{{- $zot := dict "name" "zot" "credentials" (list $credential) "actions" (list "Read:zot" "Write:zot" "List:zot" "Tagging:zot") -}}
{{- $_ := set $config "identities" (append $identities $zot) -}}
{{- mustToJson $config -}}
data:
- secretKey: baseConfig
remoteRef:
key: k8s/seaweedfs-s3 key: k8s/seaweedfs-s3
property: seaweedfs_s3_config
- secretKey: zotAccessKey
remoteRef:
key: k8s/zot-s3
property: access_key
- secretKey: zotSecretKey
remoteRef:
key: k8s/zot-s3
property: secret_key
@@ -0,0 +1,31 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: external-secrets
namespace: external-secrets
spec:
chart:
spec:
chart: external-secrets
interval: 1h
sourceRef:
kind: HelmRepository
name: external-secrets
version: 2.8.0
driftDetection:
mode: enabled
install:
strategy:
name: RetryOnFailure
retryInterval: 5m
interval: 30m
releaseName: external-secrets
targetNamespace: external-secrets
timeout: 10m
upgrade:
strategy:
name: RetryOnFailure
retryInterval: 5m
valuesFrom:
- kind: ConfigMap
name: external-secrets-values
@@ -0,0 +1,8 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: external-secrets
namespace: external-secrets
spec:
interval: 1h
url: https://charts.external-secrets.io
@@ -0,0 +1,14 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
generatorOptions:
disableNameSuffixHash: true
labels:
reconcile.fluxcd.io/watch: Enabled
configMapGenerator:
- name: external-secrets-values
namespace: external-secrets
files:
- values.yaml=values.yaml
resources:
- helmrepository.yaml
- helmrelease.yaml
+9
View File
@@ -7,6 +7,15 @@ would still be privileged and is blocked by the node's AppArmor user-namespace
policy, so this deployment uses regular DinD instead of weakening that host-wide policy, so this deployment uses regular DinD instead of weakening that host-wide
policy. Only trusted workflows may target this runner. policy. Only trusted workflows may target this runner.
DinD 同时使用 `--mtu=1450` 和
`--default-network-opt=bridge=com.docker.network.driver.mtu=1450`,与 k3s Pod 的
`eth0` 一致。前者只覆盖 Docker 默认 bridge;act 为每个 job 创建 user-defined
bridge,必须由后者设置默认 MTU。不要在未验证节点 Pod MTU 的情况下删除或修改这
两个参数:MTU 1500 的 job 容器虽然能够解析 GitHub、甚至建立 TCP 连接,但较大的
TLS 数据包会在嵌套网络路径中丢失,表现为 `github.com` / `api.github.com` 超时或
`setup-go` 每次请求卡满 6 分钟后重试。Pod 网络和默认 Docker bridge 正常不代表
Actions job bridge 正常。
The runner is registered at instance scope so it is available to every repository The runner is registered at instance scope so it is available to every repository
on this Gitea instance. Repository permissions and protected-branch review are on this Gitea instance. Repository permissions and protected-branch review are
therefore the security boundary; do not enable Actions for untrusted repositories. therefore the security boundary; do not enable Actions for untrusted repositories.
+8
View File
@@ -51,3 +51,11 @@ statefulset:
repository: docker repository: docker
tag: 29.7.1-dind tag: 29.7.1-dind
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
# k3s uses a 1450-byte pod MTU. Without matching it here, nested Actions
# networks advertise 1500 and GitHub TLS packets disappear on the outer
# overlay path while direct pod traffic remains healthy.
extraArgs:
- --mtu=1450
# --mtu only changes Docker's default bridge. act creates a user-defined
# bridge per job, so give every new bridge the same explicit default.
- --default-network-opt=bridge=com.docker.network.driver.mtu=1450
+28
View File
@@ -13,6 +13,34 @@ Jaeger API), Grafana (3 datasources healthy, VM dashboard loaded, Tailscale ingr
`localpv-zfs-ceph`. Note the vmagent hot-reload RBAC fix (`metrics/reload-rbac.yaml`, `localpv-zfs-ceph`. Note the vmagent hot-reload RBAC fix (`metrics/reload-rbac.yaml`,
see `metrics/README-reload.md`) and that VL/VT CRDs are served under `.../v1`. see `metrics/README-reload.md`) and that VL/VT CRDs are served under `.../v1`.
### Flux 接管状态
可观测性栈使用一个 `observability` Flux Kustomization,按依赖顺序逐步加入 Operator、
Metrics、Logs、Traces 和 Grafana,避免多个 reconciler 争夺共享 namespace、Helm source
或 VictoriaMetrics CRD。brownfield 迁移期间保持 `prune: false`。
VictoriaMetrics Operator 已固定 chart `0.66.2` / app `v0.73.1` 完成分阶段 Flux
HelmRelease 接管,保存 values 与 `operator/values.yaml` 一致。阶段一确认 Helm revision
保持为 2、Operator Pod UID 未变、24 个 CRD 及 6 个核心 VM/VL/VT CR 未变化;随后
通过独立 PR 解除暂停。
其余已部署资源也已统一完成接管:Metrics CR、rules、scrapes、node-exporter,Logs 的
VLSingle 与固定 `0.3.6` 的 Collector,以及 Traces 的 VTSingle、OTel Collector,最后是
固定 `10.5.15` 的 Grafana 与 dashboard ConfigMap。阶段一确认 11 个 Pod UID、4 个 PVC、
6 个核心 CR、17 个 rule/scrape 对象及 dashboard 哈希均未变化,Collector 和 Grafana
Helm revision 均保持为 1;随后在同一独立 PR 中解除两个 HelmRelease 的暂停。
首次 Grafana Helm reconcile 暴露了本地 ZFS RWO 卷限制:chart 默认 `RollingUpdate`
会先创建新 Pod,但 CSI 拒绝在旧 Pod 仍挂载 PVC 时再次 mount,使 upgrade 卡在
`pending-upgrade`。`grafana/values.yaml` 因此显式使用 `deploymentStrategy.type: Recreate`。
Grafana 升级会有一次短暂停机,但旧 Pod 会先退出,新 Pod 才挂载同一 PVC;不要改回
RollingUpdate,除非存储改为真正支持并发挂载的 RWX。
以下内容明确不属于本批接管:尚未部署的 kube-state-metrics、`vlogs-ingress.yaml`,以及
仅作 standalone chart 参考的 `traces-values.yaml`。`grafana/oidc-secret.yaml` 是不含真实
值的占位模板;live `grafana-oidc` Secret 继续只被 HelmRelease 引用,尚未由 OpenBao/ESO
纳管。
Still host-side (not yet done): deploy `docker-hosts/compose.yaml` on the Docker Still host-side (not yet done): deploy `docker-hosts/compose.yaml` on the Docker
hosts and `logs/vlogs-ingress.yaml` to push their logs. hosts and `logs/vlogs-ingress.yaml` to push their logs.
@@ -0,0 +1,8 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: grafana
namespace: monitoring
spec:
interval: 1h
url: https://grafana.github.io/helm-charts
@@ -0,0 +1,31 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: grafana
namespace: monitoring
spec:
chart:
spec:
chart: grafana
interval: 1h
sourceRef:
kind: HelmRepository
name: grafana
version: 10.5.15
driftDetection:
mode: enabled
install:
strategy:
name: RetryOnFailure
retryInterval: 5m
interval: 30m
releaseName: grafana
targetNamespace: monitoring
timeout: 10m
upgrade:
strategy:
name: RetryOnFailure
retryInterval: 5m
valuesFrom:
- kind: ConfigMap
name: grafana-values
@@ -0,0 +1,21 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
generatorOptions:
disableNameSuffixHash: true
configMapGenerator:
- name: grafana-values
namespace: monitoring
files:
- values.yaml=values.yaml
options:
labels:
reconcile.fluxcd.io/watch: Enabled
- name: grafana-dashboard-victoriametrics
namespace: monitoring
files:
- victoriametrics.json=dashboards/victoriametrics.json
options:
labels:
grafana_dashboard: "1"
resources:
- helmrelease.yaml
@@ -48,6 +48,12 @@ persistence:
storageClassName: localpv-zfs-ceph storageClassName: localpv-zfs-ceph
size: 5Gi size: 5Gi
# localpv-zfs-ceph cannot mount the same RWO volume into the old and new Grafana
# Pods concurrently. RollingUpdate leaves the old Pod serving while the new Pod
# blocks forever in verifyMount, so upgrades must stop the old Pod first.
deploymentStrategy:
type: Recreate
# --- Private exposure via the Tailscale ingress (like seaweedfs-admin) --- # --- Private exposure via the Tailscale ingress (like seaweedfs-admin) ---
# The tailscale operator provisions grafana.<tailnet>.ts.net and a TLS cert. # The tailscale operator provisions grafana.<tailnet>.ts.net and a TLS cert.
ingress: ingress:
@@ -0,0 +1,8 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: vm
namespace: monitoring
spec:
interval: 1h
url: https://victoriametrics.github.io/helm-charts/
+11
View File
@@ -0,0 +1,11 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- helmrepository.yaml
- grafana-helmrepository.yaml
- operator
- metrics
- logs
- traces
- grafana
@@ -0,0 +1,31 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: victoria-logs-collector
namespace: monitoring
spec:
chart:
spec:
chart: victoria-logs-collector
interval: 1h
sourceRef:
kind: HelmRepository
name: vm
version: 0.3.6
driftDetection:
mode: enabled
install:
strategy:
name: RetryOnFailure
retryInterval: 5m
interval: 30m
releaseName: victoria-logs-collector
targetNamespace: monitoring
timeout: 10m
upgrade:
strategy:
name: RetryOnFailure
retryInterval: 5m
valuesFrom:
- kind: ConfigMap
name: victoria-logs-collector-values
@@ -0,0 +1,14 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
generatorOptions:
disableNameSuffixHash: true
labels:
reconcile.fluxcd.io/watch: Enabled
configMapGenerator:
- name: victoria-logs-collector-values
namespace: monitoring
files:
- values.yaml=collector-values.yaml
resources:
- vlsingle.yaml
- helmrelease.yaml
@@ -0,0 +1,16 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- vmsingle.yaml
- vmagent.yaml
- vmalert.yaml
- vmalertmanager.yaml
- reload-rbac.yaml
- rules/vm-health.yaml
- rules/vmagent.yaml
- rules/vmalert.yaml
- rules/vmsingle.yaml
- scrapes/blocky.yaml
- scrapes/docker-hosts.yaml
- scrapes/kubelet.yaml
- exporters/node-exporter.yaml
@@ -0,0 +1,33 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: vm-operator
namespace: monitoring
spec:
chart:
spec:
chart: victoria-metrics-operator
interval: 1h
sourceRef:
kind: HelmRepository
name: vm
version: 0.66.2
driftDetection:
mode: enabled
install:
crds: CreateReplace
strategy:
name: RetryOnFailure
retryInterval: 5m
interval: 30m
releaseName: vm-operator
targetNamespace: monitoring
timeout: 10m
upgrade:
crds: CreateReplace
strategy:
name: RetryOnFailure
retryInterval: 5m
valuesFrom:
- kind: ConfigMap
name: vm-operator-values
@@ -0,0 +1,13 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
generatorOptions:
disableNameSuffixHash: true
labels:
reconcile.fluxcd.io/watch: Enabled
configMapGenerator:
- name: vm-operator-values
namespace: monitoring
files:
- values.yaml=values.yaml
resources:
- helmrelease.yaml
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- vtsingle.yaml
- otel-collector.yaml
+53
View File
@@ -0,0 +1,53 @@
# OpenEBS — 本地 ZFS 存储
OpenEBS umbrella chart 为单节点 k3s 提供 `zfs-localpv` CSI provisioner,并保留
chart 自带的 local PV、Loki、Alloy 与 MinIO 组件。业务 PVC 使用独立声明的
`localpv-zfs-ceph` StorageClass,在宿主机 ZFS pool `data/ceph` 上创建 dataset/zvol。
## 当前版本与引擎
- release:`openebs`,namespace:`openebs`
- chart/app:`4.4.0`
- ZFS LocalPV:启用
- LVM LocalPV、RawFile LocalPV、Mayastor:显式禁用
- bundled Loki SingleBinary:单副本
不要在 chart 升级时顺带启用其他存储引擎。`localpv-zfs-ceph` 当前承载 SeaweedFS、
shared PostgreSQL、NetBox、VictoriaMetrics、VictoriaLogs、VictoriaTraces、Grafana 与
RustFS 的持久卷;删除 StorageClass 不会删除已有 PV,但误删/重建 chart 资源可能中断
provisioner,因此 brownfield 接管期间保持 `prune: false`。
## Flux 接管状态
现有 Helm release 保存的 user-supplied values 与本目录 `values.yaml` 一致,并已固定
`4.4.0` 完成分阶段 Flux HelmRelease 接管。阶段一确认 source Ready、Helm revision
保持为 3、8 个 OpenEBS Pod UID 未变、10/10 ZFS PV 和集群 20/20 PVC Bound,且
`data/ceph` 下 10 个 volume 未变;随后通过独立 PR 解除暂停。
CRD 在 install/upgrade 时使用 `CreateReplace`,但绝不通过删除 CRD 迁移。接管和升级
是两个独立动作;首次激活不得改变 chart、values、StorageClass 或存储引擎。
## Break-glass 手工恢复
正常变更应提交 Git。Flux 不可用时可暂时执行:
```bash
helm upgrade --install openebs openebs/openebs --version 4.4.0 \
-n openebs --create-namespace -f platform/openebs/values.yaml
kubectl apply -f platform/openebs/storageclasses.yaml
```
恢复 Flux 后,应以 Git 为准并确认 HelmRelease 没有 drift。
## 验证
```bash
kubectl -n openebs get pods
kubectl get storageclass localpv-zfs-ceph
kubectl get pv | grep localpv-zfs-ceph
kubectl get pvc -A
zfs list -r data/ceph
```
所有既有 PV 必须保持 `Bound`。验证新 provisioner 时只创建单独的临时 PVC,不要修改
或删除业务 PVC,也不要用 `helm uninstall` 验证接管。
+33
View File
@@ -0,0 +1,33 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: openebs
namespace: openebs
spec:
chart:
spec:
chart: openebs
interval: 1h
sourceRef:
kind: HelmRepository
name: openebs
version: 4.4.0
driftDetection:
mode: enabled
install:
crds: CreateReplace
strategy:
name: RetryOnFailure
retryInterval: 5m
interval: 30m
releaseName: openebs
targetNamespace: openebs
timeout: 15m
upgrade:
crds: CreateReplace
strategy:
name: RetryOnFailure
retryInterval: 5m
valuesFrom:
- kind: ConfigMap
name: openebs-values
+8
View File
@@ -0,0 +1,8 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: openebs
namespace: openebs
spec:
interval: 1h
url: https://openebs.github.io/openebs
+15
View File
@@ -0,0 +1,15 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
generatorOptions:
disableNameSuffixHash: true
labels:
reconcile.fluxcd.io/watch: Enabled
configMapGenerator:
- name: openebs-values
namespace: openebs
files:
- values.yaml=values.yaml
resources:
- helmrepository.yaml
- helmrelease.yaml
- storageclasses.yaml
+94
View File
@@ -0,0 +1,94 @@
# SPIFFE/SPIRE workload identity
SPIRE 是 homelab 的机器与 workload identity 根。人类身份继续由 Samba AD 与
Authelia 提供;SPIRE 不替代人类 OIDC,也不承担目标服务的资源授权。
部署状态、workload 接入、JWT-SVID → OpenBao exchange、安全规则和故障恢复详见
[RUNBOOK.md](RUNBOOK.md)。本文只保留部署声明与关键恢复边界。
## 部署范围
Flux 安装 SPIFFE hardened charts:
- `spire-crds` `0.6.1`;
- `spire` `0.30.2`(SPIRE `1.15.3`);
- SPIRE Server、Agent、Controller Manager、SPIFFE CSI Driver;
- OIDC Discovery Provider。
未启用 Tornjak、SPIRE Identity Exchange、SPIKE、federation、Delegated Identity
API 或 Broker API。Trust domain 是 `ddupan.top`,Kubernetes cluster name 是
`homelab`。
## PostgreSQL bootstrap
SPIRE registration datastore 使用共享 CloudNativePG:
```text
host: shared-postgresql-rw.shared-db.svc.cluster.local:5432
database: spire
role: spire
```
数据库与 role 当前是手工创建的临时 bootstrap。密码只存在于
`spire-server/spire-postgresql` Secret 的 `password` key 中,不提交到 Git。
在 PostgreSQL tenant operator/DBaaS 接管前,不得删除该 Secret 或重置数据库
role 密码。
后续声明式管理必须保持这一 Secret 接口,或者在同一个变更中更新
`spire-server.dataStore.sql.externalSecret`,避免数据库凭据出现两个写入方。
PostgreSQL保存 registration state;SPIRE Server 的 disk KeyManager 仍使用一个
`1Gi`、`localpv-zfs-ceph` PVC 保存 trust-domain signing keys。数据库备份不能替代
该 PVC/密钥的备份。
## 身份签发策略
默认的全 Pod fallback `ClusterSPIFFEID` 已关闭。新增 workload 必须显式创建
`ClusterSPIFFEID`,并以 namespace、ServiceAccount、Pod label 等 selector 收窄。
不得仅因 Pod 能挂载 CSI socket 就给它签发身份。
稳定的 JWT issuer 预留为:
```text
https://spire-oidc.ad.ddupan.top
```
OIDC Discovery Provider 在 Pod 内部使用明文 HTTP,由现有 Envoy Gateway 的
`https` listener 使用 `*.ad.ddupan.top` wildcard certificate 终止 TLS。对应的
`HTTPRoute` 将 `spire-oidc.ad.ddupan.top` 转发到 ClusterIP Service;AD DNS 记录
声明在 `../../infrastructure/dns/records.yml`,由 Samba DNS Ansible 流程应用。
接入 OpenBao 前必须从集群内和 LAN 分别验证 discovery document 的 `issuer` 与
上述 URL 完全一致。该 endpoint 只发布公开的 discovery metadata 和 JWKS,不能
在其 HTTPRoute 上添加 Authelia forward-auth。
## 首次部署与验证
合并后观察:
```bash
sudo k3s kubectl -n flux-system get kustomization spire
sudo k3s kubectl -n spire-mgmt get helmrelease
sudo k3s kubectl -n spire-server get pods,pvc
sudo k3s kubectl -n spire-system get daemonset,pods
```
必须先确认 `spire-crds` Ready,随后 `spire` Ready。SPIRE Server 应连接 PostgreSQL,
Agent 应通过 PSAT attestation 注册,CSI Driver 应在节点 Ready。
2026-09-14 已使用临时测试 Pod 与 `ClusterSPIFFEID` 完成
`aud=openbao` JWT-SVID → OpenBao 登录、token 自省和主动吊销的端到端验收;临时
Kubernetes 资源与 registration entries 已清理。
OpenBao 中对应的 Terraform 资源位于
`../../infrastructure/openbao/terraform/auth-spire.tf`。PoC role 只接受精确 subject
`spiffe://ddupan.top/ns/spire-poc/sa/spire-jwt-poc`,token 不包含 default policy,
且 `spire-poc` policy 不允许读取任何业务 secret。
## 恢复边界
- 恢复顺序:共享 PostgreSQL、SPIRE Server signing-key PVC、SPIRE Server、Agent;
- issuer URL 与 trust domain 初始化后不得随意修改;
- 丢失 signing keys 会使既有 SVID 和下游 JWKS 信任失效;
- PostgreSQL或 SPIRE 不可用时,不得用新的空数据库覆盖现有状态;
- 当前 Flux root 与本 Kustomization 均保持 `prune: false`,删除资源需单独审计。
+436
View File
@@ -0,0 +1,436 @@
# SPIRE 与 OpenBao workload identity runbook
本文记录 homelab 中 workload 如何取得 SPIFFE 身份、如何把 JWT-SVID 交换成
OpenBao 短期 token,以及相关的部署、接入、验证和恢复操作。这里的命令默认在
`laptop` 上执行。
## 1. 当前架构
```text
Kubernetes Pod
│ Pod ServiceAccount + Pod UID
▼
SPIRE Agent(每节点 DaemonSet,k8s workload attestor)
│ Unix socket: /spiffe-workload-api/spire-agent.sock
│ Agent 自身通过 k8s_psat 向 Server 证明节点身份
▼
SPIRE Server(trust domain: ddupan.top)
├─ registration state → shared PostgreSQL
├─ signing keys → localpv-zfs-ceph PVC
└─ JWT public keys → OIDC Discovery Provider
│
▼
https://spire-oidc.ad.ddupan.top
│ discovery + JWKS
▼
OpenBao auth/jwt-spire/login
│ exact sub + aud + role
▼
短期、最小权限 Bao token
```
各层职责必须保持分离:
- Kubernetes ServiceAccount 是 Pod 的初始身份证明,不是跨平台 IAM token;
- SPIRE 负责 workload 身份、证明和 SVID 签发,不保存业务 secret;
- OpenBao 验证 JWT-SVID,并把身份映射为本地 policy;
- 目标服务最终仍负责自己的资源授权,SPIFFE ID 本身不等于权限;
- 人类身份继续使用 Samba AD + Authelia OIDC,不经过 `jwt-spire`。
## 2. 线上对象与稳定标识
| 项目 | 当前值 |
|---|---|
| SPIRE chart | `0.30.2` |
| SPIRE | `1.15.3` |
| SPIRE CRDs chart | `0.6.1` |
| trust domain | `ddupan.top` |
| Kubernetes cluster name | `homelab` |
| Controller Manager class | `spire-mgmt-spire` |
| JWT issuer | `https://spire-oidc.ad.ddupan.top` |
| OpenBao auth mount | `jwt-spire` |
| OpenBao login endpoint | `auth/jwt-spire/login` |
| SPIRE Server namespace | `spire-server` |
| Agent/CSI namespace | `spire-system` |
| Helm management namespace | `spire-mgmt` |
`trustDomain`、`clusterName`、issuer URL 与 Controller Manager class 都进入身份或
下游信任配置。修改它们不是普通 rename,必须按 trust-domain migration 处理。
## 3. 身份与授权模型
Kubernetes workload 的默认 SPIFFE ID 约定为:
```text
spiffe://ddupan.top/ns/<namespace>/sa/<service-account>
```
身份必须同时在两侧声明:
1. SPIRE `ClusterSPIFFEID` 决定哪些 Pod 可以取得该身份;
2. OpenBao JWT role 决定该 `sub`、`aud` 能换取哪些 policy。
这两个声明是有意的双重门:只有 SPIRE entry 而没有 Bao role 时,workload 能取得
SVID,但不能登录 Bao;只有 Bao role 而没有 SPIRE entry 时,没有 workload 能铸造
满足条件的 JWT。
禁止使用以下宽泛规则:
- 给所有 Pod 启用 fallback `ClusterSPIFFEID`;
- OpenBao role 接受整个 `spiffe://ddupan.top/*`;
- 仅按 namespace 匹配高权限身份,却不限制 ServiceAccount 和 Pod labels;
- 多个安全边界不同的 workload 共用同一个 ServiceAccount;
- 给 workload token 附带 `default` 或 `admin` policy。
## 4. 新 workload 接入流程
以下示例为 namespace `example` 中的 ServiceAccount `example-worker`。
### 4.1 创建专用 ServiceAccount
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: example-worker
namespace: example
```
不要使用 namespace 的 `default` ServiceAccount。
### 4.2 声明 ClusterSPIFFEID
```yaml
apiVersion: spire.spiffe.io/v1alpha1
kind: ClusterSPIFFEID
metadata:
name: example-worker
labels:
spire.spiffe.io/class-name: spire-mgmt-spire
spec:
className: spire-mgmt-spire
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: example
podSelector:
matchLabels:
app.kubernetes.io/name: example-worker
spiffeIDTemplate: "spiffe://{{ .TrustDomain }}/ns/{{ .PodMeta.Namespace }}/sa/{{ .PodSpec.ServiceAccountName }}"
```
Controller Manager 最终会为具体 Pod UID 创建 registration entry。检查状态:
```bash
sudo k3s kubectl get clusterspiffeid example-worker -o yaml
sudo k3s kubectl -n spire-server exec statefulset/spire-server -c spire-server -- \
/opt/spire/bin/spire-server entry show \
-spiffeID spiffe://ddupan.top/ns/example/sa/example-worker
```
`status.stats.entryFailures` 必须是 `0`。Pod 重建后 UID 会变化,短暂看到旧 entry
属于正常收敛过程。
### 4.3 挂载 Workload API
```yaml
spec:
serviceAccountName: example-worker
containers:
- name: worker
volumeMounts:
- name: spiffe-workload-api
mountPath: /spiffe-workload-api
readOnly: true
env:
- name: SPIFFE_ENDPOINT_SOCKET
value: unix:///spiffe-workload-api/spire-agent.sock
volumes:
- name: spiffe-workload-api
csi:
driver: csi.spiffe.io
readOnly: true
```
挂载 socket 不会自动获得身份。Agent 会对调用进程执行 workload attestation,只有
selector 命中 registration entry 才签发 SVID。
应用应优先使用 SPIFFE SDK,通过 Workload API 按需取得并自动轮换 SVID。不要把
JWT-SVID 写入 Kubernetes Secret、镜像、持久卷、CI artifact 或日志。
### 4.4 声明 OpenBao policy
在 `infrastructure/openbao/terraform/policies/` 中为 workload 建独立 policy。例如:
```hcl
path "kv/data/apps/example/*" {
capabilities = ["read"]
}
path "auth/token/lookup-self" {
capabilities = ["read"]
}
path "auth/token/revoke-self" {
capabilities = ["update"]
}
```
不要直接复用 `admin`。动态数据库凭据、SSH 签名和 KV 应分别授权到精确 path。
### 4.5 声明 OpenBao JWT role
在 `infrastructure/openbao/terraform/auth-spire.tf` 增加 role:
```hcl
resource "vault_jwt_auth_backend_role" "example_worker" {
backend = vault_jwt_auth_backend.spire.path
role_name = "example-worker"
role_type = "jwt"
user_claim = "sub"
bound_audiences = ["openbao"]
bound_claims = {
sub = "spiffe://ddupan.top/ns/example/sa/example-worker"
}
token_policies = [vault_policy.example_worker.name]
token_no_default_policy = true
token_ttl = 300
token_max_ttl = 900
}
```
同一个 JWT 可以请求多个 audience,但 Bao role 只接受 `openbao`。未来接入其他服务时
应给对应服务使用独立 audience,不能把 `openbao` 当作通用 audience。
Terraform apply 必须遵循 `infrastructure/openbao/README.md` 的 remote-state 和认证
流程。任何包含 destroy/replace 的 plan 都应停止审查;JWT role/policy 的正常新增应为
纯 `add`。
## 5. JWT-SVID 交换流程
逻辑请求如下:
```http
POST /v1/auth/jwt-spire/login
Content-Type: application/json
{
"role": "example-worker",
"jwt": "<aud=openbao 的 JWT-SVID>"
}
```
成功响应中的 `auth.client_token` 是短期 Bao token。它只应存在于进程内存或
job-scoped `tmpfs`;通常无需主动续期,过期前重新用 Workload API 获取 JWT-SVID 并
登录即可。
如果使用 SPIRE CLI 调试,`1.15.3` 的 JSON 输出顶层是数组,JWT 位于:
```text
.[0].svids[0].svid
```
调试脚本必须把 JSON 捕获到变量中,禁止直接输出:
```bash
set -euo pipefail
JWT_RESPONSE="$(spire-agent api fetch jwt \
-audience openbao \
-socketPath /spiffe-workload-api/spire-agent.sock \
-output json)"
JWT_SVID="$(printf '%s' "$JWT_RESPONSE" | jq -er '.[0].svids[0].svid')"
LOGIN_PAYLOAD="$(jq -nc \
--arg role example-worker \
--arg jwt "$JWT_SVID" \
'{role:$role,jwt:$jwt}')"
LOGIN_RESPONSE="$(curl --fail-with-body --silent --show-error \
-H 'Content-Type: application/json' \
--data "$LOGIN_PAYLOAD" \
https://bao.ad.ddupan.top:8200/v1/auth/jwt-spire/login)"
export BAO_ADDR=https://bao.ad.ddupan.top:8200
export BAO_TOKEN="$(printf '%s' "$LOGIN_RESPONSE" | jq -er '.auth.client_token')"
```
不要在 shell 中启用 `set -x`,不要 `echo "$JWT_SVID"` 或输出完整 login response。
cleanup 阶段可以尽力主动吊销;短 TTL 仍是主要安全边界:
```bash
bao token revoke -self
unset BAO_TOKEN JWT_SVID JWT_RESPONSE LOGIN_RESPONSE LOGIN_PAYLOAD
```
## 6. CI 与 AI Agent 使用方式
CI job/Agent 不应接收长期 `BAO_TOKEN`。标准启动顺序是:
1. 调度到带 SPIRE Agent 与 CSI Driver 的节点;
2. 以专用 ServiceAccount 启动,挂载 Workload API socket;
3. 获取目标 audience 的 JWT-SVID;
4. 用对应 Bao role 换取短期 token;
5. 在同一进程树中以环境变量调用 `tofu`、Ansible 或其他工具;
6. cleanup 尝试 `revoke-self`,随后销毁 job/VM/容器。
通用 credential-exec 包装器未来应负责步骤 3–6。它必须满足:
- 不把 JWT-SVID 或 Bao token 写到 stdout/stderr;
- 不把凭据传入命令行参数,避免出现在进程列表;
- 子进程退出后清除环境和临时文件;
- 不尝试把短期 token 上传到 Actions Secret 或 artifact;
- role、audience 和目标命令由受审查的 pipeline 配置决定。
Kubernetes 以外的执行环境不能伪造 ServiceAccount。未来应分别使用 host SPIRE
Agent、TPM/DevID、cloud instance identity、GitHub OIDC 等初始证明接入同一信任模型。
## 7. 日常检查
### Flux 与 Helm
```bash
sudo k3s kubectl -n flux-system get kustomization spire
sudo k3s kubectl -n spire-mgmt get helmrepository,helmrelease
```
### Server、Agent 与 CSI
```bash
sudo k3s kubectl -n spire-server get pods,pvc
sudo k3s kubectl -n spire-system get daemonset,pods
sudo k3s kubectl -n spire-server exec statefulset/spire-server -c spire-server -- \
/opt/spire/bin/spire-server agent list
```
Agent 应显示 `Attestation type: k8s_psat` 与 `Can re-attest: true`。
### OIDC discovery 与 JWKS
```bash
dig @192.168.10.5 spire-oidc.ad.ddupan.top A +short
dig @192.168.10.127 spire-oidc.ad.ddupan.top A +short
curl --fail --silent \
https://spire-oidc.ad.ddupan.top/.well-known/openid-configuration | jq
curl --fail --silent https://spire-oidc.ad.ddupan.top/keys | jq '.keys | length'
```
discovery 的 `issuer` 必须严格等于
`https://spire-oidc.ad.ddupan.top`。路径、scheme、hostname 或尾部 `/` 的差异都会
导致 JWT 验证失败。JWKS 是公开验证材料,不是 secret。
### OpenBao
管理员只检查非敏感配置:
```bash
export BAO_ADDR=https://bao.ad.ddupan.top:8200
bao auth list
bao read auth/jwt-spire/role/<role-name>
bao policy read <policy-name>
```
## 8. 故障排查
### `no identity issued`
依次检查:
```bash
sudo k3s kubectl get clusterspiffeid <name> -o yaml
sudo k3s kubectl -n <namespace> get pod <pod> \
-o custom-columns=NAME:.metadata.name,UID:.metadata.uid,SA:.spec.serviceAccountName,LABELS:.metadata.labels
sudo k3s kubectl -n spire-server exec statefulset/spire-server -c spire-server -- \
/opt/spire/bin/spire-server entry show -spiffeID <expected-spiffe-id>
```
确认 namespace、ServiceAccount、Pod labels 与 entry 中的 `k8s:pod-uid`。刚启动的
一次性 Job 可能在 Controller Manager 建 entry 前请求身份并失败;生产客户端应重试
Workload API,而不是假设 Pod 一启动身份就已可用。
### `auth/jwt-spire/login` 返回 400
常见原因:
- JWT audience 不是 role 的 `bound_audiences`;
- JWT `sub` 与 role 的 `bound_claims.sub` 不完全一致;
- issuer 与 `bound_issuer` 不一致;
- Bao 无法解析或验证 `spire-oidc.ad.ddupan.top`;
- issuer/JWKS route 被 Authelia forward-auth 拦截;
- SVID 已过期,或者节点与 Bao 时钟偏差过大;
- 脚本错误解析 CLI JSON,向 Bao 发送了空 JWT。
排查时只解码 JWT header/claims,禁止记录原始 token。公开 endpoint 可单独验证:
```bash
curl --fail https://spire-oidc.ad.ddupan.top/.well-known/openid-configuration
curl --fail https://spire-oidc.ad.ddupan.top/keys
```
### CSI mount 失败
如果事件包含 `driver name csi.spiffe.io not found`:
```bash
sudo k3s kubectl get csidriver csi.spiffe.io
sudo k3s kubectl -n spire-system get daemonset spire-spiffe-csi-driver
sudo k3s kubectl -n spire-system logs daemonset/spire-spiffe-csi-driver \
-c spiffe-csi-driver --tail=100
```
首次安装时 OIDC Provider/业务 Pod 可能早于 CSI registration,短暂 mount retry 正常;
持续失败才需要处理。
### Server 无法连接 PostgreSQL
```bash
sudo k3s kubectl -n shared-db get cluster shared-postgresql
sudo k3s kubectl -n spire-server get secret spire-postgresql \
-o go-template='{{range $k, $_ := .data}}{{$k}}{{"\n"}}{{end}}'
sudo k3s kubectl -n spire-server logs statefulset/spire-server \
-c spire-server --tail=100
```
Secret 必须有 `password` key。禁止为排障直接输出或提交其值。
## 9. 轮换、备份与恢复
SPIRE 会自动轮换 X.509 CA 与 JWT signing key;OIDC Provider 从 Workload API 获取
当前 JWKS,下游按 `kid` 验证。正常轮换不应要求更新 Bao role。
必须备份两类不同状态:
- PostgreSQL:registration entries、agent state 和 SPIRE metadata;
- `spire-data-spire-server-0` PVC:disk KeyManager 的 trust-domain signing keys。
仅恢复 PostgreSQL 而丢失 PVC,不等于恢复 SPIRE。签名密钥丢失会使既有 SVID/JWKS
信任链失效。恢复顺序:
1. 恢复共享 PostgreSQL;
2. 恢复 signing-key PVC;
3. 启动 SPIRE Server;
4. 确认 bundle/JWKS 后启动或恢复 Agent;
5. 最后恢复依赖 SPIRE 登录 Bao 的 workload。
不要以空数据库或空 PVC“修复”启动失败。若确实需要重建 trust domain,应把它作为
全体下游重新建立信任的灾难恢复事件处理。
## 10. 当前 PoC 结论与后续工作
2026-09-14 已完成并清理一次临时 PoC:
- workload 取得 `aud=openbao` JWT-SVID;
- 精确 subject 成功登录 `jwt-spire/spire-poc`;
- 返回 token 仅含 `spire-poc` policy,TTL 为 300 秒,无 default policy;
- `lookup-self` 成功,随后 `revoke-self` 并验证 token 已失效;
- 临时 Namespace、Pod/Job、ServiceAccount、`ClusterSPIFFEID` 与 registration entries
均已删除;
- Terraform 完整 plan 最终为 `No changes`。
下一步不是重复 PoC,而是为真实 CI/AI Agent 定义:
- 独立 ServiceAccount 和稳定 SPIFFE ID;
- 按能力拆分的 OpenBao policy(例如 SSH CA、S3 state、数据库动态凭据);
- 通用 credential-exec 包装器;
- token 获取失败、过期与 cleanup 的客户端重试语义;
- Kubernetes 外 host/VM/microVM 的 SPIRE Agent attestation 方案。
+30
View File
@@ -0,0 +1,30 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: spire-crds
namespace: spire-mgmt
spec:
chart:
spec:
chart: spire-crds
interval: 1h
sourceRef:
kind: HelmRepository
name: spiffe-hardened
version: 0.6.1
driftDetection:
mode: enabled
install:
crds: CreateReplace
strategy:
name: RetryOnFailure
retryInterval: 5m
interval: 30m
releaseName: spire-crds
targetNamespace: spire-mgmt
timeout: 10m
upgrade:
crds: CreateReplace
strategy:
name: RetryOnFailure
retryInterval: 5m
+34
View File
@@ -0,0 +1,34 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: spire
namespace: spire-mgmt
spec:
chart:
spec:
chart: spire
interval: 1h
sourceRef:
kind: HelmRepository
name: spiffe-hardened
version: 0.30.2
dependsOn:
- name: spire-crds
namespace: spire-mgmt
driftDetection:
mode: enabled
install:
strategy:
name: RetryOnFailure
retryInterval: 5m
interval: 30m
releaseName: spire
targetNamespace: spire-mgmt
timeout: 15m
upgrade:
strategy:
name: RetryOnFailure
retryInterval: 5m
valuesFrom:
- kind: ConfigMap
name: spire-values
+8
View File
@@ -0,0 +1,8 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: spiffe-hardened
namespace: spire-mgmt
spec:
interval: 1h
url: https://spiffe.github.io/helm-charts-hardened/
+16
View File
@@ -0,0 +1,16 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: spire-oidc-discovery-provider
namespace: spire-server
spec:
parentRefs:
- name: eg
namespace: envoy-gateway-system
sectionName: https
hostnames:
- spire-oidc.ad.ddupan.top
rules:
- backendRefs:
- name: spire-spiffe-oidc-discovery-provider
port: 80
+17
View File
@@ -0,0 +1,17 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
generatorOptions:
disableNameSuffixHash: true
labels:
reconcile.fluxcd.io/watch: Enabled
configMapGenerator:
- name: spire-values
namespace: spire-mgmt
files:
- values.yaml=values.yaml
resources:
- namespaces.yaml
- helmrepository.yaml
- helmrelease-crds.yaml
- helmrelease.yaml
- httproute.yaml
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Namespace
metadata:
name: spire-mgmt
---
apiVersion: v1
kind: Namespace
metadata:
name: spire-server
---
apiVersion: v1
kind: Namespace
metadata:
name: spire-system
+103
View File
@@ -0,0 +1,103 @@
global:
k8s:
clusterDomain: cluster.local
spire:
clusterName: homelab
trustDomain: ddupan.top
# This is a stable external identity even before its Gateway route is added.
# Changing the issuer after consumers trust it would invalidate their config.
jwtIssuer: https://spire-oidc.ad.ddupan.top
caSubject:
country: CN
organization: ddupan homelab
commonName: ddupan.top
namespaces:
create: false
system:
name: spire-system
server:
name: spire-server
recommendations:
enabled: true
namespaceLayout: true
namespacePSS: true
priorityClassName: true
strictMode: true
securityContexts: true
prometheus: false
spire-server:
kind: statefulset
replicaCount: 1
auditLogEnabled: true
persistence:
# PostgreSQL stores registrations, but the disk KeyManager still needs durable
# storage for the trust-domain signing keys.
type: pvc
size: 1Gi
accessMode: ReadWriteOnce
storageClass: localpv-zfs-ceph
dataStore:
sql:
databaseType: postgres
databaseName: spire
host: shared-postgresql-rw.shared-db.svc.cluster.local
port: 5432
username: spire
options:
- sslmode: require
externalSecret:
enabled: true
name: spire-postgresql
key: password
maxOpenConns: 20
maxIdleConns: 2
connMaxLifetime: 30m
controllerManager:
enabled: true
# Identities are opt-in. Do not issue a fallback SVID to every pod in the
# cluster merely because it can mount the Workload API socket.
identities:
clusterSPIFFEIDs:
default:
enabled: false
test-keys:
enabled: false
spire-agent:
nodeAttestor:
k8sPSAT:
enabled: true
workloadAttestors:
k8s:
enabled: true
unix:
enabled: false
spiffe-csi-driver:
enabled: true
spiffe-oidc-discovery-provider:
enabled: true
# TLS will terminate at the existing Envoy Gateway when the stable issuer is
# exposed. The service remains ClusterIP-only during the first deployment.
tls:
spire:
enabled: false
externalSecret:
enabled: false
certManager:
enabled: false
upstream:
enabled: false
tornjak-frontend:
enabled: false
spire-identity-exchange:
enabled: false
spike-keeper:
enabled: false
spike-nexus:
enabled: false
spike-pilot:
enabled: false