Skip to content

Implement DDEV addon: Claude Code with firewall + SSH signing #1

@davidmondok

Description

@davidmondok

Overview

Problem: Need to run Claude Code with --dangerously-skip-permissions inside DDEV containers safely, as an alternative to the official devcontainer.

Solution: DDEV addon that installs Claude Code + iptables firewall (ported from official devcontainer) into the web container. Firewall restricts outbound to whitelisted domains only, preventing exfiltration. Includes SSH-based git signing and OAuth credential persistence.

Key decisions:

  • Firewall adapted from official devcontainer, with DDEV-specific allowances (Docker networking, inbound HTTP)
  • SSH signing instead of GPG/1Password
  • Extra domains configurable via extra-domains.txt (one per line)
  • OAuth login for API key, persisted in named Docker volume
  • ALL ALL sudoers for portability across DDEV users

Reference implementations:


Task Checklist

1. Repository Setup

  • Add MIT LICENSE
  • Create install.yaml — verify: valid YAML with all project_files listed
  • Create .gitignore

2. Dockerfile & Docker Compose

  • Create web-build/Dockerfile.claude-code — install iptables, ipset, iproute2, dnsutils, jq, claude-code, sudoers entry
  • Create docker-compose.claude-code.yaml — NET_ADMIN/NET_RAW caps, named volume for claude config persistence
  • Verify ${DDEV_USER} resolves in docker-compose context; if not, use fixed path + CLAUDE_CONFIG_DIR env var

3. Firewall Script

  • Create claude-code/init-firewall.sh adapted from official devcontainer
  • Preserve Docker DNS rules before flushing
  • Allow DNS (UDP 53), SSH (TCP 22), loopback
  • Allow all RFC1918 private ranges (Docker inter-container networking)
  • Allow inbound HTTP on ports 80, 443, 8025, 5173
  • Allow host network (default gateway /24)
  • Fetch GitHub IP ranges from /meta API, add to ipset (no aggregate, add individually)
  • Resolve and whitelist base domains: api.anthropic.com, statsig.anthropic.com, statsig.com, sentry.io, registry.npmjs.org, packagist.org, repo.packagist.org, getcomposer.org, api.wordpress.org, downloads.wordpress.org
  • Read and whitelist domains from extra-domains.txt if it exists
  • Set DROP policies last (prevent bricking on partial failure)
  • Verification: block example.com, allow api.anthropic.com
  • Make script executable: chmod +x

4. Hooks & Config

  • Create config.claude-code.yaml with post-start hooks
  • Hook: extract SSH public key from agent → ~/.ssh/signing-key.pub
  • Hook: configure git SSH signing (gpg.format=ssh, commit.gpgsign=true)
  • Hook: run firewall init script via sudo

5. Command Wrapper

  • Create commands/web/claude with ExecRaw: true for TTY passthrough
  • Bake in --dangerously-skip-permissions flag

6. Domain Customization

  • Create claude-code/extra-domains.example.txt with instructions
  • Create claude-code/.gitignore to ignore auth data

7. Documentation

  • Create README.md with: overview, installation, first run, per-project customization, verification commands, SSH signing setup (one-time GitHub key addition)

8. Testing

  • Install addon in a test DDEV project
  • ddev restart succeeds without errors
  • ddev exec curl --connect-timeout 3 https://example.com fails (blocked)
  • ddev exec curl --connect-timeout 3 https://api.anthropic.com succeeds
  • ddev claude --help works with TTY
  • WordPress site still accessible via browser
  • ddev exec git log --show-signature -1 shows SSH signature
  • OAuth credentials persist across ddev restart

Completion Signal

All checkboxes checked + addon installs cleanly + firewall verification passes + WordPress still works = DONE


Implementation Details

install.yaml

name: claude-code

project_files:
  - web-build/Dockerfile.claude-code
  - docker-compose.claude-code.yaml
  - config.claude-code.yaml
  - commands/web/claude
  - claude-code/init-firewall.sh
  - claude-code/extra-domains.example.txt
  - claude-code/.gitignore

web-build/Dockerfile.claude-code

#ddev-generated
RUN apt-get update && apt-get install -y --no-install-recommends \
  iptables ipset iproute2 dnsutils jq \
  && apt-get clean && rm -rf /var/lib/apt/lists/*
RUN npm install -g @anthropic-ai/claude-code@latest
RUN echo "ALL ALL=(root) NOPASSWD: /mnt/ddev_config/claude-code/init-firewall.sh" \
  > /etc/sudoers.d/claude-firewall && chmod 0440 /etc/sudoers.d/claude-firewall

docker-compose.claude-code.yaml

#ddev-generated
services:
  web:
    cap_add:
      - NET_ADMIN
      - NET_RAW
    volumes:
      - claude-config:/home/${DDEV_USER}/.claude
volumes:
  claude-config:
    name: "${COMPOSE_PROJECT_NAME}-claude-config"

Note: verify ${DDEV_USER} resolves. Fallback: fixed path + CLAUDE_CONFIG_DIR env var.

config.claude-code.yaml

#ddev-generated
hooks:
  post-start:
    - exec: mkdir -p ~/.ssh && ssh-add -L | head -1 > ~/.ssh/signing-key.pub 2>/dev/null || true
    - exec: |
        git config --global gpg.format ssh
        git config --global user.signingkey ~/.ssh/signing-key.pub
        git config --global commit.gpgsign true
    - exec: sudo /mnt/ddev_config/claude-code/init-firewall.sh

commands/web/claude

#\!/bin/bash
#ddev-generated
## Description: Run Claude Code with --dangerously-skip-permissions
## Usage: claude [flags] [args]
## Example: "ddev claude" or "ddev claude --help"
## ExecRaw: true

claude --dangerously-skip-permissions "$@"

claude-code/init-firewall.sh

Adapt from official script with these DDEV-specific changes:

  1. Allow RFC1918 private ranges — covers all Docker networking (db, router, ssh-agent, host) without needing to discover specific CIDRs
  2. Allow inbound HTTP on ports 80, 443, 8025 (Mailpit), 5173 (Vite) — web container must keep serving WordPress
  3. Read extra-domains.txtEXTRA_DOMAINS_FILE="/mnt/ddev_config/claude-code/extra-domains.txt", parse non-comment non-empty lines
  4. Skip aggregate — add GitHub CIDRs individually via jq
  5. Non-fatal warnings — DNS resolution failures warn but don't exit
  6. DROP policies last — prevents bricking container if script fails midway

claude-code/extra-domains.example.txt

# Add project-specific domains here, one per line.
# Copy this file to extra-domains.txt and add your domains.
# Example:
# satis.example.com
# my-api.example.com

claude-code/.gitignore

.claude/
.claude.json

Open Questions

  1. Multiple SSH keys in agent — ssh-add -L | head -1 takes the first. Match by fingerprint/comment instead?
  2. Pin @anthropic-ai/claude-code version or use @latest?
  3. Does ${DDEV_USER} resolve in docker-compose.yaml context?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions