diff --git a/scripts/publish-gate b/scripts/publish-gate new file mode 100755 index 00000000..8ce9947c --- /dev/null +++ b/scripts/publish-gate @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# publish-gate — gate a Canonical repo's PUBLIC flip (ADR-0002). +# A repo may go public ONLY on a CLEAN verdict; a DIRTY verdict means it stays +# private — canonical history is never rewritten for publication. +# +# Checks (full git history, not just the worktree): +# 1. gitleaks — secret patterns across all commits +# 2. trufflehog (docker) — verified-credential detection across all commits +# 3. PII heuristics — emails/phones/keys in tracked files + fixture inventory +# +# Usage: publish-gate +# Exit: 0 = CLEAN, 1 = DIRTY, 2 = scanner error. Report: /tmp/publish-gate-.txt +set -uo pipefail +CLONE=${1:?usage: publish-gate } +CLONE=$(cd "$CLONE" && pwd) +NAME=$(basename "$CLONE") +REPORT="/tmp/publish-gate-$NAME.txt" +DIRTY=0; ERR=0 + +say() { echo "$@" | tee -a "$REPORT"; } +: > "$REPORT" +say "== publish-gate: $NAME @ $(git -C "$CLONE" rev-parse --short HEAD) ($(date -u +%FT%TZ)) ==" + +# --- 1. gitleaks (full history) --- +say ""; say "-- gitleaks (full history) --" +if gitleaks git "$CLONE" --no-banner --redact --report-path /tmp/publish-gate-$NAME-gitleaks.json >>"$REPORT" 2>&1; then + say "gitleaks: CLEAN" +else + rc=$? + if [ "$rc" = 1 ]; then say "gitleaks: LEAKS FOUND (see $REPORT + json)"; DIRTY=1 + else say "gitleaks: scanner error rc=$rc"; ERR=1; fi +fi + +# --- 2. trufflehog (verified credentials, full history) --- +say ""; say "-- trufflehog (verified only, full history) --" +if docker run --rm -v "$CLONE":/repo:ro trufflesecurity/trufflehog:latest \ + git file:///repo --only-verified --fail --no-update >>"$REPORT" 2>&1; then + say "trufflehog: CLEAN (no verified credentials)" +else + rc=$? + if [ "$rc" = 183 ]; then say "trufflehog: VERIFIED CREDENTIALS FOUND"; DIRTY=1 + else say "trufflehog: scanner error rc=$rc"; ERR=1; fi +fi + +# --- 3. PII heuristics on tracked files --- +say ""; say "-- PII heuristics (tracked files) --" +cd "$CLONE" +EMAILS=$(git grep -hoiE '[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}' -- ':!*.lock' 2>/dev/null \ + | grep -viE '@(viktorbarzin\.me|example\.(com|org|test)|test\.(com|local)|localhost|users\.noreply\.github\.com|googlegroups\.com)' \ + | grep -viE '^(noreply|no-reply|ci|admin|info|support|hello|user|foo|bar|test.*)@' \ + | sort -u | head -20) +if [ -n "$EMAILS" ]; then say "real-looking emails found:"; say "$EMAILS"; say "(review: PII?)"; DIRTY=1; else say "emails: none beyond allowlist"; fi +KEYS=$(git grep -l 'BEGIN.*PRIVATE KEY' 2>/dev/null | head -5) +[ -n "$KEYS" ] && { say "PRIVATE KEY blocks in: $KEYS"; DIRTY=1; } || say "private keys: none" +ENVF=$(git ls-files | grep -E '(^|/)\.env($|\.)' | head -5) +[ -n "$ENVF" ] && { say "committed .env files: $ENVF (review)"; DIRTY=1; } || say ".env files: none" +FIXTURES=$(git ls-files | grep -iE '(fixtures?|testdata|tests?/data)/' | head -10) +if [ -n "$FIXTURES" ]; then say "fixture files present (eyeball for PII):"; say "$FIXTURES"; else say "fixtures: none"; fi + +say "" +if [ "$ERR" = 1 ]; then say "VERDICT: ERROR (scanner failed — fix and re-run)"; exit 2; fi +if [ "$DIRTY" = 1 ]; then say "VERDICT: DIRTY — repo stays PRIVATE (do not rewrite history)"; exit 1; fi +say "VERDICT: CLEAN — public flip approved" +exit 0 diff --git a/stacks/kyverno/modules/kyverno/ghcr-credentials.tf b/stacks/kyverno/modules/kyverno/ghcr-credentials.tf index 9586163c..2ca0bf25 100644 --- a/stacks/kyverno/modules/kyverno/ghcr-credentials.tf +++ b/stacks/kyverno/modules/kyverno/ghcr-credentials.tf @@ -13,6 +13,10 @@ locals { ghcr_private_namespaces = [ "tripit", + # tuya-bridge runs a PUBLIC-decision image, but new ghcr packages default + # PRIVATE until their visibility is flipped (UI) — safety net so pulls + # work from the first deploy; prune once the package is public. + "tuya-bridge", "f1-stream", "job-hunter", "instagram-poster", diff --git a/stacks/tuya-bridge/main.tf b/stacks/tuya-bridge/main.tf index 85b1a0b5..bca2299c 100644 --- a/stacks/tuya-bridge/main.tf +++ b/stacks/tuya-bridge/main.tf @@ -3,7 +3,7 @@ resource "kubernetes_namespace" "tuya-bridge" { name = "tuya-bridge" labels = { "istio-injection" : "disabled" - tier = local.tiers.cluster + tier = local.tiers.cluster "keel.sh/enrolled" = "true" } } @@ -75,8 +75,13 @@ resource "kubernetes_deployment" "tuya-bridge" { image_pull_secrets { name = "registry-credentials" } + # ghcr image (ADR-0002 off-infra builds); secret cloned by the kyverno + # sync-ghcr-credentials policy (safety net while the package is private). + image_pull_secrets { + name = "ghcr-credentials" + } container { - image = "forgejo.viktorbarzin.me/viktor/tuya_bridge:${var.image_tag}" + image = "ghcr.io/viktorbarzin/tuya_bridge:${var.image_tag}" image_pull_policy = "IfNotPresent" name = "tuya-bridge" port {