fix(cd): verify API executor boundary in production
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m37s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled

This commit is contained in:
ogt
2026-07-11 00:12:04 +08:00
parent 24f8643825
commit 3b8bdcea80
3 changed files with 94 additions and 7 deletions

View File

@@ -2160,14 +2160,13 @@ jobs:
echo "✅ Service Registry ConfigMap 已更新"
# ─── Step 2: 更新 kustomization.yaml image tag ───
# host runner 不保證有 root 權限kustomize 安裝在使用者目錄。
# kustomize is an immutable dependency of the internal runner image.
# GitHub is frozen, so CD must never download tools from github.com.
export PATH="${HOME}/.local/bin:${PATH}"
if ! command -v kustomize &>/dev/null; then
mkdir -p "${HOME}/.local/bin"
curl -sL https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv5.3.0/kustomize_v5.3.0_linux_amd64.tar.gz \
| tar xz -C "${HOME}/.local/bin"
chmod +x "${HOME}/.local/bin/kustomize"
fi
command -v kustomize >/dev/null 2>&1 || {
echo "❌ internal runner image is missing required kustomize"
exit 1
}
cd k8s/awoooi-prod
# kustomize edit set image 更新 tag
@@ -2395,6 +2394,69 @@ jobs:
$KUBECTL rollout status deployment/awoooi-worker -n awoooi-prod --timeout=120s
echo "✅ 部署完成"
# AIA-P0-011: production must prove that the public API cannot mutate
# workloads or read host executor credentials. Source YAML alone is
# insufficient because stale SSA-owned list fields can survive sync.
API_SA=$($KUBECTL get deployment/awoooi-api -n awoooi-prod \
-o jsonpath='{.spec.template.spec.serviceAccountName}')
WORKER_SA=$($KUBECTL get deployment/awoooi-worker -n awoooi-prod \
-o jsonpath='{.spec.template.spec.serviceAccountName}')
API_MOUNTS=$($KUBECTL get deployment/awoooi-api -n awoooi-prod \
-o jsonpath='{range .spec.template.spec.containers[?(@.name=="api")].volumeMounts[*]}{.name}{"\n"}{end}')
API_VOLUMES=$($KUBECTL get deployment/awoooi-api -n awoooi-prod \
-o jsonpath='{range .spec.template.spec.volumes[*]}{.name}{"\n"}{end}')
API_FS_GROUP=$($KUBECTL get deployment/awoooi-api -n awoooi-prod \
-o jsonpath='{.spec.template.spec.securityContext.fsGroup}')
API_SSH_MCP=$($KUBECTL get deployment/awoooi-api -n awoooi-prod \
-o jsonpath='{.spec.template.spec.containers[?(@.name=="api")].env[?(@.name=="SSH_MCP_ENABLED")].value}')
API_CHECK_WORKER=$($KUBECTL get deployment/awoooi-api -n awoooi-prod \
-o jsonpath='{.spec.template.spec.containers[?(@.name=="api")].env[?(@.name=="ENABLE_AWOOOP_ANSIBLE_CHECK_MODE_WORKER")].value}')
API_BACKFILL_WORKER=$($KUBECTL get deployment/awoooi-api -n awoooi-prod \
-o jsonpath='{.spec.template.spec.containers[?(@.name=="api")].env[?(@.name=="ENABLE_AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_WORKER")].value}')
API_CAN_GET=$($KUBECTL auth can-i \
--as=system:serviceaccount:awoooi-prod:awoooi-api \
get pods --all-namespaces)
API_CAN_PATCH=$($KUBECTL auth can-i \
--as=system:serviceaccount:awoooi-prod:awoooi-api \
patch deployments.apps --all-namespaces)
API_CAN_DELETE=$($KUBECTL auth can-i \
--as=system:serviceaccount:awoooi-prod:awoooi-api \
delete pods --all-namespaces)
test "$API_SA" = "awoooi-api" || {
echo "❌ API ServiceAccount drift: $API_SA"; exit 1;
}
test "$WORKER_SA" = "awoooi-executor" || {
echo "❌ worker ServiceAccount drift: $WORKER_SA"; exit 1;
}
if printf '%s\n%s\n' "$API_MOUNTS" "$API_VOLUMES" \
| grep -Eq '^(repair-ssh-key|repair-known-hosts|ssh-mcp-key)$'; then
echo "❌ API still mounts executor SSH credentials"
exit 1
fi
test -z "$API_FS_GROUP" || {
echo "❌ API still carries executor fsGroup: $API_FS_GROUP"; exit 1;
}
test "$API_SSH_MCP" = "false" || {
echo "❌ API SSH MCP is not disabled"; exit 1;
}
test "$API_CHECK_WORKER" = "false" || {
echo "❌ API check-mode executor loop is not disabled"; exit 1;
}
test "$API_BACKFILL_WORKER" = "false" || {
echo "❌ API candidate backfill executor loop is not disabled"; exit 1;
}
test "$API_CAN_GET" = "yes" || {
echo "❌ API read capability missing"; exit 1;
}
test "$API_CAN_PATCH" = "no" || {
echo "❌ API can still patch workloads"; exit 1;
}
test "$API_CAN_DELETE" = "no" || {
echo "❌ API can still delete pods"; exit 1;
}
echo "✅ AIA-P0-011 API/executor production boundary verified"
# Health Check
HEALTH_PASS=0
for i in 1 2 3; do

View File

@@ -96,6 +96,27 @@ def test_api_reader_rbac_has_no_workload_write_verbs() -> None:
)
def test_cd_prunes_and_verifies_api_executor_boundary_in_production() -> None:
repo_root = Path(__file__).resolve().parents[3]
deployment = next(
document
for document in yaml.safe_load_all(
(repo_root / "k8s/awoooi-prod/06-deployment-api.yaml").read_text()
)
if document and document.get("kind") == "Deployment"
)
workflow = (repo_root / ".gitea/workflows/cd.yaml").read_text()
assert deployment["metadata"]["annotations"][
"argocd.argoproj.io/sync-options"
] == "Replace=true"
assert "AIA-P0-011 API/executor production boundary verified" in workflow
assert "patch deployments.apps --all-namespaces" in workflow
assert "delete pods --all-namespaces" in workflow
assert "repair-ssh-key|repair-known-hosts|ssh-mcp-key" in workflow
assert "github.com/kubernetes-sigs/kustomize" not in workflow
def test_api_log_does_not_claim_skipped_executor_was_scheduled() -> None:
repo_root = Path(__file__).resolve().parents[3]
main_source = (repo_root / "apps/api/src/main.py").read_text()

View File

@@ -9,6 +9,10 @@ kind: Deployment
metadata:
name: awoooi-api
namespace: awoooi-prod
annotations:
# One controlled replace prunes legacy SSH mounts that were not owned by
# Argo CD's server-side apply field manager. Remove after prod verification.
argocd.argoproj.io/sync-options: Replace=true
labels:
app: awoooi-api
system: awoooi