From df7386c60d91c8bc1cfb09237047fac61971baf8 Mon Sep 17 00:00:00 2001 From: continuist Date: Sun, 13 Jul 2025 11:35:11 -0400 Subject: [PATCH] Configure for self-signed TLS --- CI_CD_PIPELINE_SETUP_GUIDE.md | 213 ++++++++++++++++++++++----- registry/Caddyfile | 5 +- registry/config.yml | 2 +- registry/docker-compose.registry.yml | 1 + 4 files changed, 183 insertions(+), 38 deletions(-) diff --git a/CI_CD_PIPELINE_SETUP_GUIDE.md b/CI_CD_PIPELINE_SETUP_GUIDE.md index 073a30d..a5f2aba 100644 --- a/CI_CD_PIPELINE_SETUP_GUIDE.md +++ b/CI_CD_PIPELINE_SETUP_GUIDE.md @@ -714,9 +714,163 @@ sudo sed -i "s/YOUR_CI_CD_IP/YOUR_ACTUAL_IP_ADDRESS/g" /opt/registry/config.yml sudo chown CI_SERVICE_USER:CI_SERVICE_USER /opt/registry/config.yml ``` +#### 5.4 Generate TLS Certificate +**Choose one of the following options based on whether you have a domain name:** -#### 5.5 Start Docker Registry with Docker Compose +### **Option A: Self-Signed Certificate (No Domain Required)** + +This option works with IP addresses and is suitable for development/testing environments. + +```bash +# Create certificate directory +sudo mkdir -p /opt/registry/certs +sudo chown CI_SERVICE_USER:CI_SERVICE_USER /opt/registry/certs +cd /opt/registry/certs + +# Generate private key (4096-bit RSA) +sudo -u CI_SERVICE_USER openssl genrsa -out registry.key 4096 + +# Generate certificate signing request +sudo -u CI_SERVICE_USER openssl req -new -key registry.key \ + -out registry.csr \ + -subj "/C=US/ST=State/L=City/O=Organization/OU=IT/CN=YOUR_ACTUAL_IP_ADDRESS" + +# Create certificate configuration for Subject Alternative Names (SAN) +sudo -u CI_SERVICE_USER tee registry.conf > /dev/null << EOF +[req] +distinguished_name = req_distinguished_name +req_extensions = v3_req +prompt = no + +[req_distinguished_name] +C = US +ST = State +L = City +O = Organization +OU = IT +CN = YOUR_ACTUAL_IP_ADDRESS + +[v3_req] +keyUsage = keyEncipherment, dataEncipherment +extendedKeyUsage = serverAuth +subjectAltName = @alt_names + +[alt_names] +DNS.1 = YOUR_ACTUAL_IP_ADDRESS +DNS.2 = localhost +IP.1 = YOUR_ACTUAL_IP_ADDRESS +IP.2 = 127.0.0.1 +EOF + +# Generate self-signed certificate (valid for 365 days) +sudo -u CI_SERVICE_USER openssl x509 -req -in registry.csr \ + -signkey registry.key \ + -out registry.crt \ + -days 365 \ + -extensions v3_req \ + -extfile registry.conf + +# Set proper permissions +sudo chmod 600 registry.key +sudo chmod 644 registry.crt + +# Verify certificate +sudo -u CI_SERVICE_USER openssl x509 -in registry.crt -text -noout + +echo "Self-signed TLS certificate generated successfully!" +echo "Certificate: /opt/registry/certs/registry.crt" +echo "Private key: /opt/registry/certs/registry.key" +echo "Note: This certificate will need to be renewed manually in 365 days" +``` + +### **Option B: Let's Encrypt Certificate (Domain Required)** + +If you have a domain name pointing to your server, use this option for production-ready certificates. + +```bash +# Install Certbot and Nginx plugin +sudo apt update +sudo apt install -y certbot python3-certbot-nginx + +# Generate certificate using standalone mode +sudo certbot certonly --standalone \ + --email your-email@example.com \ + --agree-tos \ + --no-eff-email \ + -d YOUR_DOMAIN_NAME + +# Verify certificate generation +sudo certbot certificates + +# Create certificate directory for Caddy +sudo mkdir -p /opt/registry/certs +sudo chown CI_SERVICE_USER:CI_SERVICE_USER /opt/registry/certs + +# Copy Let's Encrypt certificates to registry directory +sudo cp /etc/letsencrypt/live/YOUR_DOMAIN_NAME/fullchain.pem /opt/registry/certs/registry.crt +sudo cp /etc/letsencrypt/live/YOUR_DOMAIN_NAME/privkey.pem /opt/registry/certs/registry.key + +# Set proper permissions +sudo chown CI_SERVICE_USER:CI_SERVICE_USER /opt/registry/certs/registry.crt +sudo chown CI_SERVICE_USER:CI_SERVICE_USER /opt/registry/certs/registry.key +sudo chmod 644 /opt/registry/certs/registry.crt +sudo chmod 600 /opt/registry/certs/registry.key + +echo "Let's Encrypt certificate generated successfully!" +echo "Certificate: /opt/registry/certs/registry.crt" +echo "Private key: /opt/registry/certs/registry.key" +echo "Certificate will auto-renew every 60 days" +``` + +**Note**: +- For **Option A**: Replace `YOUR_ACTUAL_IP_ADDRESS` with your server's IP address +- For **Option B**: Replace `YOUR_DOMAIN_NAME` with your domain name and `your-email@example.com` with your email address + +#### 5.5 Install Certificate into Docker Trust Store + +**Important**: This step adds the Let's Encrypt certificate to Docker's trust store. Since Let's Encrypt is a trusted CA, Docker will automatically trust this certificate. + +```bash +# Create Docker certificates directory for your domain +sudo mkdir -p /etc/docker/certs.d/YOUR_DOMAIN_NAME + +# Copy certificate to Docker trust store +sudo cp /opt/registry/certs/registry.crt /etc/docker/certs.d/YOUR_DOMAIN_NAME/ca.crt + +# Restart Docker daemon to pick up the new certificate +sudo systemctl restart docker + +# Verify certificate installation +if [ -f "/etc/docker/certs.d/YOUR_DOMAIN_NAME/ca.crt" ]; then + echo "✅ Let's Encrypt certificate installed in Docker trust store" +else + echo "❌ Failed to install certificate in Docker trust store" + exit 1 +fi + +echo "Certificate installation completed successfully!" +echo "Docker can now connect to the registry securely using your domain name" +``` + +#### 5.6 Set Up Automatic Certificate Renewal + +**Important**: Let's Encrypt certificates expire after 90 days, so we need to set up automatic renewal. + +```bash +# Test automatic renewal +sudo certbot renew --dry-run + +# Set up automatic renewal cron job +sudo crontab -e +# Add this line to renew certificates twice daily (Let's Encrypt allows renewal 30 days before expiry): +# 0 12,18 * * * /usr/bin/certbot renew --quiet --post-hook "cp /etc/letsencrypt/live/YOUR_DOMAIN_NAME/fullchain.pem /opt/registry/certs/registry.crt && cp /etc/letsencrypt/live/YOUR_DOMAIN_NAME/privkey.pem /opt/registry/certs/registry.key && chown CI_SERVICE_USER:CI_SERVICE_USER /opt/registry/certs/registry.* && chmod 644 /opt/registry/certs/registry.crt && chmod 600 /opt/registry/certs/registry.key && systemctl restart docker-registry.service" + +echo "Automatic certificate renewal configured!" +echo "Certificates will be renewed automatically and the registry service will be restarted" +``` + +#### 5.7 Start Docker Registry with Docker Compose ```bash # Switch to CI_SERVICE_USER @@ -725,6 +879,13 @@ sudo su - CI_SERVICE_USER # Navigate to registry directory cd /opt/registry +# Copy updated Docker Compose and Caddy configuration with certificate support +sudo cp /opt/APP_NAME/registry/docker-compose.registry.yml docker-compose.yml +sudo cp /opt/APP_NAME/registry/Caddyfile Caddyfile + +# Update Caddyfile with your actual IP address +sudo sed -i "s/YOUR_CI_CD_IP/YOUR_ACTUAL_IP_ADDRESS/g" Caddyfile + # Start the Docker Registry and Caddy services docker compose up -d @@ -735,7 +896,7 @@ docker compose ps exit ``` -#### 5.6 Create Systemd Service for Docker Compose +#### 5.8 Create Systemd Service for Docker Compose ```bash # Create systemd service file for Docker Registry with Docker Compose @@ -768,55 +929,33 @@ sudo systemctl start docker-registry.service sudo journalctl -u docker-registry.service -f ``` -#### 5.7 Configure Registry Access - -The Docker Registry is now configured with the following access model: - -**Authentication Model:** -- **Pulls**: Unauthenticated (public read access) - `/v2/*/blobs/*`, `/v2/*/manifests/*`, `/v2/_catalog`, `/v2/*/tags/list` -- **Pushes**: Require authentication with `registry-user` credentials - `/v2/*/blobs/uploads/*`, `/v2/*/manifests/*` (PUT/POST/PATCH/DELETE) - -**Registry Credentials:** -- **Username**: `registry-user` -- **Password**: The password you set in the environment file (default: `your-secure-registry-password`) - -**Registry URL**: `https://YOUR_CI_CD_IP` - -**Security Features:** -- **URL-based access control**: Different paths require different authentication levels -- **Method-based restrictions**: Push operations (PUT/POST/PATCH/DELETE) require authentication -- **Path validation**: Prevents method spoofing by validating both URL patterns and HTTP methods -- **Security headers**: X-Content-Type-Options, X-Frame-Options for additional protection - -**Note**: The authentication is handled by Caddy using the environment variables in the `.env` file. The Docker Registry itself runs without authentication, but Caddy enforces authentication for push operations based on URL patterns and HTTP methods. - -#### 5.8 Test Registry Setup +#### 5.9 Test Registry Setup ```bash # Switch to CI_SERVICE_USER for testing (CI_SERVICE_USER runs CI pipeline and Docker operations) sudo su - CI_SERVICE_USER -# Test Docker login and push -echo "your-secure-registry-password" | docker login YOUR_CI_CD_IP -u registry-user --password-stdin +# Test Docker login and push (now using Let's Encrypt certificate with domain) +echo "your-secure-registry-password" | docker login YOUR_DOMAIN_NAME -u registry-user --password-stdin # Create and push test image echo "FROM alpine:latest" > /tmp/test.Dockerfile -docker build -f /tmp/test.Dockerfile -t YOUR_CI_CD_IP/APP_NAME/test:latest /tmp -docker push YOUR_CI_CD_IP/APP_NAME/test:latest +docker build -f /tmp/test.Dockerfile -t YOUR_DOMAIN_NAME/APP_NAME/test:latest /tmp +docker push YOUR_DOMAIN_NAME/APP_NAME/test:latest # Test public pull (no authentication) -docker logout YOUR_CI_CD_IP -docker pull YOUR_CI_CD_IP/APP_NAME/test:latest +docker logout YOUR_DOMAIN_NAME +docker pull YOUR_DOMAIN_NAME/APP_NAME/test:latest # Test that unauthorized push is blocked echo "FROM alpine:latest" > /tmp/unauthorized.Dockerfile -docker build -f /tmp/unauthorized.Dockerfile -t YOUR_CI_CD_IP/APP_NAME/unauthorized:latest /tmp -docker push YOUR_CI_CD_IP/APP_NAME/unauthorized:latest +docker build -f /tmp/unauthorized.Dockerfile -t YOUR_DOMAIN_NAME/APP_NAME/unauthorized:latest /tmp +docker push YOUR_DOMAIN_NAME/APP_NAME/unauthorized:latest # Expected: This should fail with authentication error # Clean up -docker rmi YOUR_CI_CD_IP/APP_NAME/test:latest -docker rmi YOUR_CI_CD_IP/APP_NAME/unauthorized:latest +docker rmi YOUR_DOMAIN_NAME/APP_NAME/test:latest +docker rmi YOUR_DOMAIN_NAME/APP_NAME/unauthorized:latest exit ``` @@ -824,7 +963,9 @@ exit - ✅ Push requires authentication with `registry-user` credentials - ✅ Pull works without authentication (public read access) - ✅ Unauthorized push is blocked -- ✅ Registry accessible at `https://YOUR_CI_CD_IP` +- ✅ Registry accessible at `https://YOUR_DOMAIN_NAME` with valid Let's Encrypt certificate +- ✅ No insecure registry configuration needed +- ✅ Certificate automatically renews every 60 days ### Step 6: Install Forgejo Actions Runner diff --git a/registry/Caddyfile b/registry/Caddyfile index fcae3d0..2b91cf2 100644 --- a/registry/Caddyfile +++ b/registry/Caddyfile @@ -4,7 +4,10 @@ } } -YOUR_CI_CD_IP { +YOUR_DOMAIN_NAME { + # Use generated TLS certificate + tls /etc/caddy/certs/registry.crt /etc/caddy/certs/registry.key + # Security headers header { X-Content-Type-Options nosniff diff --git a/registry/config.yml b/registry/config.yml index bc28ab4..f4036d2 100644 --- a/registry/config.yml +++ b/registry/config.yml @@ -16,4 +16,4 @@ middleware: storage: - name: Redirect options: - baseurl: https://YOUR_CI_CD_IP \ No newline at end of file + baseurl: https://YOUR_DOMAIN_NAME \ No newline at end of file diff --git a/registry/docker-compose.registry.yml b/registry/docker-compose.registry.yml index a06a0b9..05145ca 100644 --- a/registry/docker-compose.registry.yml +++ b/registry/docker-compose.registry.yml @@ -23,6 +23,7 @@ services: - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile + - ./certs:/etc/caddy/certs - caddy_data:/data - caddy_config:/config env_file: