diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index a6732ca..5feac00 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -53,6 +53,10 @@ jobs: timeout 15 bash -c 'until docker exec ci-dind docker version > /dev/null 2>&1; do echo "Waiting for Docker daemon inside DinD..."; sleep 5; done' echo "DinD container is ready" + # Install Cosign in DinD container + echo "Installing Cosign..." + docker exec ci-dind sh -c "wget -O /usr/local/bin/cosign https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 && chmod +x /usr/local/bin/cosign" + # Login to Docker Registry (using HTTPS port 443) echo "${{ secrets.REGISTRY_PASSWORD }}" | docker exec -i ci-dind docker login ${{ secrets.CI_HOST }}:443 -u ${{ secrets.REGISTRY_USER }} --password-stdin @@ -108,13 +112,22 @@ jobs: # Tag for Docker Registry docker exec ci-dind docker tag "$image" "$registry_image" - # Push to Docker Registry - if docker exec ci-dind docker push "$registry_image"; then - echo "✓ Successfully pushed $registry_image to Docker Registry" - else - echo "✗ Failed to push $registry_image to Docker Registry" - exit 1 - fi + # Push to Docker Registry + if docker exec ci-dind docker push "$registry_image"; then + echo "✓ Successfully pushed $registry_image to Docker Registry" + + # Sign the image with Cosign (keyless OIDC) + echo "Signing image with Cosign..." + if docker exec ci-dind sh -c "COSIGN_EXPERIMENTAL=1 cosign sign --keyless $registry_image"; then + echo "✓ Successfully signed $registry_image with Cosign" + else + echo "✗ Failed to sign $registry_image with Cosign" + exit 1 + fi + else + echo "✗ Failed to push $registry_image to Docker Registry" + exit 1 + fi else echo "✗ Failed to pull $image from Docker Hub" exit 1 @@ -251,6 +264,10 @@ jobs: --cache-to type=gha,mode=max \ -f /workspace/backend/Dockerfile \ /workspace/backend + + # Sign the backend image with Cosign (keyless OIDC) + echo "Signing backend image with Cosign..." + docker exec ci-dind sh -c "COSIGN_EXPERIMENTAL=1 cosign sign --keyless ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/backend:${{ gitea.sha }}" - name: Build and push frontend image run: | @@ -263,6 +280,10 @@ jobs: --cache-to type=gha,mode=max \ -f /workspace/frontend/Dockerfile \ /workspace/frontend + + # Sign the frontend image with Cosign (keyless OIDC) + echo "Signing frontend image with Cosign..." + docker exec ci-dind sh -c "COSIGN_EXPERIMENTAL=1 cosign sign --keyless ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/frontend:${{ gitea.sha }}" - name: Cleanup Testing Environment if: always() @@ -334,6 +355,11 @@ jobs: # Wait for Docker to be ready timeout 30 bash -c 'until docker info; do sleep 1; done' + + # Verify signed images before deployment + echo "Verifying signed images..." + cosign verify --key /etc/containers/keys/org-cosign.pub ${{ secrets.CI_HOST }}:443/${{ secrets.APP_NAME || 'sharenet' }}/backend:${{ gitea.sha }} + cosign verify --key /etc/containers/keys/org-cosign.pub ${{ secrets.CI_HOST }}:443/${{ secrets.APP_NAME || 'sharenet' }}/frontend:${{ gitea.sha }} - name: Validate migration files run: | diff --git a/.gitignore b/.gitignore index a28d49e..ef8fb5a 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,16 @@ yarn-error.log* Thumbs.db *.pem +# Cosign keys and certificates +*.key +*.pem +*.crt +*.pub +cosign.key +cosign.pub +org-cosign.key +org-cosign.pub + # Build outputs dist/ build/ diff --git a/CI_CD_PIPELINE_SETUP_GUIDE.md b/CI_CD_PIPELINE_SETUP_GUIDE.md index 312a37c..88488f6 100644 --- a/CI_CD_PIPELINE_SETUP_GUIDE.md +++ b/CI_CD_PIPELINE_SETUP_GUIDE.md @@ -1808,8 +1808,8 @@ Go to your Forgejo repository and add these secrets in **Settings → Secrets an - `POSTGRES_PASSWORD`: A strong password for the PostgreSQL database - `REGISTRY_CLIENT_CERT`: Path to client certificate for mTLS authentication (e.g., `/etc/registry/certs/clients/client.crt`) - `REGISTRY_CLIENT_KEY`: Path to client private key for mTLS authentication (e.g., `/etc/registry/certs/private/client.key`) -- `REGISTRY_PUSH_URL`: Docker Registry v2 URL for authenticated pushes (e.g., `YOUR_CI_CD_IP:4443`) -- `REGISTRY_PULL_URL`: Docker Registry v2 URL for unauthenticated pulls (e.g., `YOUR_CI_CD_IP`) + +**Note**: The CI pipeline now uses mTLS authentication for pushes (port 4443) and Cosign for image signing. The registry policy enforces Sigstore signatures for all images consumed from the registry. diff --git a/Docker_Registry_Install_Guide.md b/Docker_Registry_Install_Guide.md index c964401..e704234 100644 --- a/Docker_Registry_Install_Guide.md +++ b/Docker_Registry_Install_Guide.md @@ -382,6 +382,23 @@ sudo cp /etc/registry/certs/ca/ca.crt /usr/local/share/ca-certificates/registry- # This step should complete with "1 added, 0 removed". If it does not, there could be a problem with the certificate you generated, or you might already have a certificate in the trust store sudo update-ca-certificates + +# 4. Generate Cosign key pair for image signing +# Install Cosign if not already installed +wget -O /usr/local/bin/cosign https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 +chmod +x /usr/local/bin/cosign + +# Generate Cosign key pair (or use keyless OIDC in CI) +cosign generate-key-pair + +# Create directory for Cosign public key distribution +sudo mkdir -p /etc/containers/keys +sudo cp cosign.pub /etc/containers/keys/org-cosign.pub +sudo chmod 644 /etc/containers/keys/org-cosign.pub + +# Secure the private key (keep this safe and distribute to CI systems) +sudo chmod 600 cosign.key +echo "IMPORTANT: Secure cosign.key and distribute to CI systems for image signing" ``` ## Step 4: Configure Firewall and Start Services @@ -410,13 +427,17 @@ sudo ufw allow from 192.168.0.0/16 to any port 4443 proto tcp ## Client Trust Configuration -For clients to trust your registry certificates, they should install the server CA certificate: +For clients to trust your registry certificates and enforce image signatures, they should install the server CA certificate and Cosign public key: **For pulls (port 443):** ```bash # On client systems - use the actual FQDN/IP from your certificates sudo mkdir -p /etc/containers/certs.d/REGISTRY_HOST sudo cp /path/to/registry-ca.crt /etc/containers/certs.d/REGISTRY_HOST/ca.crt + +# Install Cosign public key for signature verification +sudo mkdir -p /etc/containers/keys +sudo cp /path/to/org-cosign.pub /etc/containers/keys/org-cosign.pub ``` **For pushes (port 4443, mTLS):** @@ -426,9 +447,23 @@ sudo mkdir -p /etc/containers/certs.d/REGISTRY_HOST:4443 sudo cp /path/to/registry-ca.crt /etc/containers/certs.d/REGISTRY_HOST:4443/ca.crt sudo cp /path/to/client.crt /etc/containers/certs.d/REGISTRY_HOST:4443/client.cert sudo cp /path/to/client.key /etc/containers/certs.d/REGISTRY_HOST:4443/client.key + +# Install Cosign public key for signature verification +sudo mkdir -p /etc/containers/keys +sudo cp /path/to/org-cosign.pub /etc/containers/keys/org-cosign.pub ``` -**Note:** Replace `REGISTRY_HOST` with the actual FQDN or IP address that matches your certificate's Subject Alternative Name (SAN). For pushes, both the server CA certificate and client certificate/key are required for mTLS authentication. +**Note:** Replace `REGISTRY_HOST` with the actual FQDN or IP address that matches your certificate's Subject Alternative Name (SAN). For pushes, both the server CA certificate and client certificate/key are required for mTLS authentication. The Cosign public key is required for signature verification on both ports. + +## Security Model + +This setup implements a multi-layered security approach: + +1. **mTLS on port 4443**: Controls **who can push** to the registry +2. **Cosign signatures**: Controls **what content is trusted** - all images must be signed +3. **Policy enforcement**: The `containers-policy.json` rejects unsigned images from the registry +4. **Port separation**: Unauthenticated pulls (443) vs authenticated pushes (4443) +5. **Rootless registry**: Registry runs as non-root user with minimal privileges ``` ### 4.2 Enable and Start Services @@ -548,6 +583,10 @@ openssl s_client -connect YOUR_ACTUAL_IP_ADDRESS:4443 -servername YOUR_ACTUAL_IP -cert /etc/registry/certs/clients/client.crt \ -key /etc/registry/certs/private/client.key < /dev/null +# Test Cosign signature verification +echo "Testing Cosign signature verification..." +cosign verify --key /etc/containers/keys/org-cosign.pub YOUR_ACTUAL_IP_ADDRESS/APP_NAME/test:latest + # Verify nginx is using the correct certificates sudo ls -la /etc/registry/certs/registry.crt /etc/registry/certs/private/registry.key /etc/registry/certs/clients/ca.crt diff --git a/registry/containers-policy.json b/registry/containers-policy.json index f44727a..2467b3e 100644 --- a/registry/containers-policy.json +++ b/registry/containers-policy.json @@ -1,19 +1,19 @@ { - "default": [ - { - "type": "reject" - } - ], + "default": [{ "type": "reject" }], "transports": { "docker": { - "localhost:443": [ + "REGISTRY_HOST": [ { - "type": "insecureAcceptAnything" + "type": "sigstoreSigned", + "keyPath": "/etc/containers/keys/org-cosign.pub", + "signedIdentity": { "type": "matchRepository" } } ], - "localhost:4443": [ + "REGISTRY_HOST:4443": [ { - "type": "insecureAcceptAnything" + "type": "sigstoreSigned", + "keyPath": "/etc/containers/keys/org-cosign.pub", + "signedIdentity": { "type": "matchRepository" } } ], "docker.io": [ @@ -22,12 +22,6 @@ } ] }, - "docker-daemon": { - "": [ - { - "type": "insecureAcceptAnything" - } - ] - } + "docker-daemon": { "": [{ "type": "reject" }] } } }