diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0bcf871 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +DOMAIN=local.example.com +UPSTREAM_URL=http://host.docker.internal:3000 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..eb01982 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,69 @@ +name: Test + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + DOMAIN: sslproxy.stackpop.com + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Start mock upstream + run: | + docker run -d --name upstream -p 3000:80 nginx:alpine + sleep 2 + + - name: Create .env + run: | + echo "DOMAIN=${{ env.DOMAIN }}" > .env + echo "UPSTREAM_URL=http://host.docker.internal:3000" >> .env + + - name: Add test domain to hosts + run: echo "127.0.0.1 ${{ env.DOMAIN }}" | sudo tee -a /etc/hosts + + - name: Build images + run: docker compose build + + - name: Generate certificates + run: docker compose --profile setup run --rm mkcert + + - name: Verify certificates exist + run: | + test -f certs/${{ env.DOMAIN }}.pem + test -f certs/${{ env.DOMAIN }}.key.pem + test -f certs/${{ env.DOMAIN }}.rootCA.pem + + - name: Start proxy + run: docker compose up -d + + - name: Wait for Caddy to start + run: sleep 3 + + - name: Check Caddy is running + run: docker compose ps --status running --services | grep -q '^caddy$' + + - name: Test HTTP redirect + run: | + curl -s -o /dev/null -w "%{http_code}" http://${{ env.DOMAIN }}:8080 | grep -q "301\|308" + + - name: Test HTTPS proxies to upstream + run: | + curl -s --cacert certs/${{ env.DOMAIN }}.rootCA.pem https://${{ env.DOMAIN }}:8443 | grep -q "nginx" + + - name: Show logs on failure + if: failure() + run: docker compose logs + + - name: Stop proxy + if: always() + run: | + docker compose down + docker rm -f upstream || true diff --git a/.gitignore b/.gitignore index daa21fa..eb353ba 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ certs/ .env # AI agents -.claude/ \ No newline at end of file +.claude/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..03359dd --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,54 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +A Caddy-based reverse proxy for local development with automatic SSL certificate generation. + +## Architecture + +- **mkcert container**: Generates SSL certificates on first run, stores in `./certs/` +- **Caddy container**: Reverse proxy with HTTPS, depends on mkcert completing first +- **Bind mount**: Certs stored locally in `./certs/` for easy access + +## Key Commands + +```bash +# Generate certificates (first time only) +docker-compose --profile setup run --rm mkcert + +# Start the proxy +docker-compose up -d --build + +# View logs +docker-compose logs -f caddy + +# Stop the proxy +docker-compose down + +# Regenerate certificates +rm -rf certs/* && docker-compose --profile setup run --rm mkcert + +# Install CA on macOS +sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ./certs/${DOMAIN}.rootCA.pem +``` + +## Configuration + +Environment variables (set in `.env`): +- `DOMAIN` - Domain name for SSL cert (default: `localhost`) +- `UPSTREAM_URL` - URL for your local app (default: `http://host.docker.internal:3000`) + +## Files + +- **config/Caddyfile**: Proxy rules, TLS config, CSP header removal +- **scripts/mkcert/entrypoint.sh**: Script that generates certs if they don't exist +- **docker-compose.yml**: Service definitions with mkcert → caddy dependency +- **Dockerfile.mkcert**: Alpine image with mkcert for cert generation +- **Dockerfile.caddy**: Minimal Caddy image + +## Ports + +- `8080` → HTTP (redirects to HTTPS on 8443) +- `8443` → HTTPS (proxies to `${UPSTREAM_URL}`) diff --git a/Dockerfile.caddy b/Dockerfile.caddy new file mode 100644 index 0000000..4205ab1 --- /dev/null +++ b/Dockerfile.caddy @@ -0,0 +1,5 @@ +FROM caddy:2-alpine + +EXPOSE 80 443 + +CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] diff --git a/Dockerfile.mkcert b/Dockerfile.mkcert new file mode 100644 index 0000000..36a6c21 --- /dev/null +++ b/Dockerfile.mkcert @@ -0,0 +1,18 @@ +FROM alpine:3.19 + +ARG MKCERT_VERSION=1.4.4 + +RUN apk add --no-cache ca-certificates nss-tools curl \ + && ARCH=$(uname -m) \ + && case "$ARCH" in \ + aarch64) MKCERT_ARCH="linux-arm64" ;; \ + x86_64) MKCERT_ARCH="linux-amd64" ;; \ + *) echo "Unsupported architecture: $ARCH" && exit 1 ;; \ + esac \ + && curl -L "https://github.com/FiloSottile/mkcert/releases/download/v${MKCERT_VERSION}/mkcert-v${MKCERT_VERSION}-${MKCERT_ARCH}" -o /usr/local/bin/mkcert \ + && chmod +x /usr/local/bin/mkcert + +COPY scripts/mkcert/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..2ecaef1 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# SSL Proxy + +A Dockerized Caddy reverse proxy with automatic SSL certificate generation for local development. + +## Features + +- Automatic SSL certificate generation via mkcert +- Strips Content-Security-Policy headers +- HTTP to HTTPS redirect +- Configurable domain and upstream URL + +## Quick Start + +1. Configure your domain in `.env`: + + ``` + DOMAIN=local.example.com + UPSTREAM_URL=http://host.docker.internal:3000 + ``` + + `UPSTREAM_URL` must include the scheme and port. + +2. Add to `/etc/hosts`: + + ``` + 127.0.0.1 local.example.com + ``` + +3. Generate certificates (first time only): + + ```bash + docker-compose --profile setup run --rm mkcert + ``` + +4. Install the CA certificate (one-time): + + ```bash + sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ./certs/local.example.com.rootCA.pem + ``` + +5. Start the proxy: + + ```bash + docker-compose up -d + ``` + +6. Visit: `https://local.example.com:8443` + +Note (Linux): Requires Docker Engine 20.10+ for `host-gateway` support. + +## Configuration + +| Variable | Default | Description | +| --------------- | ----------- | ---------------------- | +| `DOMAIN` | `localhost` | Domain for SSL cert | +| `UPSTREAM_URL` | `http://host.docker.internal:3000` | URL for your local app | + +## Ports + +- `8080` - HTTP (redirects to HTTPS) +- `8443` - HTTPS + +## Layout + +``` +├── config/Caddyfile # Caddy configuration +├── scripts/mkcert/entrypoint.sh # Cert generation script +├── docker-compose.yml # Service definitions +├── Dockerfile.caddy # Caddy image +├── Dockerfile.mkcert # Certificate generator +└── .env # Your configuration +``` diff --git a/config/Caddyfile b/config/Caddyfile new file mode 100644 index 0000000..e551ffa --- /dev/null +++ b/config/Caddyfile @@ -0,0 +1,17 @@ +:80 { + redir https://{$DOMAIN:localhost}:8443{uri} permanent +} + +{$DOMAIN:localhost} { + tls /etc/caddy/certs/{$DOMAIN:localhost}.pem /etc/caddy/certs/{$DOMAIN:localhost}.key.pem + + reverse_proxy {$UPSTREAM_URL:http://host.docker.internal:3000} { + header_down -Content-Security-Policy + header_down -Content-Security-Policy-Report-Only + } + + log { + output stdout + format console + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..340f49a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +services: + mkcert: + build: + context: . + dockerfile: Dockerfile.mkcert + container_name: mkcert + profiles: + - setup + environment: + - DOMAIN=${DOMAIN:-localhost} + volumes: + - ./certs:/certs + + caddy: + build: + context: . + dockerfile: Dockerfile.caddy + container_name: ssl-proxy + ports: + - "8080:80" + - "8443:443" + environment: + - DOMAIN=${DOMAIN:-localhost} + - UPSTREAM_URL=${UPSTREAM_URL:-http://host.docker.internal:3000} + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - ./config/Caddyfile:/etc/caddy/Caddyfile:ro + - ./certs:/etc/caddy/certs:ro + - caddy_data:/data + - caddy_config:/config + restart: unless-stopped + +volumes: + caddy_data: + caddy_config: diff --git a/scripts/mkcert/entrypoint.sh b/scripts/mkcert/entrypoint.sh new file mode 100644 index 0000000..3fa4f7f --- /dev/null +++ b/scripts/mkcert/entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/sh +set -e + +DOMAIN="${DOMAIN:-localhost}" +CERT_FILE="/certs/${DOMAIN}.pem" +KEY_FILE="/certs/${DOMAIN}.key.pem" +CA_FILE="/certs/${DOMAIN}.rootCA.pem" + +if [ ! -f "$CERT_FILE" ]; then + echo "Generating SSL certificate for ${DOMAIN}..." + mkcert -install + mkcert -cert-file "$CERT_FILE" \ + -key-file "$KEY_FILE" \ + "$DOMAIN" + cp "$(mkcert -CAROOT)/rootCA.pem" "$CA_FILE" + echo "=== Certificate generated ===" +else + echo "Certificate already exists for ${DOMAIN}, skipping generation." +fi + +echo "Install CA on macOS:" +echo " sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ./certs/${DOMAIN}.rootCA.pem"