diff --git a/.github/workflows/reusable-gatekeeper.yml b/.github/workflows/reusable-gatekeeper.yml new file mode 100644 index 0000000..f9ca5ed --- /dev/null +++ b/.github/workflows/reusable-gatekeeper.yml @@ -0,0 +1,264 @@ +name: Reusable Gatekeeper Policy Enforcer + +on: + workflow_call: + inputs: + aws-role-arn: + required: true + type: string + kube-infra-ref: + required: false + type: string + default: 'reusable_gatekeeper' + caller-repo-name: + required: true + type: string + description: "Must be either 'kube-infra' or 'kube-platform-apps'" + aws-region: + required: false + type: string + default: "ap-southeast-2" + secrets: + GEONETCI_EKS_WORKFLOW: + required: true + +env: + REQUIRED_STRING_GENERIC: ${{ startsWith(github.repository, 'GeoNet/') == false }} + REQUIRED_STRING_APPS: ${{ startsWith(github.repository, 'GeoNet/') == true }} + +jobs: + enumerate-dev-clusters: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + outputs: + cluster_list: ${{ steps.get-clusters.outputs.matrix }} + steps: + - name: Validate Repository Organization + run: | + if [ "${{ env.REQUIRED_STRING_GENERIC }}" = "true" ]; then + echo "Error: This generic workflow must be run from within the GeoNet organization." >&2 + exit 1 + fi + + if [ "${{ inputs.caller-repo-name }}" = "kube-platform-apps" ] && [ "${{ env.REQUIRED_STRING_APPS }}" = "false" ]; then + echo "Error: App deployment workflows are restricted strictly to GeoNet repositories." >&2 + exit 1 + fi + + - name: Configure Dev AWS Credentials + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + role-to-assume: ${{ inputs.aws-role-arn }} + aws-region: ${{ inputs.aws-region }} + + - name: Enumerate EKS Clusters + id: get-clusters + run: | + CLUSTER_JSON=$(aws eks list-clusters --region "${{ inputs.aws-region }}" --query "clusters" --output json | jq -c '.') + echo "matrix=${CLUSTER_JSON}" >> $GITHUB_OUTPUT + + gatekeeper-enforce-dev: + needs: enumerate-dev-clusters + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + cluster: ${{ fromJson(needs.enumerate-dev-clusters.outputs.cluster_list) }} + + steps: + - name: Checkout Caller Repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + path: caller-repo + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Checkout Kube-Infra Repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: GeoNet/kube-infra + path: kube-infra + ref: ${{ inputs.kube-infra-ref }} + token: ${{ secrets.GEONETCI_EKS_WORKFLOW }} + + - name: Verify Kube-Infra Checkout + run: | + cd "$GITHUB_WORKSPACE/kube-infra" + + echo "===== INPUT REF =====" + echo "${{ inputs.kube-infra-ref }}" + + echo + echo "===== HEAD SHA =====" + git rev-parse HEAD + + echo + echo "===== CURRENT BRANCH =====" + git branch --show-current || true + + echo + echo "===== LATEST COMMIT =====" + git log -1 --decorate --oneline + + echo + echo "===== SEARCHING FOR CUSTOM STRING =====" + grep -n "SECURITY FAILURE" \ + infrastructure/gatekeeper/constraint-templates/allow-privilege-escalation.yaml \ + || echo "SECURITY FAILURE STRING NOT FOUND" + + - name: Configure Dev AWS Credentials + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + role-to-assume: ${{ inputs.aws-role-arn }} + aws-region: ${{ inputs.aws-region }} + + - name: Build and Enforce (Dev) + run: | + WORKSPACE_ROOT="$GITHUB_WORKSPACE" + AUDIT_SANDBOX="/tmp/gator-sandbox" + mkdir -p "$AUDIT_SANDBOX" + + # 🚀 FIX 1: If running inside kube-infra itself, sync workspace modifications + # directly into the overlay path so kustomize catches your draft fixes! + if [ "${{ inputs.caller-repo-name }}" = "kube-infra" ]; then + echo "🔄 Merging draft workspace policies into compilation tracking directory..." + cp -r "$WORKSPACE_ROOT/caller-repo/infrastructure/gatekeeper/policies/"* "$WORKSPACE_ROOT/kube-infra/infrastructure/gatekeeper/policies/" + fi + + # 1. Compile the self-contained cluster bundle + OVERLAY_PATH="$WORKSPACE_ROOT/kube-infra/infrastructure/gatekeeper/policies/overlays/${{ matrix.cluster }}" + + if [ -d "$OVERLAY_PATH" ]; then + echo "Found complete policy footprint for ${{ matrix.cluster }}. Compiling..." + kubectl kustomize "$OVERLAY_PATH" > "$AUDIT_SANDBOX/complete-cluster-bundle.yaml" + + echo "===== SEARCHING RENDERED BUNDLE =====" + + echo "--- Looking for custom SECURITY FAILURE message ---" + grep -n "SECURITY FAILURE" \ + "$AUDIT_SANDBOX/complete-cluster-bundle.yaml" || echo "NOT FOUND" + + echo "--- Looking for ConstraintTemplate name ---" + grep -n "k8spspallowprivilegeescalationcontainer" \ + "$AUDIT_SANDBOX/complete-cluster-bundle.yaml" || echo "NOT FOUND" + + else + echo "No kustomize overlay found for ${{ matrix.cluster }} at $OVERLAY_PATH, skipping." + exit 0 + fi + + # Initialize arguments using only the clean kustomize bundle + GATOR_EXEC_ARGS="-f $AUDIT_SANDBOX/complete-cluster-bundle.yaml" + + # 2. Track down changed files in the caller repo + cd "$WORKSPACE_ROOT/caller-repo" + + BEFORE_SHA="${{ github.event.before }}" + AFTER_SHA="${{ github.event.after }}" + + if [ "$BEFORE_SHA" = "0000000000000000000000000000000000000000" ] || [ -z "$BEFORE_SHA" ]; then + echo "New branch or missing history. Evaluating changes in the last commit (HEAD~1...HEAD)." + DIFF_RANGE="HEAD~1...HEAD" + else + echo "Fetching push range context to prevent symmetric difference expression errors..." + git fetch origin "$BEFORE_SHA" --no-tags --quiet || true + git fetch origin "$AFTER_SHA" --no-tags --quiet || true + DIFF_RANGE="$BEFORE_SHA...$AFTER_SHA" + fi + + # 3. Scan application directory + if [ "${{ inputs.caller-repo-name }}" = "kube-platform-apps" ]; then + echo "Caller identified as kube-platform-apps. Parsing 'apps/' directory..." + RAW_CHANGES=$(git diff --name-only $DIFF_RANGE -- apps/ | grep -E '.*\.ya?ml$' || true) + + if [ -z "$RAW_CHANGES" ]; then + echo "No application manifests changed in this push. Skipping." + exit 0 + fi + + CHANGED_FILES="" + for file in $RAW_CHANGES; do + APP_DIR=$(echo "$file" | cut -d'/' -f1,2) + if [ -d "$APP_DIR" ]; then + APP_MANIFESTS=$(find "$APP_DIR" -type f \( -name "*.yaml" -o -name "*.yml" \)) + CHANGED_FILES=$(printf "%s\n%s" "$CHANGED_FILES" "$APP_MANIFESTS") + fi + done + CHANGED_FILES=$(echo "$CHANGED_FILES" | sort -u | sed '/^$/d') + fi + + if [ -z "$CHANGED_FILES" ]; then + echo "No relevant manifests changed. Skipping enforcement." + exit 0 + fi + + # 4. Append target app manifests cleanly without any overlapping rules + for file in $CHANGED_FILES; do + TARGET_PATH="$WORKSPACE_ROOT/caller-repo/$file" + if [ -f "$TARGET_PATH" ]; then + echo "✅ Staging target app manifest: $file" + GATOR_EXEC_ARGS="$GATOR_EXEC_ARGS -f $TARGET_PATH" + fi + done + + # 5. Install Gator CLI dynamically + GATOR_VERSION="v3.22.2" + if ! command -v gator &> /dev/null; then + echo "📥 Downloading Gator Engine Binary..." + curl -L "https://github.com/open-policy-agent/gatekeeper/releases/download/${GATOR_VERSION}/gator-${GATOR_VERSION}-linux-amd64.tar.gz" | tar -xz + sudo mv gator /usr/local/bin/ + fi + + echo "=======================================================" + echo "=== RUNNING ISOLATED PRE-FLIGHT VERIFICATION TEST ===" + echo "=======================================================" + + # 1. Compile the pure base blueprint bundle directly + BASE_POLICY_PATH="$WORKSPACE_ROOT/kube-infra/infrastructure/gatekeeper/policies/base" + kubectl kustomize "$BASE_POLICY_PATH" > "$AUDIT_SANDBOX/pure-base-bundle.yaml" + + echo "=======================================================" + echo "=== DEBUG: DUMPING BASELINE PRIVILEGE ESCALATION POLICY ===" + echo "=======================================================" + + echo "📄 --- CONSTRAINT TEMPLATE ---" + find "$WORKSPACE_ROOT/kube-infra/infrastructure/gatekeeper/" -name "*allow-privilege-escalation*" -exec grep -l "ConstraintTemplate" {} \; | xargs cat + + echo "📄 --- ACTUAL CONSTRAINT ---" + find "$WORKSPACE_ROOT/kube-infra/infrastructure/gatekeeper/" -name "*allow-privilege-escalation*" -exec grep -l "kind:" {} \; | grep -v "Template" | xargs cat + + echo "=======================================================" + echo "=== RUNNING ISOLATED PRE-FLIGHT VERIFICATION TEST ===" + echo "=======================================================" + + # 2. Run the isolated test using the raw baseline bundle + set +e + gator test \ + -f "$AUDIT_SANDBOX/pure-base-bundle.yaml" \ + -f "$WORKSPACE_ROOT/caller-repo/apps/test-app/deploy-test-app.yaml" + + PREFLIGHT_EXIT=$? + set -e + echo "Preflight exit code: $PREFLIGHT_EXIT" + + echo "=======================================================" + echo "=== EXECUTING CLEAN UNIFIED PRODUCTION CLUSTER AUDIT ===" + echo "=======================================================" + + set +e + GATOR_OUTPUT=$(gator test $GATOR_EXEC_ARGS 2>&1) + GATOR_EXIT_CODE=$? + set -e + + echo "Gator engine finalized execution with Exit Code: $GATOR_EXIT_CODE" + echo "--- Captured Gator Stream Dump ---" + if [ -z "$GATOR_OUTPUT" ]; then + echo "[SUCCESS] - Manifest complies perfectly with all cluster policies." + else + echo "$GATOR_OUTPUT" + fi + echo "----------------------------------" + + exit $GATOR_EXIT_CODE diff --git a/USAGE.md b/USAGE.md index fe105f5..3040362 100644 --- a/USAGE.md +++ b/USAGE.md @@ -27,6 +27,7 @@ - [Clean container versions](#clean-container-versions) - [ESLint](#eslint) - [AWS deploy](#aws-deploy) + - [Gatekeeper](#gatekeeper) - [Composite Actions](#composite-actions) - [Tagging](#tagging) - [Validate bucket URI](#validate-bucket-uri) @@ -1207,6 +1208,24 @@ The terraform module `ecs_docker_task_ng` can be used to configure services for Some example repos using this workflow: `DevTools` and `gloria`. +### Gatekeeper + +STATUS: alpha + +This reusable workflow evaluates Kubernetes overlay manifests against pre-defined constraint templates before applying the deployment to a cluster + +Example: + +\```yaml +uses: GeoNet/kube-infra/.github/workflows/reusable-gatekeeper.yml@main +with: + aws-dev-role-arn: "arn:aws:iam::123456789012:role/dev-role" + caller-repo-name: "kube-infra" +secrets: + GEONETCI_EKS_WORKFLOW: ${{ secrets.GEONETCI_EKS_WORKFLOW }} +\``` + +The gator binary is used for the evaluation. The constraint templates are defined in: ```https://github.com/GeoNet/kube-infra/tree/main/infrastructure/gatekeeper/constraint-templates``` ## Composite Actions