Ratelimit
This commit is contained in:
@@ -1,5 +1,25 @@
|
|||||||
npm install
|
#!/usr/bin/env bash
|
||||||
docker build -t registry.c5ai.ch/pieced/pieced-portal:0.1.4 .
|
set -euo pipefail
|
||||||
docker push registry.c5ai.ch/pieced/pieced-portal:0.1.4
|
|
||||||
kubectl rollout restart deployment pieced-portal -n pieced-system
|
|
||||||
|
|
||||||
|
VERSION="${1:-}"
|
||||||
|
REGISTRY="registry.c5ai.ch/pieced/pieced-portal"
|
||||||
|
|
||||||
|
if [[ -z "$VERSION" ]]; then
|
||||||
|
echo "Usage: ./buildanddeploy.sh <version>"
|
||||||
|
echo "Example: ./buildanddeploy.sh 0.2.0"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building pieced-portal:${VERSION}..."
|
||||||
|
npm install
|
||||||
|
docker build -t "${REGISTRY}:${VERSION}" .
|
||||||
|
docker push "${REGISTRY}:${VERSION}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✓ Pushed ${REGISTRY}:${VERSION}"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Update image tag in pieced-gitops/apps/portal/deployment.yaml:"
|
||||||
|
echo " image: ${REGISTRY}:${VERSION}"
|
||||||
|
echo " 2. git commit + push to pieced-gitops"
|
||||||
|
echo " 3. ArgoCD syncs automatically"
|
||||||
|
|||||||
4
scripts/cilium-audit-results-20260412-170456.md
Normal file
4
scripts/cilium-audit-results-20260412-170456.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Cilium Network Policy Audit Results
|
||||||
|
|
||||||
|
| Test | From | To | Expected | Actual | Result |
|
||||||
|
|------|------|----|----------|--------|--------|
|
||||||
37
scripts/cilium-audit-results-20260412-170833.md
Normal file
37
scripts/cilium-audit-results-20260412-170833.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Cilium Network Policy Audit Results
|
||||||
|
|
||||||
|
| Test | From | To | Expected | Actual | Result |
|
||||||
|
|------|------|----|----------|--------|--------|
|
||||||
|
| Cross-tenant: alpha→testfirma:18789 | tenant-alpha | openclaw.tenant-testfirma:18789 | block | blocked | ✅ PASS |
|
||||||
|
| Cross-tenant: testfirma→alpha:18789 | tenant-testfirma | openclaw.tenant-alpha:18789 | block | blocked | ✅ PASS |
|
||||||
|
| Cross-tenant: alpha→testfirma:18793 | tenant-alpha | openclaw.tenant-testfirma:18793 | block | blocked | ✅ PASS |
|
||||||
|
| Cross-tenant: alpha→testfirma:9090 | tenant-alpha | openclaw.tenant-testfirma:9090 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→OpenBao | tenant-alpha | openbao:8200 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→ZITADEL (svc) | tenant-alpha | zitadel:8080 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→Portal | tenant-alpha | pieced-portal:3000 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→Portal DB | tenant-alpha | portal-db-rw:5432 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→ArgoCD | tenant-alpha | argocd-server:443 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→K8s API | tenant-alpha | kubernetes.default:443 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→K8s API | tenant-testfirma | kubernetes.default:443 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→DNS | tenant-alpha | kube-dns | allow | allowed | ✅ PASS |
|
||||||
|
| Tenant→LiteLLM | tenant-alpha | litellm.inference:4000 | allow | allowed | ✅ PASS |
|
||||||
|
| Tenant→world:443 | tenant-alpha | httpbin.org:443 | allow | allowed | ✅ PASS |
|
||||||
|
| Platform→OpenBao | pieced-system | openbao:8200 | allow | **BLOCKED** | ❌ FAIL |
|
||||||
|
| Platform→ZITADEL | pieced-system | zitadel:8080 | allow | allowed | ✅ PASS |
|
||||||
|
| Platform→K8s API | pieced-system | kubernetes.default:443 | allow | allowed | ✅ PASS |
|
||||||
|
| Platform→LiteLLM | pieced-system | litellm.inference:4000 | allow | allowed | ✅ PASS |
|
||||||
|
| Platform→Portal DB | pieced-system | portal-db-rw:5432 | allow | **BLOCKED** | ❌ FAIL |
|
||||||
|
| Tenant→Operator | tenant-alpha | pieced-operator:8080 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→metadata endpoint | tenant-alpha | 169.254.169.254 | block | blocked | ✅ PASS |
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
- **Passed**: 19
|
||||||
|
- **Failed**: 2
|
||||||
|
- **Date**: 2026-04-12 15:09:45 UTC
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- DNS exfiltration: DNS is allowed for tenants (required for egress). DNS tunneling is a theoretical risk — acceptable for pilot. Consider Cilium DNS-aware policies post-pilot.
|
||||||
|
- LiteLLM namespace: Tests assume `litellm.inference.svc:4000`. Adjust if your LiteLLM is in a different namespace.
|
||||||
|
- K8s API blocking: If this test fails, you need an explicit CiliumClusterwideNetworkPolicy denying egress to the API server CIDR from tenant namespaces. The API server is typically at the host IP or 10.96.0.1, not in a pod namespace, so namespace-based deny may not cover it.
|
||||||
37
scripts/cilium-audit-results-20260412-171458.md
Normal file
37
scripts/cilium-audit-results-20260412-171458.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Cilium Network Policy Audit Results
|
||||||
|
|
||||||
|
| Test | From | To | Expected | Actual | Result |
|
||||||
|
|------|------|----|----------|--------|--------|
|
||||||
|
| Cross-tenant: alpha→testfirma:18789 | tenant-alpha | openclaw.tenant-testfirma:18789 | block | blocked | ✅ PASS |
|
||||||
|
| Cross-tenant: testfirma→alpha:18789 | tenant-testfirma | openclaw.tenant-alpha:18789 | block | blocked | ✅ PASS |
|
||||||
|
| Cross-tenant: alpha→testfirma:18793 | tenant-alpha | openclaw.tenant-testfirma:18793 | block | blocked | ✅ PASS |
|
||||||
|
| Cross-tenant: alpha→testfirma:9090 | tenant-alpha | openclaw.tenant-testfirma:9090 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→OpenBao | tenant-alpha | openbao:8200 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→ZITADEL (svc) | tenant-alpha | zitadel:8080 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→Portal | tenant-alpha | pieced-portal:3000 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→Portal DB | tenant-alpha | portal-db-rw:5432 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→ArgoCD | tenant-alpha | argocd-server:443 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→K8s API | tenant-alpha | kubernetes.default:443 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→K8s API | tenant-testfirma | kubernetes.default:443 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→DNS | tenant-alpha | kube-dns | allow | allowed | ✅ PASS |
|
||||||
|
| Tenant→LiteLLM | tenant-alpha | litellm.inference:4000 | allow | allowed | ✅ PASS |
|
||||||
|
| Tenant→world:443 | tenant-alpha | httpbin.org:443 | allow | allowed | ✅ PASS |
|
||||||
|
| Platform→OpenBao | pieced-system | openbao:8200 | allow | allowed | ✅ PASS |
|
||||||
|
| Platform→ZITADEL | pieced-system | zitadel:8080 | allow | allowed | ✅ PASS |
|
||||||
|
| Platform→K8s API | pieced-system | kubernetes.default:443 | allow | allowed | ✅ PASS |
|
||||||
|
| Platform→LiteLLM | pieced-system | litellm.inference:4000 | allow | allowed | ✅ PASS |
|
||||||
|
| Platform→Portal DB | pieced-system | portal-db-rw:5432 | allow | **BLOCKED** | ❌ FAIL |
|
||||||
|
| Tenant→Operator | tenant-alpha | pieced-operator:8080 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→metadata endpoint | tenant-alpha | 169.254.169.254 | block | blocked | ✅ PASS |
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
- **Passed**: 20
|
||||||
|
- **Failed**: 1
|
||||||
|
- **Date**: 2026-04-12 15:16:10 UTC
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- DNS exfiltration: DNS is allowed for tenants (required for egress). DNS tunneling is a theoretical risk — acceptable for pilot. Consider Cilium DNS-aware policies post-pilot.
|
||||||
|
- LiteLLM namespace: Tests assume `litellm.inference.svc:4000`. Adjust if your LiteLLM is in a different namespace.
|
||||||
|
- K8s API blocking: If this test fails, you need an explicit CiliumClusterwideNetworkPolicy denying egress to the API server CIDR from tenant namespaces. The API server is typically at the host IP or 10.96.0.1, not in a pod namespace, so namespace-based deny may not cover it.
|
||||||
37
scripts/cilium-audit-results-20260412-171801.md
Normal file
37
scripts/cilium-audit-results-20260412-171801.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Cilium Network Policy Audit Results
|
||||||
|
|
||||||
|
| Test | From | To | Expected | Actual | Result |
|
||||||
|
|------|------|----|----------|--------|--------|
|
||||||
|
| Cross-tenant: alpha→testfirma:18789 | tenant-alpha | openclaw.tenant-testfirma:18789 | block | blocked | ✅ PASS |
|
||||||
|
| Cross-tenant: testfirma→alpha:18789 | tenant-testfirma | openclaw.tenant-alpha:18789 | block | blocked | ✅ PASS |
|
||||||
|
| Cross-tenant: alpha→testfirma:18793 | tenant-alpha | openclaw.tenant-testfirma:18793 | block | blocked | ✅ PASS |
|
||||||
|
| Cross-tenant: alpha→testfirma:9090 | tenant-alpha | openclaw.tenant-testfirma:9090 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→OpenBao | tenant-alpha | openbao:8200 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→ZITADEL (svc) | tenant-alpha | zitadel:8080 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→Portal | tenant-alpha | pieced-portal:3000 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→Portal DB | tenant-alpha | portal-db-rw:5432 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→ArgoCD | tenant-alpha | argocd-server:443 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→K8s API | tenant-alpha | kubernetes.default:443 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→K8s API | tenant-testfirma | kubernetes.default:443 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→DNS | tenant-alpha | kube-dns | allow | allowed | ✅ PASS |
|
||||||
|
| Tenant→LiteLLM | tenant-alpha | litellm.inference:4000 | allow | allowed | ✅ PASS |
|
||||||
|
| Tenant→world:443 | tenant-alpha | httpbin.org:443 | allow | allowed | ✅ PASS |
|
||||||
|
| Platform→OpenBao | pieced-system | openbao:8200 | allow | allowed | ✅ PASS |
|
||||||
|
| Platform→ZITADEL | pieced-system | zitadel:8080 | allow | allowed | ✅ PASS |
|
||||||
|
| Platform→K8s API | pieced-system | kubernetes.default:443 | allow | allowed | ✅ PASS |
|
||||||
|
| Platform→LiteLLM | pieced-system | litellm.inference:4000 | allow | allowed | ✅ PASS |
|
||||||
|
| Platform→Portal DB | pieced-system | portal-db-rw:5432 | allow | **BLOCKED** | ❌ FAIL |
|
||||||
|
| Tenant→Operator | tenant-alpha | pieced-operator:8080 | block | blocked | ✅ PASS |
|
||||||
|
| Tenant→metadata endpoint | tenant-alpha | 169.254.169.254 | block | blocked | ✅ PASS |
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
- **Passed**: 20
|
||||||
|
- **Failed**: 1
|
||||||
|
- **Date**: 2026-04-12 15:19:15 UTC
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- DNS exfiltration: DNS is allowed for tenants (required for egress). DNS tunneling is a theoretical risk — acceptable for pilot. Consider Cilium DNS-aware policies post-pilot.
|
||||||
|
- LiteLLM namespace: Tests assume `litellm.inference.svc:4000`. Adjust if your LiteLLM is in a different namespace.
|
||||||
|
- K8s API blocking: If this test fails, you need an explicit CiliumClusterwideNetworkPolicy denying egress to the API server CIDR from tenant namespaces. The API server is typically at the host IP or 10.96.0.1, not in a pod namespace, so namespace-based deny may not cover it.
|
||||||
283
scripts/cilium-audit.sh
Normal file
283
scripts/cilium-audit.sh
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================================
|
||||||
|
# PieCed IT — Session 7.1: Cilium Network Policy Audit
|
||||||
|
# ============================================================================
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - kubectl configured for the cluster
|
||||||
|
# - Existing pods:
|
||||||
|
# tenant-alpha/openclaw-0 (3 containers)
|
||||||
|
# tenant-testfirma/openclaw-0 (3 containers)
|
||||||
|
# pieced-system/pieced-portal-* (1 container)
|
||||||
|
#
|
||||||
|
# This script deploys temporary netshoot pods (they have curl, nslookup, etc.)
|
||||||
|
# into each namespace, runs the tests, then cleans up.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# chmod +x cilium-audit.sh
|
||||||
|
# ./cilium-audit.sh
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GRN='\033[0;32m'
|
||||||
|
YLW='\033[1;33m'
|
||||||
|
RST='\033[0m'
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
WARN=0
|
||||||
|
|
||||||
|
# Results file
|
||||||
|
RESULTS_FILE="cilium-audit-results-$(date +%Y%m%d-%H%M%S).md"
|
||||||
|
|
||||||
|
log_header() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${YLW}═══════════════════════════════════════════════════${RST}"
|
||||||
|
echo -e "${YLW} $1${RST}"
|
||||||
|
echo -e "${YLW}═══════════════════════════════════════════════════${RST}"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_result() {
|
||||||
|
local test_name="$1"
|
||||||
|
local from_ns="$2"
|
||||||
|
local to_target="$3"
|
||||||
|
local expected="$4" # "block" or "allow"
|
||||||
|
local actual="$5" # exit code from curl/nslookup: 0=success, non-0=fail
|
||||||
|
|
||||||
|
if [[ "$expected" == "block" ]]; then
|
||||||
|
if [[ "$actual" -ne 0 ]]; then
|
||||||
|
echo -e " ${GRN}✓ PASS${RST} [$from_ns → $to_target] $test_name (blocked as expected)"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
echo "| $test_name | $from_ns | $to_target | block | blocked | ✅ PASS |" >> "$RESULTS_FILE"
|
||||||
|
else
|
||||||
|
echo -e " ${RED}✗ FAIL${RST} [$from_ns → $to_target] $test_name (SHOULD BE BLOCKED but succeeded!)"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
echo "| $test_name | $from_ns | $to_target | block | **ALLOWED** | ❌ FAIL |" >> "$RESULTS_FILE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [[ "$actual" -eq 0 ]]; then
|
||||||
|
echo -e " ${GRN}✓ PASS${RST} [$from_ns → $to_target] $test_name (allowed as expected)"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
echo "| $test_name | $from_ns | $to_target | allow | allowed | ✅ PASS |" >> "$RESULTS_FILE"
|
||||||
|
else
|
||||||
|
echo -e " ${RED}✗ FAIL${RST} [$from_ns → $to_target] $test_name (SHOULD BE ALLOWED but blocked!)"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
echo "| $test_name | $from_ns | $to_target | allow | **BLOCKED** | ❌ FAIL |" >> "$RESULTS_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Deploy netshoot pods
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
deploy_netshoot() {
|
||||||
|
local ns="$1"
|
||||||
|
local name="netshoot-audit"
|
||||||
|
echo " Deploying netshoot in $ns..."
|
||||||
|
kubectl run "$name" -n "$ns" \
|
||||||
|
--image=nicolaka/netshoot \
|
||||||
|
--restart=Never \
|
||||||
|
--labels="app=netshoot-audit" \
|
||||||
|
--command -- sleep 600 2>/dev/null || true
|
||||||
|
kubectl wait --for=condition=Ready pod/"$name" -n "$ns" --timeout=60s
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_netshoot() {
|
||||||
|
echo ""
|
||||||
|
echo "Cleaning up netshoot pods..."
|
||||||
|
for ns in tenant-alpha tenant-testfirma pieced-system; do
|
||||||
|
kubectl delete pod netshoot-audit -n "$ns" --ignore-not-found --wait=false 2>/dev/null || true
|
||||||
|
done
|
||||||
|
echo "Done."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clean up on exit
|
||||||
|
trap cleanup_netshoot EXIT
|
||||||
|
|
||||||
|
# Run a command in netshoot pod, return exit code
|
||||||
|
# Uses --connect-timeout 5 for curl, timeout 5 for nslookup
|
||||||
|
run_in() {
|
||||||
|
local ns="$1"
|
||||||
|
shift
|
||||||
|
kubectl exec -n "$ns" netshoot-audit -- "$@" >/dev/null 2>&1
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Start
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "PieCed IT — Cilium Network Policy Audit"
|
||||||
|
echo "Date: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Initialize results markdown
|
||||||
|
cat > "$RESULTS_FILE" <<'EOF'
|
||||||
|
# Cilium Network Policy Audit Results
|
||||||
|
|
||||||
|
| Test | From | To | Expected | Actual | Result |
|
||||||
|
|------|------|----|----------|--------|--------|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Deploy netshoot pods
|
||||||
|
log_header "Deploying audit pods"
|
||||||
|
deploy_netshoot tenant-alpha
|
||||||
|
deploy_netshoot tenant-testfirma
|
||||||
|
deploy_netshoot pieced-system
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# SECTION 1: Tenant-to-Tenant Isolation
|
||||||
|
# ============================================================================
|
||||||
|
log_header "1. Tenant-to-Tenant Isolation"
|
||||||
|
|
||||||
|
# tenant-alpha → tenant-testfirma OpenClaw (port 18789)
|
||||||
|
run_in tenant-alpha curl -s --connect-timeout 5 http://openclaw.tenant-testfirma.svc:18789 && rc=0 || rc=$?
|
||||||
|
log_result "Cross-tenant: alpha→testfirma:18789" "tenant-alpha" "openclaw.tenant-testfirma:18789" "block" "$rc"
|
||||||
|
|
||||||
|
# tenant-testfirma → tenant-alpha OpenClaw (port 18789)
|
||||||
|
run_in tenant-testfirma curl -s --connect-timeout 5 http://openclaw.tenant-alpha.svc:18789 && rc=0 || rc=$?
|
||||||
|
log_result "Cross-tenant: testfirma→alpha:18789" "tenant-testfirma" "openclaw.tenant-alpha:18789" "block" "$rc"
|
||||||
|
|
||||||
|
# Cross-tenant on other OpenClaw ports
|
||||||
|
run_in tenant-alpha curl -s --connect-timeout 5 http://openclaw.tenant-testfirma.svc:18793 && rc=0 || rc=$?
|
||||||
|
log_result "Cross-tenant: alpha→testfirma:18793" "tenant-alpha" "openclaw.tenant-testfirma:18793" "block" "$rc"
|
||||||
|
|
||||||
|
run_in tenant-alpha curl -s --connect-timeout 5 http://openclaw.tenant-testfirma.svc:9090 && rc=0 || rc=$?
|
||||||
|
log_result "Cross-tenant: alpha→testfirma:9090" "tenant-alpha" "openclaw.tenant-testfirma:9090" "block" "$rc"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# SECTION 2: Tenant → Platform Services (must be blocked except LiteLLM)
|
||||||
|
# ============================================================================
|
||||||
|
log_header "2. Tenant → Platform Services"
|
||||||
|
|
||||||
|
# OpenBao
|
||||||
|
run_in tenant-alpha curl -s --connect-timeout 5 http://openbao.openbao-system.svc:8200/v1/sys/health && rc=0 || rc=$?
|
||||||
|
log_result "Tenant→OpenBao" "tenant-alpha" "openbao:8200" "block" "$rc"
|
||||||
|
|
||||||
|
# ZITADEL (direct svc, not via ingress)
|
||||||
|
run_in tenant-alpha curl -s --connect-timeout 5 http://zitadel.zitadel.svc:8080/debug/healthz && rc=0 || rc=$?
|
||||||
|
log_result "Tenant→ZITADEL (svc)" "tenant-alpha" "zitadel:8080" "block" "$rc"
|
||||||
|
|
||||||
|
# Portal
|
||||||
|
run_in tenant-alpha curl -s --connect-timeout 5 http://pieced-portal.pieced-system.svc:3000 && rc=0 || rc=$?
|
||||||
|
log_result "Tenant→Portal" "tenant-alpha" "pieced-portal:3000" "block" "$rc"
|
||||||
|
|
||||||
|
# Portal DB
|
||||||
|
run_in tenant-alpha curl -s --connect-timeout 5 http://portal-db-rw.pieced-system.svc:5432 && rc=0 || rc=$?
|
||||||
|
log_result "Tenant→Portal DB" "tenant-alpha" "portal-db-rw:5432" "block" "$rc"
|
||||||
|
|
||||||
|
# ArgoCD
|
||||||
|
run_in tenant-alpha curl -sk --connect-timeout 5 https://argocd-server.argocd.svc:443 && rc=0 || rc=$?
|
||||||
|
log_result "Tenant→ArgoCD" "tenant-alpha" "argocd-server:443" "block" "$rc"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# SECTION 3: Tenant → K8s API Server (must be blocked)
|
||||||
|
# ============================================================================
|
||||||
|
log_header "3. Tenant → K8s API Server"
|
||||||
|
|
||||||
|
run_in tenant-alpha curl -sk --connect-timeout 5 https://kubernetes.default.svc:443/version && rc=0 || rc=$?
|
||||||
|
log_result "Tenant→K8s API" "tenant-alpha" "kubernetes.default:443" "block" "$rc"
|
||||||
|
|
||||||
|
# Also test from the other tenant
|
||||||
|
run_in tenant-testfirma curl -sk --connect-timeout 5 https://kubernetes.default.svc:443/version && rc=0 || rc=$?
|
||||||
|
log_result "Tenant→K8s API" "tenant-testfirma" "kubernetes.default:443" "block" "$rc"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# SECTION 4: Tenant → Allowed Paths (must succeed)
|
||||||
|
# ============================================================================
|
||||||
|
log_header "4. Tenant → Allowed Paths"
|
||||||
|
|
||||||
|
# DNS resolution
|
||||||
|
run_in tenant-alpha nslookup -timeout=5 google.com && rc=0 || rc=$?
|
||||||
|
log_result "Tenant→DNS" "tenant-alpha" "kube-dns" "allow" "$rc"
|
||||||
|
|
||||||
|
# LiteLLM (adjust namespace if different — check your actual LiteLLM svc namespace)
|
||||||
|
# Based on .env.example: LITELLM_INTERNAL_URL=http://litellm.inference.svc:4000
|
||||||
|
run_in tenant-alpha curl -s --connect-timeout 5 http://litellm.inference.svc:4000/health && rc=0 || rc=$?
|
||||||
|
log_result "Tenant→LiteLLM" "tenant-alpha" "litellm.inference:4000" "allow" "$rc"
|
||||||
|
|
||||||
|
# External HTTPS (world:443)
|
||||||
|
run_in tenant-alpha curl -s --connect-timeout 5 https://httpbin.org/status/200 && rc=0 || rc=$?
|
||||||
|
log_result "Tenant→world:443" "tenant-alpha" "httpbin.org:443" "allow" "$rc"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# SECTION 5: Platform Pod → Platform Services (must succeed)
|
||||||
|
# ============================================================================
|
||||||
|
log_header "5. Platform → Platform Services"
|
||||||
|
|
||||||
|
# Platform → OpenBao
|
||||||
|
run_in pieced-system curl -s --connect-timeout 5 http://openbao.openbao.svc:8200/v1/sys/health && rc=0 || rc=$?
|
||||||
|
log_result "Platform→OpenBao" "pieced-system" "openbao:8200" "allow" "$rc"
|
||||||
|
|
||||||
|
# Platform → ZITADEL
|
||||||
|
run_in pieced-system curl -s --connect-timeout 5 http://zitadel.zitadel.svc:8080/debug/healthz && rc=0 || rc=$?
|
||||||
|
log_result "Platform→ZITADEL" "pieced-system" "zitadel:8080" "allow" "$rc"
|
||||||
|
|
||||||
|
# Platform → K8s API
|
||||||
|
run_in pieced-system curl -sk --connect-timeout 5 https://kubernetes.default.svc:443/version && rc=0 || rc=$?
|
||||||
|
log_result "Platform→K8s API" "pieced-system" "kubernetes.default:443" "allow" "$rc"
|
||||||
|
|
||||||
|
# Platform → LiteLLM
|
||||||
|
run_in pieced-system curl -s --connect-timeout 5 http://litellm.inference.svc:4000/health && rc=0 || rc=$?
|
||||||
|
log_result "Platform→LiteLLM" "pieced-system" "litellm.inference:4000" "allow" "$rc"
|
||||||
|
|
||||||
|
# Platform → Portal DB (internal connectivity)
|
||||||
|
run_in pieced-system curl -s --connect-timeout 5 http://portal-db-rw.pieced-system.svc:5432 && rc=0 || rc=$?
|
||||||
|
log_result "Platform→Portal DB" "pieced-system" "portal-db-rw:5432" "allow" "$rc"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# SECTION 6: Reverse — Tenant → Platform Pod (must be blocked)
|
||||||
|
# ============================================================================
|
||||||
|
log_header "6. Tenant → Platform Pods (reverse check)"
|
||||||
|
|
||||||
|
# Tenant → operator
|
||||||
|
run_in tenant-alpha curl -s --connect-timeout 5 http://pieced-operator.pieced-system.svc:8080 && rc=0 || rc=$?
|
||||||
|
log_result "Tenant→Operator" "tenant-alpha" "pieced-operator:8080" "block" "$rc"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# SECTION 7: Metadata / Edge Cases
|
||||||
|
# ============================================================================
|
||||||
|
log_header "7. Edge Cases"
|
||||||
|
|
||||||
|
# Cloud metadata endpoint (should be unreachable on bare metal, but verify)
|
||||||
|
run_in tenant-alpha curl -s --connect-timeout 3 http://169.254.169.254/latest/meta-data/ && rc=0 || rc=$?
|
||||||
|
log_result "Tenant→metadata endpoint" "tenant-alpha" "169.254.169.254" "block" "$rc"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Summary
|
||||||
|
# ============================================================================
|
||||||
|
echo ""
|
||||||
|
echo -e "${YLW}═══════════════════════════════════════════════════${RST}"
|
||||||
|
echo -e "${YLW} SUMMARY${RST}"
|
||||||
|
echo -e "${YLW}═══════════════════════════════════════════════════${RST}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${GRN}Passed: $PASS${RST}"
|
||||||
|
echo -e " ${RED}Failed: $FAIL${RST}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Append summary to results file
|
||||||
|
cat >> "$RESULTS_FILE" <<EOF
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
- **Passed**: $PASS
|
||||||
|
- **Failed**: $FAIL
|
||||||
|
- **Date**: $(date -u '+%Y-%m-%d %H:%M:%S UTC')
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- DNS exfiltration: DNS is allowed for tenants (required for egress). DNS tunneling is a theoretical risk — acceptable for pilot. Consider Cilium DNS-aware policies post-pilot.
|
||||||
|
- LiteLLM namespace: Tests assume \`litellm.inference.svc:4000\`. Adjust if your LiteLLM is in a different namespace.
|
||||||
|
- K8s API blocking: If this test fails, you need an explicit CiliumClusterwideNetworkPolicy denying egress to the API server CIDR from tenant namespaces. The API server is typically at the host IP or 10.96.0.1, not in a pod namespace, so namespace-based deny may not cover it.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Full results written to: $RESULTS_FILE"
|
||||||
|
|
||||||
|
if [[ $FAIL -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}⚠ $FAIL test(s) failed — review results and fix network policies.${RST}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { registerCustomer } from "@/lib/zitadel";
|
import { registerCustomer } from "@/lib/zitadel";
|
||||||
|
import { rateLimit } from "@/lib/rate-limit";
|
||||||
import type { RegistrationInput } from "@/types";
|
import type { RegistrationInput } from "@/types";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -11,7 +12,34 @@ const registrationSchema = z.object({
|
|||||||
preferredLanguage: z.enum(["en", "de", "fr", "it"]).optional(),
|
preferredLanguage: z.enum(["en", "de", "fr", "it"]).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
/** 3 registrations per IP per hour */
|
||||||
|
const RATE_LIMIT = 3;
|
||||||
|
const RATE_WINDOW_MS = 3_600_000; // 1 hour
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
// --- Rate limiting ---
|
||||||
|
const ip =
|
||||||
|
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ??
|
||||||
|
request.headers.get("x-real-ip") ??
|
||||||
|
"unknown";
|
||||||
|
|
||||||
|
const rl = rateLimit(`register:${ip}`, RATE_LIMIT, RATE_WINDOW_MS);
|
||||||
|
|
||||||
|
if (!rl.allowed) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Too many registration attempts. Please try again later." },
|
||||||
|
{
|
||||||
|
status: 429,
|
||||||
|
headers: {
|
||||||
|
"Retry-After": String(Math.ceil(rl.resetMs / 1000)),
|
||||||
|
"X-RateLimit-Limit": String(RATE_LIMIT),
|
||||||
|
"X-RateLimit-Remaining": "0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Validation ---
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const parsed = registrationSchema.safeParse(body);
|
const parsed = registrationSchema.safeParse(body);
|
||||||
@@ -19,7 +47,7 @@ export async function POST(request: Request) {
|
|||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Validation failed", details: parsed.error.flatten() },
|
{ error: "Validation failed", details: parsed.error.flatten() },
|
||||||
{ status: 400 }
|
{ status: 400 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,9 +65,16 @@ export async function POST(request: Request) {
|
|||||||
{
|
{
|
||||||
orgId: result.orgId,
|
orgId: result.orgId,
|
||||||
userId: result.userId,
|
userId: result.userId,
|
||||||
message: "Registration successful. You will receive an invitation email to set your password.",
|
message:
|
||||||
|
"Registration successful. You will receive an invitation email to set your password.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 201,
|
||||||
|
headers: {
|
||||||
|
"X-RateLimit-Limit": String(RATE_LIMIT),
|
||||||
|
"X-RateLimit-Remaining": String(rl.remaining),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{ status: 201 }
|
|
||||||
);
|
);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Registration failed:", e);
|
console.error("Registration failed:", e);
|
||||||
@@ -48,14 +83,14 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: zitadelMessage || "Registration failed. Please try again." },
|
{ error: zitadelMessage || "Registration failed. Please try again." },
|
||||||
{ status: e.statusCode || 500 }
|
{ status: e.statusCode || 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ZITADEL errors come as:
|
* ZITADEL errors come as:
|
||||||
* "ZITADEL POST /path: 400 {"code":3, "message":"..."}"
|
* "ZITADEL POST /path: 400 {"code":3, "message":"..."}"
|
||||||
* Extract the human-readable "message" field.
|
* Extract the human-readable "message" field.
|
||||||
*/
|
*/
|
||||||
function extractZitadelMessage(errorMsg: string): string | null {
|
function extractZitadelMessage(errorMsg: string): string | null {
|
||||||
|
|||||||
71
src/lib/rate-limit.ts
Normal file
71
src/lib/rate-limit.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* In-memory sliding-window rate limiter.
|
||||||
|
*
|
||||||
|
* Suitable for single-node deployments (pilot scale).
|
||||||
|
* For multi-replica, replace with Redis-backed store.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface RateLimitEntry {
|
||||||
|
timestamps: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = new Map<string, RateLimitEntry>();
|
||||||
|
|
||||||
|
// Cleanup stale entries every 10 minutes
|
||||||
|
if (typeof globalThis !== "undefined") {
|
||||||
|
// Use globalThis to survive HMR in dev — only one interval
|
||||||
|
const key = "__rateLimitCleanup";
|
||||||
|
if (!(globalThis as any)[key]) {
|
||||||
|
(globalThis as any)[key] = setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [k, entry] of store) {
|
||||||
|
entry.timestamps = entry.timestamps.filter((t) => now - t < 3_600_000);
|
||||||
|
if (entry.timestamps.length === 0) store.delete(k);
|
||||||
|
}
|
||||||
|
}, 600_000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RateLimitResult {
|
||||||
|
allowed: boolean;
|
||||||
|
remaining: number;
|
||||||
|
/** Milliseconds until the oldest request in the window expires */
|
||||||
|
resetMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and record a rate-limited action.
|
||||||
|
*
|
||||||
|
* @param key - Unique key, e.g. `register:${ip}`
|
||||||
|
* @param limit - Max allowed actions in the window
|
||||||
|
* @param windowMs - Window size in milliseconds
|
||||||
|
*/
|
||||||
|
export function rateLimit(
|
||||||
|
key: string,
|
||||||
|
limit: number,
|
||||||
|
windowMs: number,
|
||||||
|
): RateLimitResult {
|
||||||
|
const now = Date.now();
|
||||||
|
const entry = store.get(key) ?? { timestamps: [] };
|
||||||
|
|
||||||
|
// Prune expired timestamps
|
||||||
|
entry.timestamps = entry.timestamps.filter((t) => now - t < windowMs);
|
||||||
|
|
||||||
|
if (entry.timestamps.length >= limit) {
|
||||||
|
const oldest = entry.timestamps[0];
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
remaining: 0,
|
||||||
|
resetMs: oldest + windowMs - now,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.timestamps.push(now);
|
||||||
|
store.set(key, entry);
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: true,
|
||||||
|
remaining: limit - entry.timestamps.length,
|
||||||
|
resetMs: entry.timestamps[0] + windowMs - now,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user