Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31d2a63223
|
@@ -1,79 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
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
|
||||
+77
-14
@@ -1,26 +1,20 @@
|
||||
---
|
||||
# Stage 1 of the infra pipeline: static checks only. No cluster access, no
|
||||
# credentials or mutation. It runs only when YAML-related paths change.
|
||||
# credentials, no mutation — so this is safe to run on every push from day one.
|
||||
#
|
||||
# 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
|
||||
# there can never block this one.
|
||||
name: yaml
|
||||
name: lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- '**/*.yaml'
|
||||
- '**/*.yml'
|
||||
- '.yamllint.yml'
|
||||
- '.gitea/workflows/lint.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/*.yaml'
|
||||
- '**/*.yml'
|
||||
- '.yamllint.yml'
|
||||
- '.gitea/workflows/lint.yml'
|
||||
|
||||
env:
|
||||
# ansible-lint and ansible-core install as separate uv tools. Install Galaxy
|
||||
# collections into this shared path so both isolated environments can see them.
|
||||
ANSIBLE_COLLECTIONS_PATH: /root/.ansible/collections
|
||||
|
||||
jobs:
|
||||
yaml:
|
||||
@@ -29,7 +23,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Bootstrap uv
|
||||
# Pin the tool for reproducibility; PyPI also avoids another setup action.
|
||||
# setup-uv queries api.github.com, which is unreachable from the nested
|
||||
# job network. Official PyPI is reachable; pin the tool for reproducibility.
|
||||
run: |
|
||||
python3 -m pip install --user --break-system-packages \
|
||||
--index-url https://pypi.org/simple --quiet uv==0.11.7
|
||||
@@ -52,3 +47,71 @@ jobs:
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
files=$(git ls-files '*.yaml' '*.yml' | grep -vE '^apps/netboot/')
|
||||
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
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -4,8 +4,6 @@
|
||||
- `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.
|
||||
- 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
|
||||
和项目文档默认优先使用中文。代码标识符、命令、配置键、上游专有名称,以及
|
||||
使用英文能避免歧义的技术字段可保留英文。
|
||||
|
||||
+6
-6
@@ -34,7 +34,8 @@ What changed in this homelab, when, and why. Newest first.
|
||||
| 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 秒成功 |
|
||||
| Gitea 1.27 result | Flux 以 Helm revision 17 成功部署 chart `12.7.0` / 实际 Gitea `1.27.3-rootless`,migration 331–342 与 init containers 全部成功;内外 API、Git write 路径与 Flux source 均通过,Pod Ready 且零重启。Chart metadata 的 appVersion `1.27.0` 不代表实际固定镜像版本 |
|
||||
| Gitea upgrade policy | 连续两次跨 minor 升级证明当前 Flux + Recreate + rootless PVC + 外部 CNPG 路径稳定;后续常规 patch/minor 默认缩减为 release-note review、render、UpgradeSucceeded/Pod Ready/API、OIDC 与 Git 抽查。只有数据库/存储/rootless/PVC/部署策略/重大 chart 或 breaking migration 变化及实际失败时,才恢复停机备份和扩展验收 |
|
||||
|
||||
### Incident: Gitea 备份后的恢复命令被 stdin 校验阻塞
|
||||
|
||||
@@ -45,11 +46,10 @@ What changed in this homelab, when, and why. Newest first.
|
||||
旧版 Gitea 恢复后内外 API 和 Flux source 均正常;后续 runbook 不再把 stdin 管道与
|
||||
恢复命令放进同一个 shell transaction。
|
||||
|
||||
`Carried forward`: complete the two-stage zero-change `gitea` HelmRelease
|
||||
adoption, migrate its remaining manual OIDC Secret to OpenBao/ESO, then upgrade
|
||||
Gitea and add credential-free PR plan output before ordering the remaining Helm
|
||||
migrations by dependency and blast radius. Root Flux prune remains disabled
|
||||
until brownfield ownership is audited.
|
||||
`Carried forward`: migrate Gitea's remaining manual OIDC Secret to OpenBao/ESO,
|
||||
then add credential-free PR plan output and order the remaining Helm migrations
|
||||
by dependency and blast radius. Root Flux prune remains disabled until
|
||||
brownfield ownership is audited.
|
||||
|
||||
## 2026-09-09
|
||||
|
||||
|
||||
@@ -132,10 +132,9 @@ recovered, so `.vault_pass.gpg` is the authoritative recovery path.
|
||||
|
||||
## Working rules
|
||||
|
||||
- **Do not update `CHANGELOG.md`.** It is a frozen historical snapshot; requiring every PR
|
||||
to append to one shared text file caused needless conflicts and duplicated Git/PR history.
|
||||
Put durable service state and operational knowledge in the component README or runbook,
|
||||
agent-facing traps here, and let commits/PRs record individual changes.
|
||||
- **Record changes in `CHANGELOG.md`.** One dated section per day, newest first; incidents
|
||||
get their own subsection. Traps and procedures belong *here* in CLAUDE.md, not there —
|
||||
the changelog is for humans reading what changed.
|
||||
- **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.
|
||||
Several confident diagnoses in this repo's history were wrong until measured.
|
||||
@@ -150,11 +149,6 @@ 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.
|
||||
- **A `.tfplan` is a zip containing a full `tfstate`.** It walks straight past `*.tfstate`
|
||||
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'"`
|
||||
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`
|
||||
|
||||
@@ -11,9 +11,7 @@
|
||||
| `helm.sh` | Installs or upgrades the SeaweedFS release. |
|
||||
|
||||
**Install**
|
||||
1. 在 OpenBao `kv/k8s/seaweedfs-s3` 维护基础 S3 配置;zot 凭据单独以
|
||||
`kv/k8s/zot-s3` 为唯一来源。ESO 合成为 `seaweedfs-s3-config`,详见下文。
|
||||
不要把真实 AK/SK 放进 `values.yaml`。
|
||||
1. Set real S3 access and secret keys in `values.yaml`.
|
||||
2. Apply the manifests:
|
||||
```bash
|
||||
bash ~/services/apps/seaweedfs/helm.sh
|
||||
@@ -33,22 +31,4 @@
|
||||
|
||||
**Notes**
|
||||
- The chart manages master, volume, filer, S3, and admin components.
|
||||
- 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 的客户端入口。
|
||||
- The chart-managed S3 secret uses the current AK/SK pair for the admin user.
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
# 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)。
|
||||
@@ -1,22 +0,0 @@
|
||||
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
|
||||
@@ -1,30 +0,0 @@
|
||||
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
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: zot
|
||||
namespace: zot
|
||||
spec:
|
||||
interval: 1h
|
||||
url: https://zotregistry.dev/helm-charts
|
||||
@@ -1,18 +0,0 @@
|
||||
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
|
||||
@@ -1,7 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: zot
|
||||
labels:
|
||||
pod-security.kubernetes.io/enforce: restricted
|
||||
pod-security.kubernetes.io/enforce-version: v1.36
|
||||
@@ -1,22 +0,0 @@
|
||||
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
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: zot
|
||||
namespace: zot
|
||||
automountServiceAccountToken: false
|
||||
@@ -1,147 +0,0 @@
|
||||
# 官方 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
|
||||
@@ -43,13 +43,6 @@ sudo k3s kubectl -n flux-system get gitrepositories,kustomizations
|
||||
- `http-echo` 的专用测试 ConfigMap 已在 `prune: true` 生效后重新纳管,并由下一
|
||||
revision 自动删除;
|
||||
- `http-echo` 保持 `prune: true`,root 保持 `prune: false`;
|
||||
- `gitea-actions`、`gitea` 与 External Secrets 已由 Flux HelmRelease 接管,Gitea 已升级到 `1.27.3`;
|
||||
- cert-manager 已固定现有 `v1.21.0` 并完成分阶段 Flux HelmRelease 接管;
|
||||
- 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`。
|
||||
- `gitea-actions` 已由 Flux HelmRelease 接管且首次 reconcile 未触发 runner rollout;
|
||||
- 下一个接管对象是现有 `gitea` Helm release,先固定 chart `12.5.3` 并分两阶段完成
|
||||
零变化 adoption,再通过独立 PR 升级 Gitea。
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
@@ -1,18 +0,0 @@
|
||||
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
|
||||
@@ -3,13 +3,6 @@ kind: Kustomization
|
||||
resources:
|
||||
- flux-system
|
||||
- namespaces/gitops-canary.yaml
|
||||
- apps/cert-manager.yaml
|
||||
- apps/envoy-gateway.yaml
|
||||
- apps/external-secrets.yaml
|
||||
- apps/gitea.yaml
|
||||
- apps/gitea-actions.yaml
|
||||
- apps/http-echo.yaml
|
||||
- apps/openebs.yaml
|
||||
- apps/spire.yaml
|
||||
- apps/observability.yaml
|
||||
- apps/zot.yaml
|
||||
|
||||
+6
-8
@@ -1,7 +1,7 @@
|
||||
# CI/CD — what we are building
|
||||
|
||||
Status: **partly built.** Stage 1、Kubernetes runner 与 Flux 已上线;credentialed
|
||||
stages 尚未实现。
|
||||
Status: **partly built.** Stage 1 and the Kubernetes runner are live; Flux and
|
||||
credentialed stages are not yet installed.
|
||||
Started 2026-07-28.
|
||||
|
||||
## Goal
|
||||
@@ -24,20 +24,18 @@ 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 |
|
||||
| stage 1 | Gitea runner 上的 `yamllint`、`ansible-lint`、Terraform fmt/validate 已上线;三个 workflow 按路径触发,feature push 不再与 PR 事件重复运行 |
|
||||
| gitea | 1.27.3,Actions 已启用;一个 instance-scoped Kubernetes runner 以 capacity 4 运行 |
|
||||
| stage 1 | Live and green on the Gitea runner — `yamllint`, `ansible-lint`, `terraform fmt`/`validate` |
|
||||
| gitea | 1.25.5, Actions enabled, `DEFAULT_ACTIONS_URL=github`; one instance-scoped Kubernetes runner is deployed with capacity four |
|
||||
| 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`) |
|
||||
| k8s | Flux 已接管 Gitea、Gitea Actions 与 http-echo canary;其余 brownfield release 逐项迁移 |
|
||||
| k8s | ~13 Helm releases, all deployed by hand |
|
||||
| secrets | 4 config files are gitignored because they embed live secrets, so their contents are **not** version controlled |
|
||||
|
||||
## The four stages
|
||||
|
||||
**Stage 1 — static. Built.**
|
||||
`yamllint`, `ansible-lint`, `terraform fmt -check` / `validate -backend=false`.
|
||||
No cluster, no credentials, no mutation. YAML、Ansible 和 Terraform 各自按相关路径
|
||||
触发;feature branch 只由 `pull_request` 检查,合并后再由 `main` push 检查,避免
|
||||
同一 revision 因 branch push 和 PR 各跑一遍。它已经
|
||||
No cluster, no credentials, no mutation, so it is safe on every push. It already
|
||||
found a real defect: `infrastructure/proxmox/ansible/` had no `requirements.yml` at all, so a
|
||||
fresh checkout could not reproduce its collections.
|
||||
|
||||
|
||||
@@ -213,10 +213,11 @@ 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
|
||||
`http-echo`; automatic deployment, replica drift repair and scoped deletion
|
||||
were verified. Root prune remains disabled for brownfield safety.
|
||||
5. **In progress:** Gitea Actions and Gitea are managed by Flux. Adopt External
|
||||
Secrets Operator next with its existing chart `2.8.0` and repository values;
|
||||
register the suspended release first, then activate it in a separate PR after
|
||||
proving the fixed render matches Helm's stored manifest.
|
||||
5. **Next:** adopt the existing `gitea-actions` Helm release with the same chart
|
||||
`0.1.1` and repository values before attempting an upgrade. Its live
|
||||
StatefulSet and Git both use regular DinD, while Helm's stored release values
|
||||
still describe the failed rootless attempt. The first Flux reconciliation
|
||||
must repair this stored-state drift without changing the live Pod spec.
|
||||
6. Move Tunnel origins to Envoy and consolidate split DNS through Blocky.
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
matters is:
|
||||
|
||||
- `CHANGELOG.md` — frozen historical snapshot; Git commits and PRs now record changes
|
||||
- `CHANGELOG.md` — what changed, for humans
|
||||
- `CLAUDE.md` — traps and procedures, for agents
|
||||
- `docs/*.md` — design docs for work not yet built
|
||||
- `<service>/README.md` — how a service actually works
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# 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 同时接管时统一修复。
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
# 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
|
||||
@@ -36,7 +36,6 @@ configure any secrets engines/auth methods — that is a separate bootstrap play
|
||||
```
|
||||
terraform/ # OpenBao's API-level CONFIGURATION (see below)
|
||||
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
|
||||
policies/*.hcl # policy bodies, kept diffable
|
||||
```
|
||||
@@ -206,11 +205,6 @@ after this, use `BAO_ADDR=https://bao.ad.ddupan.top:8200` (no skip-verify), not
|
||||
|
||||
## 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
|
||||
# human: log in via Authelia (2FA)
|
||||
bao login -method=oidc # browser → auth.ddupan.top
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# 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,11 +14,6 @@ resource "vault_policy" "ai_agent_ssh" {
|
||||
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" {
|
||||
name = "snapshot"
|
||||
policy = file("${path.module}/policies/snapshot.hcl")
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# 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"]
|
||||
}
|
||||
@@ -54,7 +54,6 @@ 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/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/legacy.yml` | opt-in retro-client protocols (§ Retro clients) |
|
||||
| `roles/windows_vm/` | unattended-installs the Windows Server 2025 admin box (§2.0) |
|
||||
@@ -63,9 +62,6 @@ second run is a no-op there and only reconciles config drift.
|
||||
| `group_vars/all/vault.yml` | admin passwords (gitignored; encrypt with ansible-vault) |
|
||||
| `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
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
[defaults]
|
||||
inventory = inventory/hosts.yml
|
||||
roles_path = roles
|
||||
collections_paths = collections
|
||||
host_key_checking = False
|
||||
callback_result_format = yaml
|
||||
nocows = True
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
tests/output/
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
# ddupan.homelab
|
||||
|
||||
Repository-local Ansible collection for homelab infrastructure modules. It is consumed
|
||||
directly through the `collections_paths` setting and is not published.
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
---
|
||||
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
|
||||
-159
@@ -1,159 +0,0 @@
|
||||
#!/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()
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
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,8 +10,29 @@ samba_ad_dc_ip: "192.168.10.5"
|
||||
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
|
||||
|
||||
# Static records now live in ../../dns/records.yml and are reconciled as complete
|
||||
# RRsets by the local samba_dns_record module.
|
||||
# Extra A records for non-domain hosts published in the AD DNS zone.
|
||||
samba_ad_extra_a_records:
|
||||
- { 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".
|
||||
samba_ad_legacy_clients: false
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
hosts: samba_dc
|
||||
become: true
|
||||
gather_facts: true
|
||||
vars_files:
|
||||
- ../../dns/records.yml
|
||||
roles:
|
||||
- role: samba_ad_dc
|
||||
post_tasks:
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
- 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,11 +198,20 @@
|
||||
- "'already exists' not in (kms_srv.stderr | default('')) + (kms_srv.stdout | default(''))"
|
||||
no_log: true
|
||||
|
||||
# --- Directory objects and explicitly managed DNS records ----------------------
|
||||
# --- Extra A records for non-domain hosts (e.g. OpenBao) -----------------------
|
||||
- name: Service accounts and RBAC groups
|
||||
ansible.builtin.import_tasks: directory_objects.yml
|
||||
tags: [directory, accounts]
|
||||
|
||||
- name: Reconcile static AD DNS records
|
||||
ansible.builtin.import_tasks: dns_records.yml
|
||||
- name: Register extra A records in the AD DNS zone
|
||||
ansible.builtin.command:
|
||||
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]
|
||||
|
||||
@@ -51,18 +51,6 @@ the only challenge that can issue a wildcard.
|
||||
|
||||
## 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
|
||||
helm repo add jetstack https://charts.jetstack.io && helm repo update jetstack
|
||||
helm upgrade --install cert-manager jetstack/cert-manager --version v1.21.0 \
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
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
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: jetstack
|
||||
namespace: cert-manager
|
||||
spec:
|
||||
interval: 1h
|
||||
url: https://charts.jetstack.io
|
||||
@@ -1,19 +0,0 @@
|
||||
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
|
||||
@@ -79,21 +79,6 @@ unauthenticated requests to an app whose auth model is "trust the header".
|
||||
|
||||
## 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
|
||||
# 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
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
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
|
||||
@@ -1,9 +0,0 @@
|
||||
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
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- helmrepository.yaml
|
||||
- helmrelease.yaml
|
||||
- gateway.yaml
|
||||
@@ -1,20 +0,0 @@
|
||||
# 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 接管稳定后再单独审计、纳管这些对象。
|
||||
@@ -101,7 +101,6 @@ spec:
|
||||
# The chart normally GENERATES seaweedfs-s3-secret from s3.credentials. We point
|
||||
# filer.s3.existingConfigSecret at this one instead, so the chart stops rendering
|
||||
# credentials from values entirely.
|
||||
# zot 的 AK/SK 只保存在 k8s/zot-s3;在此组装服务端配置,不在基础配置中维护副本。
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
@@ -115,38 +114,6 @@ spec:
|
||||
target:
|
||||
name: seaweedfs-s3-config
|
||||
creationPolicy: Owner
|
||||
template:
|
||||
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:
|
||||
dataFrom:
|
||||
- extract:
|
||||
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
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
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
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: external-secrets
|
||||
namespace: external-secrets
|
||||
spec:
|
||||
interval: 1h
|
||||
url: https://charts.external-secrets.io
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
@@ -7,15 +7,6 @@ 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. 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
|
||||
on this Gitea instance. Repository permissions and protected-branch review are
|
||||
therefore the security boundary; do not enable Actions for untrusted repositories.
|
||||
|
||||
@@ -51,11 +51,3 @@ statefulset:
|
||||
repository: docker
|
||||
tag: 29.7.1-dind
|
||||
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
|
||||
|
||||
@@ -13,34 +13,6 @@ 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`,
|
||||
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
|
||||
hosts and `logs/vlogs-ingress.yaml` to push their logs.
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: grafana
|
||||
namespace: monitoring
|
||||
spec:
|
||||
interval: 1h
|
||||
url: https://grafana.github.io/helm-charts
|
||||
@@ -1,31 +0,0 @@
|
||||
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
|
||||
@@ -1,21 +0,0 @@
|
||||
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,12 +48,6 @@ persistence:
|
||||
storageClassName: localpv-zfs-ceph
|
||||
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) ---
|
||||
# The tailscale operator provisions grafana.<tailnet>.ts.net and a TLS cert.
|
||||
ingress:
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: vm
|
||||
namespace: monitoring
|
||||
spec:
|
||||
interval: 1h
|
||||
url: https://victoriametrics.github.io/helm-charts/
|
||||
@@ -1,11 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- helmrepository.yaml
|
||||
- grafana-helmrepository.yaml
|
||||
- operator
|
||||
- metrics
|
||||
- logs
|
||||
- traces
|
||||
- grafana
|
||||
@@ -1,31 +0,0 @@
|
||||
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
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
@@ -1,16 +0,0 @@
|
||||
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
|
||||
@@ -1,33 +0,0 @@
|
||||
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
|
||||
@@ -1,13 +0,0 @@
|
||||
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
|
||||
@@ -1,5 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- vtsingle.yaml
|
||||
- otel-collector.yaml
|
||||
@@ -1,53 +0,0 @@
|
||||
# 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` 验证接管。
|
||||
@@ -1,33 +0,0 @@
|
||||
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
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: openebs
|
||||
namespace: openebs
|
||||
spec:
|
||||
interval: 1h
|
||||
url: https://openebs.github.io/openebs
|
||||
@@ -1,15 +0,0 @@
|
||||
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
|
||||
@@ -1,94 +0,0 @@
|
||||
# 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`,删除资源需单独审计。
|
||||
@@ -1,436 +0,0 @@
|
||||
# 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 方案。
|
||||
@@ -1,30 +0,0 @@
|
||||
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
|
||||
@@ -1,34 +0,0 @@
|
||||
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
|
||||
@@ -1,8 +0,0 @@
|
||||
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/
|
||||
@@ -1,16 +0,0 @@
|
||||
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
|
||||
@@ -1,17 +0,0 @@
|
||||
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
|
||||
@@ -1,14 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: spire-mgmt
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: spire-server
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: spire-system
|
||||
@@ -1,103 +0,0 @@
|
||||
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
|
||||
Reference in New Issue
Block a user