services: # HTTPS-Everywhere Phase 3 — self-signed TLS bootstrap (init container). # Generates a CN=certctl-server ECDSA-P256 (SHA-256 signature) cert with # the SAN list locked by milestone §3.6 on first boot; subsequent boots # see the cert already present in the `certs` named volume and no-op out. # Server + agent mount the volume read-only. Destroy via `docker compose # down -v` to force regeneration. This bootstrap is for docker-compose # demos and local dev only; Helm operators supply a Secret / cert-manager # Certificate per docs/tls.md. # # Rationale for ECDSA-P256 (was ed25519 pre-v2.0.48): Apple's TLS stack # — Safari Network Framework and the macOS-bundled LibreSSL 3.3.6 # /usr/bin/curl — does not advertise ed25519 in the ClientHello # signature_algorithms extension for server certs, yielding "tls: peer # doesn't support any of the certificate's signature algorithms" at # handshake. ECDSA-P256 with SHA-256 is universally supported. See # docs/tls.md Pattern 1. certctl-tls-init: image: alpine/openssl:latest container_name: certctl-tls-init restart: "no" entrypoint: /bin/sh command: - -c - | set -eu CERT=/etc/certctl/tls/server.crt KEY=/etc/certctl/tls/server.key CA=/etc/certctl/tls/ca.crt if [ -f "$$CERT" ] && [ -f "$$KEY" ] && [ -f "$$CA" ]; then echo "TLS cert already present at $$CERT — skipping generation" else mkdir -p /etc/certctl/tls openssl req -x509 -newkey ec \ -pkeyopt ec_paramgen_curve:P-256 \ -nodes \ -keyout "$$KEY" \ -out "$$CERT" \ -days 3650 \ -subj "/CN=certctl-server" \ -addext "subjectAltName=DNS:certctl-server,DNS:localhost,IP:127.0.0.1,IP:::1" cp "$$CERT" "$$CA" echo "Generated self-signed TLS cert for certctl-server (ECDSA-P256/SHA-256, 3650d, CN=certctl-server)" fi # certctl binary runs as UID 1000 inside the server container per # Dockerfile:64-65; the cert + key must be readable by that UID. chown 1000:1000 "$$CERT" "$$KEY" "$$CA" chmod 0644 "$$CERT" "$$CA" chmod 0600 "$$KEY" volumes: - certs:/etc/certctl/tls networks: - certctl-network # PostgreSQL database postgres: image: postgres:16-alpine container_name: certctl-postgres environment: POSTGRES_DB: certctl POSTGRES_USER: certctl POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-certctl} ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data - ../migrations/000001_initial_schema.up.sql:/docker-entrypoint-initdb.d/001_schema.sql - ../migrations/000002_agent_metadata.up.sql:/docker-entrypoint-initdb.d/002_agent_metadata.sql - ../migrations/000003_certificate_profiles.up.sql:/docker-entrypoint-initdb.d/003_certificate_profiles.sql - ../migrations/000004_agent_groups.up.sql:/docker-entrypoint-initdb.d/004_agent_groups.sql - ../migrations/000005_revocation.up.sql:/docker-entrypoint-initdb.d/005_revocation.sql - ../migrations/000006_discovery.up.sql:/docker-entrypoint-initdb.d/006_discovery.sql - ../migrations/000007_network_discovery.up.sql:/docker-entrypoint-initdb.d/007_network_discovery.sql - ../migrations/000008_verification.up.sql:/docker-entrypoint-initdb.d/008_verification.sql - ../migrations/000009_issuer_config.up.sql:/docker-entrypoint-initdb.d/009_issuer_config.sql - ../migrations/000010_target_config.up.sql:/docker-entrypoint-initdb.d/010_target_config.sql - ../migrations/seed.sql:/docker-entrypoint-initdb.d/020_seed.sql networks: - certctl-network healthcheck: test: ["CMD-SHELL", "pg_isready -U certctl -d certctl"] interval: 5s timeout: 5s retries: 5 restart: unless-stopped # Certctl Server (API + scheduler) certctl-server: build: context: .. dockerfile: Dockerfile # Proxy propagation (M-4, Issue #9) — forwards host shell's proxy env # vars into the Docker build so the Node frontend stage and Go module # download can reach the public registries behind corporate proxies. # Defaults to empty; omit the variables from the host environment for # un-proxied builds and the behaviour is byte-identical to the pre-fix # tree. args: HTTP_PROXY: ${HTTP_PROXY:-} HTTPS_PROXY: ${HTTPS_PROXY:-} NO_PROXY: ${NO_PROXY:-} container_name: certctl-server depends_on: postgres: condition: service_healthy certctl-tls-init: condition: service_completed_successfully environment: CERTCTL_DATABASE_URL: postgres://certctl:${POSTGRES_PASSWORD:-certctl}@postgres:5432/certctl?sslmode=disable CERTCTL_SERVER_HOST: 0.0.0.0 CERTCTL_SERVER_PORT: 8443 CERTCTL_SERVER_TLS_CERT_PATH: /etc/certctl/tls/server.crt CERTCTL_SERVER_TLS_KEY_PATH: /etc/certctl/tls/server.key CERTCTL_LOG_LEVEL: info CERTCTL_AUTH_TYPE: none CERTCTL_KEYGEN_MODE: server # Demo uses server-side keygen; production should use "agent" CERTCTL_NETWORK_SCAN_ENABLED: "true" # Enable network scan GUI with seeded demo targets CERTCTL_CONFIG_ENCRYPTION_KEY: ${CERTCTL_CONFIG_ENCRYPTION_KEY:-change-me-32-char-encryption-key} # AES-256-GCM for dynamic issuer/target config ports: - "8443:8443" volumes: - certs:/etc/certctl/tls:ro networks: - certctl-network healthcheck: test: ["CMD", "curl", "--cacert", "/etc/certctl/tls/ca.crt", "-f", "https://localhost:8443/health"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped logging: driver: "json-file" options: max-size: "10m" max-file: "3" deploy: resources: limits: cpus: '1.0' memory: 512M # Certctl Agent certctl-agent: build: context: .. dockerfile: Dockerfile.agent # Proxy propagation (M-4, Issue #9) — forwards host shell's proxy env # vars into the Docker build so the Go module download stage can reach # the public Go module proxy behind corporate proxies. Defaults to # empty; omit the variables from the host environment for un-proxied # builds and the behaviour is byte-identical to the pre-fix tree. args: HTTP_PROXY: ${HTTP_PROXY:-} HTTPS_PROXY: ${HTTPS_PROXY:-} NO_PROXY: ${NO_PROXY:-} container_name: certctl-agent depends_on: certctl-server: condition: service_healthy environment: CERTCTL_SERVER_URL: https://certctl-server:8443 CERTCTL_SERVER_CA_BUNDLE_PATH: /etc/certctl/tls/ca.crt CERTCTL_API_KEY: ${CERTCTL_API_KEY:-change-me-in-production} CERTCTL_AGENT_NAME: docker-agent CERTCTL_LOG_LEVEL: info CERTCTL_DISCOVERY_DIRS: /var/lib/certctl/keys # Agent scans this directory for existing certificates volumes: - agent_keys:/var/lib/certctl/keys - certs:/etc/certctl/tls:ro networks: - certctl-network healthcheck: test: ["CMD-SHELL", "pgrep -f certctl-agent || exit 1"] interval: 30s timeout: 5s retries: 3 restart: unless-stopped logging: driver: "json-file" options: max-size: "10m" max-file: "3" deploy: resources: limits: cpus: '0.5' memory: 256M networks: certctl-network: driver: bridge volumes: postgres_data: driver: local agent_keys: driver: local certs: driver: local