diff --git a/CI_CD_PIPELINE_SETUP_GUIDE.md b/CI_CD_PIPELINE_SETUP_GUIDE.md index 03deab4..ea2c45c 100644 --- a/CI_CD_PIPELINE_SETUP_GUIDE.md +++ b/CI_CD_PIPELINE_SETUP_GUIDE.md @@ -710,6 +710,9 @@ sudo cp /opt/APP_NAME/registry/config.yml /opt/registry/config.yml # Update the baseurl with your actual IP address sudo sed -i "s/YOUR_CI_CD_IP/YOUR_ACTUAL_IP_ADDRESS/g" /opt/registry/config.yml +# Note: For Option B (domain-based setup), you'll need to update this again later +# with: sudo sed -i "s/YOUR_ACTUAL_IP_ADDRESS/YOUR_DOMAIN_NAME/g" /opt/registry/config.yml + # Set proper permissions sudo chown CI_SERVICE_USER:CI_SERVICE_USER /opt/registry/config.yml ``` @@ -725,53 +728,49 @@ sudo chown CI_SERVICE_USER:CI_SERVICE_USER /opt/registry/config.yml **Perform all of these steps if you do NOT have a domain name:** ```bash -# 1. Generate self-signed certificate +# 1. Generate self-signed certificate with proper CA chain sudo mkdir -p /opt/registry/certs sudo chown CI_SERVICE_USER:CI_SERVICE_USER /opt/registry/certs cd /opt/registry/certs + +# Generate CA private key +sudo -u CI_SERVICE_USER openssl genrsa -out ca.key 4096 + +# Generate CA certificate +sudo -u CI_SERVICE_USER openssl req -new -x509 -key ca.key \ + -out ca.crt \ + -days 365 \ + -subj "/C=US/ST=State/L=City/O=Organization/OU=IT/CN=Registry-CA" + +# Generate server private key sudo -u CI_SERVICE_USER openssl genrsa -out registry.key 4096 + +# Generate server 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" -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 +# Copy and customize the OpenSSL configuration file +sudo cp /opt/APP_NAME/registry/openssl.conf /opt/registry/certs/ +sudo sed -i "s/YOUR_ACTUAL_IP_ADDRESS/YOUR_ACTUAL_IP_ADDRESS/g" /opt/registry/certs/openssl.conf -[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 +# Sign server certificate with CA sudo -u CI_SERVICE_USER openssl x509 -req -in registry.csr \ - -signkey registry.key \ + -CA ca.crt -CAkey ca.key -CAcreateserial \ -out registry.crt \ -days 365 \ -extensions v3_req \ - -extfile registry.conf -sudo chmod 600 registry.key -sudo chmod 644 registry.crt + -extfile /opt/registry/certs/openssl.conf + +# Set proper permissions +sudo chmod 600 ca.key registry.key +sudo chmod 644 ca.crt registry.crt sudo -u CI_SERVICE_USER openssl x509 -in registry.crt -text -noout -# 2. Install certificate into Docker trust store +# 2. Install CA certificate into Docker trust store sudo mkdir -p /etc/docker/certs.d/registry -sudo cp /opt/registry/certs/registry.crt /etc/docker/certs.d/registry/ca.crt -sudo cp /opt/registry/certs/registry.crt /usr/local/share/ca-certificates/registry-ca.crt +sudo cp /opt/registry/certs/ca.crt /etc/docker/certs.d/registry/ca.crt +sudo cp /opt/registry/certs/ca.crt /usr/local/share/ca-certificates/registry-ca.crt sudo update-ca-certificates sudo systemctl restart docker ``` @@ -921,6 +920,38 @@ sudo journalctl -u docker-registry.service -f #### 5.9 Test Registry Setup +**For Option A (Self-signed certificates):** + +```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 using IP address with self-signed certificate +echo "your-secure-registry-password" | docker login YOUR_ACTUAL_IP_ADDRESS -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_ACTUAL_IP_ADDRESS/APP_NAME/test:latest /tmp +docker push YOUR_ACTUAL_IP_ADDRESS/APP_NAME/test:latest + +# Test public pull (no authentication) +docker logout YOUR_ACTUAL_IP_ADDRESS +docker pull YOUR_ACTUAL_IP_ADDRESS/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_ACTUAL_IP_ADDRESS/APP_NAME/unauthorized:latest /tmp +docker push YOUR_ACTUAL_IP_ADDRESS/APP_NAME/unauthorized:latest +# Expected: This should fail with authentication error + +# Clean up +docker rmi YOUR_ACTUAL_IP_ADDRESS/APP_NAME/test:latest +docker rmi YOUR_ACTUAL_IP_ADDRESS/APP_NAME/unauthorized:latest +exit +``` + +**For Option B (Let's Encrypt certificates):** + ```bash # Switch to CI_SERVICE_USER for testing (CI_SERVICE_USER runs CI pipeline and Docker operations) sudo su - CI_SERVICE_USER @@ -949,13 +980,74 @@ docker rmi YOUR_DOMAIN_NAME/APP_NAME/unauthorized:latest exit ``` +**Important**: For Option B, you'll also need to update the registry config file to use your domain: + +```bash +# Update registry config for domain-based setup +sudo sed -i "s/YOUR_ACTUAL_IP_ADDRESS/YOUR_DOMAIN_NAME/g" /opt/registry/config.yml +``` + **Expected behavior**: - ✅ Push requires authentication with `registry-user` credentials - ✅ Pull works without authentication (public read access) - ✅ Unauthorized push is blocked -- ✅ Registry accessible at `https://YOUR_DOMAIN_NAME` with valid Let's Encrypt certificate -- ✅ No insecure registry configuration needed -- ✅ Certificate automatically renews every 60 days +- ✅ Registry accessible at `https://YOUR_ACTUAL_IP_ADDRESS` with self-signed certificate (Option A) +- ✅ Registry accessible at `https://YOUR_DOMAIN_NAME` with valid Let's Encrypt certificate (Option B) +- ✅ Certificate automatically renews every 60 days (Option B only) +- ✅ Proper certificate chain management for both options + +**Troubleshooting TLS Errors (Option A only):** + +If you get a TLS error like `remote error: tls: internal error` when using self-signed certificates, you need to configure Docker to trust the certificate. Since Caddy automatically generates its own certificates, we need to configure Docker to accept the insecure registry: + +```bash +# Verify the certificate was installed correctly +ls -la /etc/docker/certs.d/registry/ + +# Configure Docker to accept the insecure registry (Caddy-generated certificates) +sudo tee /etc/docker/daemon.json << EOF +{ + "insecure-registries": ["YOUR_ACTUAL_IP_ADDRESS:443"] +} +EOF +sudo systemctl restart docker + +# Test the certificate +openssl s_client -connect YOUR_ACTUAL_IP_ADDRESS:443 -servername YOUR_ACTUAL_IP_ADDRESS < /dev/null + +# Test Docker login again +echo "your-secure-registry-password" | docker login YOUR_ACTUAL_IP_ADDRESS -u registry-user --password-stdin +``` + +**Configure Caddy to Use Our Certificates** + +Since we're creating our own certificate chain, we need to configure Caddy to use our certificates instead of generating its own: + +```bash +# Update Caddyfile to use our certificates +sudo tee /opt/registry/Caddyfile << EOF +YOUR_ACTUAL_IP_ADDRESS { + tls /opt/registry/certs/registry.crt /opt/registry/certs/registry.key + + reverse_proxy registry:5000 { + header_up Host {host} + header_up X-Real-IP {remote} + header_up X-Forwarded-For {remote} + header_up X-Forwarded-Proto {scheme} + } +} +EOF + +# Restart the registry services +cd /opt/registry +docker compose down +docker compose up -d + +# Test Docker login +echo "your-secure-registry-password" | docker login YOUR_ACTUAL_IP_ADDRESS -u registry-user --password-stdin +``` + +**Note**: This approach uses our own certificate chain, which Docker already trusts, eliminating the need to extract or trust Caddy's certificates. ### Step 6: Install Forgejo Actions Runner diff --git a/registry/Caddyfile b/registry/Caddyfile index 2b91cf2..f6bde40 100644 --- a/registry/Caddyfile +++ b/registry/Caddyfile @@ -4,14 +4,18 @@ } } -YOUR_DOMAIN_NAME { - # Use generated TLS certificate - tls /etc/caddy/certs/registry.crt /etc/caddy/certs/registry.key +# Option A: Self-signed certificates (IP address) +YOUR_ACTUAL_IP_ADDRESS { + # Use our generated TLS certificate + tls /opt/registry/certs/registry.crt /opt/registry/certs/registry.key # Security headers header { X-Content-Type-Options nosniff X-Frame-Options DENY + X-XSS-Protection "1; mode=block" + Referrer-Policy "strict-origin-when-cross-origin" + Content-Security-Policy "default-src 'self'; frame-ancestors 'none'" } # Handle registry operations based on URL patterns @@ -37,6 +41,7 @@ YOUR_DOMAIN_NAME { header_up X-Forwarded-For {remote_host} header_up X-Forwarded-Proto {scheme} header_up X-Forwarded-Host {host} + header_up Host {host} } } @@ -46,6 +51,7 @@ YOUR_DOMAIN_NAME { header_up X-Forwarded-For {remote_host} header_up X-Forwarded-Proto {scheme} header_up X-Forwarded-Host {host} + header_up Host {host} } } @@ -64,3 +70,70 @@ YOUR_DOMAIN_NAME { # Compression encode zstd gzip } + +# Option B: Let's Encrypt certificates (Domain name) +# Uncomment and customize for domain-based setup +# YOUR_DOMAIN_NAME { +# # Let's Encrypt handles TLS automatically +# +# # Security headers +# header { +# X-Content-Type-Options nosniff +# X-Frame-Options DENY +# X-XSS-Protection "1; mode=block" +# Referrer-Policy "strict-origin-when-cross-origin" +# Content-Security-Policy "default-src 'self'; frame-ancestors 'none'" +# } +# +# # Handle registry operations based on URL patterns +# @push_operations { +# path /v2/*/blobs/uploads/* +# path /v2/*/manifests/* +# method PUT POST PATCH DELETE +# } +# +# @pull_operations { +# path /v2/*/blobs/* +# path /v2/*/manifests/* +# path /v2/_catalog +# path /v2/*/tags/list +# method GET HEAD OPTIONS +# } +# +# # Require authentication for push operations +# handle @push_operations { +# import registry_auth +# reverse_proxy registry:5000 { +# header_up Authorization {http.request.header.Authorization} +# header_up X-Forwarded-For {remote_host} +# header_up X-Forwarded-Proto {scheme} +# header_up X-Forwarded-Host {host} +# header_up Host {host} +# } +# } +# +# # Allow unauthenticated pull operations +# handle @pull_operations { +# reverse_proxy registry:5000 { +# header_up X-Forwarded-For {remote_host} +# header_up X-Forwarded-Proto {scheme} +# header_up X-Forwarded-Host {host} +# header_up Host {host} +# } +# } +# +# # Block all other requests +# handle { +# respond "Registry operation not allowed" 405 +# } +# +# # Logging +# log { +# output file /var/log/caddy/registry.log +# format json +# level INFO +# } +# +# # Compression +# encode zstd gzip +# } diff --git a/registry/config.yml b/registry/config.yml index f4036d2..f68872c 100644 --- a/registry/config.yml +++ b/registry/config.yml @@ -16,4 +16,4 @@ middleware: storage: - name: Redirect options: - baseurl: https://YOUR_DOMAIN_NAME \ No newline at end of file + baseurl: https://YOUR_ACTUAL_IP_ADDRESS \ No newline at end of file diff --git a/registry/openssl.conf b/registry/openssl.conf new file mode 100644 index 0000000..81bd817 --- /dev/null +++ b/registry/openssl.conf @@ -0,0 +1,23 @@ +[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 \ No newline at end of file