From eb6e37398171eb0aec19f65c104f8f78c1a196c5 Mon Sep 17 00:00:00 2001 From: continuist Date: Sat, 30 Aug 2025 19:38:54 -0400 Subject: [PATCH] Change to using Forgejo Container Registry --- .forgejo/workflows/ci.yml | 241 +++++---- CI_CD_PIPELINE_SETUP_GUIDE.md | 201 +++++-- Docker_Registry_Install_Guide.md | 768 --------------------------- README.md | 21 + backend/Dockerfile.test-postgres | 6 +- backend/Dockerfile.test-rust | 6 +- test-pod.yaml => ci-pod.yaml | 0 docker-compose.prod.yml | 84 --- docker-compose.test.yml | 70 --- frontend/Dockerfile.test-node | 6 +- production-pod.yaml => prod-pod.yaml | 8 +- registry/README.md | 92 ---- registry/containers-policy.json | 23 - registry/docker-registry.service | 32 -- registry/nginx.conf | 220 -------- registry/openssl.conf | 19 - registry/registry-pod.yaml | 169 ------ scripts/monitor.sh | 192 ------- scripts/test_dind_setup.sh | 113 ---- 19 files changed, 332 insertions(+), 1939 deletions(-) delete mode 100644 Docker_Registry_Install_Guide.md rename test-pod.yaml => ci-pod.yaml (100%) delete mode 100644 docker-compose.prod.yml delete mode 100644 docker-compose.test.yml rename production-pod.yaml => prod-pod.yaml (90%) delete mode 100644 registry/README.md delete mode 100644 registry/containers-policy.json delete mode 100644 registry/docker-registry.service delete mode 100644 registry/nginx.conf delete mode 100644 registry/openssl.conf delete mode 100644 registry/registry-pod.yaml delete mode 100755 scripts/monitor.sh delete mode 100755 scripts/test_dind_setup.sh diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 04c5101..8d131f3 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI/CD Pipeline (Fully Isolated DinD) +name: CI/CD Pipeline (Forgejo Container Registry) on: push: @@ -7,8 +7,8 @@ on: branches: [ main ] env: - REGISTRY: ${{ secrets.CI_HOST }}:443 - IMAGE_NAME: ${{ secrets.APP_NAME || 'sharenet' }} + REGISTRY_HOST: ${{ secrets.REGISTRY_HOST }} + OWNER_REPO: ${{ gitea.repository }} jobs: # Job 1: Testing - Uses DinD with multiple containers for comprehensive testing @@ -57,9 +57,6 @@ jobs: echo "Installing Cosign..." docker exec ci-dind sh -c "COSIGN_VERSION=v2.2.4 && wget -O /usr/local/bin/cosign https://github.com/sigstore/cosign/releases/download/\${COSIGN_VERSION}/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 - echo "DinD container setup complete" fi @@ -77,57 +74,63 @@ jobs: docker exec ci-dind rm -rf /workspace/* /workspace/.* 2>/dev/null || true docker cp /tmp/ci-workspace/. ci-dind:/workspace/ - - name: Check and prepare Docker Registry base images + - name: Check and prepare base images run: | # Set environment variables - export CI_HOST="${{ secrets.CI_HOST }}" - export APP_NAME="${{ secrets.APP_NAME || 'sharenet' }}" - export REGISTRY_USER="${{ secrets.REGISTRY_USER }}" - export REGISTRY_PASSWORD="${{ secrets.REGISTRY_PASSWORD }}" + export REGISTRY_HOST="${{ secrets.REGISTRY_HOST }}" + export OWNER_REPO="${{ gitea.repository }}" + export REGISTRY_USERNAME="${{ secrets.REGISTRY_USERNAME }}" + export REGISTRY_TOKEN="${{ secrets.REGISTRY_TOKEN }}" - # Login to Docker Registry - echo "Logging into Docker Registry..." - echo "$REGISTRY_PASSWORD" | docker exec -i ci-dind docker login "$CI_HOST:443" -u "$REGISTRY_USER" --password-stdin + # Login to Forgejo Container Registry + echo "Logging into Forgejo Container Registry..." + echo "$REGISTRY_TOKEN" | docker exec -i ci-dind docker login "$REGISTRY_HOST" -u "$REGISTRY_USERNAME" --password-stdin - # Check if base images exist in Docker Registry, pull from Docker Hub if not + # Check if base images exist in Forgejo Container Registry, pull from Docker Hub if not BASE_IMAGES=("rust:1.75-slim" "node:20-slim" "postgres:15-alpine") for image in "${BASE_IMAGES[@]}"; do image_name=$(echo "$image" | cut -d: -f1) image_tag=$(echo "$image" | cut -d: -f2) - registry_image="$CI_HOST:443/$APP_NAME/$image_name:$image_tag" + registry_image="$REGISTRY_HOST/$OWNER_REPO/$image_name:$image_tag" - echo "Checking if $registry_image exists in Docker Registry..." + echo "Checking if $registry_image exists in Forgejo Container Registry..." - # Try to pull from Docker Registry first + # Try to pull from Forgejo Container Registry first if docker exec ci-dind docker pull "$registry_image" 2>/dev/null; then - echo "✓ Found $registry_image in Docker Registry" + echo "✓ Found $registry_image in Forgejo Container Registry" else - echo "✗ $registry_image not found in Docker Registry, pulling from Docker Hub..." + echo "✗ $registry_image not found in Forgejo Container Registry, pulling from Docker Hub..." # Pull from Docker Hub if docker exec ci-dind docker pull "$image"; then echo "✓ Successfully pulled $image from Docker Hub" - # Tag for Docker Registry + # Tag for Forgejo Container 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" - - # 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 + # Push to Forgejo Container Registry + if docker exec ci-dind docker push "$registry_image"; then + echo "✓ Successfully pushed $registry_image to Forgejo Container Registry" + + # Sign the image with Cosign (optional) + if [ -n "${{ secrets.COSIGN_PRIVATE_KEY }}" ]; then + echo "Signing image with Cosign..." + echo "${{ secrets.COSIGN_PRIVATE_KEY }}" | docker exec -i ci-dind sh -c "cat > /tmp/cosign.key && chmod 600 /tmp/cosign.key" + if docker exec ci-dind sh -c "COSIGN_PASSWORD='${{ secrets.COSIGN_PASSWORD }}' cosign sign -y --key /tmp/cosign.key $registry_image"; then + echo "✓ Successfully signed $registry_image with Cosign" + else + echo "✗ Failed to sign $registry_image with Cosign" + exit 1 + fi + docker exec ci-dind rm -f /tmp/cosign.key + else + echo "Skipping Cosign signing (no private key provided)" + fi + else + echo "✗ Failed to push $registry_image to Forgejo Container Registry" + exit 1 + fi else echo "✗ Failed to pull $image from Docker Hub" exit 1 @@ -135,19 +138,20 @@ jobs: fi done - echo "All base images are ready in Docker Registry!" + echo "All base images are ready in Forgejo Container Registry!" - name: Start testing environment run: | - # Start testing environment using dedicated compose file inside DinD + # Start testing environment using Kubernetes pod inside DinD echo "Starting testing environment..." - # Set environment variables for docker-compose + # Set environment variables export CI_HOST="${{ secrets.CI_HOST }}" export APP_NAME="${{ secrets.APP_NAME || 'sharenet' }}" - # Start the testing environment with build args - docker exec ci-dind sh -c "export CI_HOST='$CI_HOST' && export APP_NAME='$APP_NAME' && docker compose -f /workspace/docker-compose.test.yml up -d --build" + # Create workspace directory and start pod + docker exec ci-dind sh -c "mkdir -p /tmp/ci-workspace && cp -r /workspace/* /tmp/ci-workspace/" + docker exec ci-dind sh -c "podman play kube /workspace/ci-pod.yaml" # Wait for all services to be ready with better error handling echo "Waiting for testing environment to be ready..." @@ -155,40 +159,39 @@ jobs: WAIT_COUNT=0 while [ $WAIT_COUNT -lt $MAX_WAIT ]; do - # Check if all containers are running - RUNNING_CONTAINERS=$(docker exec ci-dind docker compose -f /workspace/docker-compose.test.yml ps -q | wc -l) - EXPECTED_CONTAINERS=3 # postgres, rust-toolchain, node-toolchain + # Check if pod is running and ready + POD_STATUS=$(docker exec ci-dind podman pod ps --filter name=ci-cd-test-pod --format "{{.Status}}" 2>/dev/null || echo "") - if [ "$RUNNING_CONTAINERS" -eq "$EXPECTED_CONTAINERS" ]; then - echo "All containers are running" + if [ "$POD_STATUS" = "Running" ]; then + echo "Pod is running" break else - echo "Waiting for containers to start... ($RUNNING_CONTAINERS/$EXPECTED_CONTAINERS running)" + echo "Waiting for pod to start... (Status: $POD_STATUS)" sleep 2 WAIT_COUNT=$((WAIT_COUNT + 2)) fi done if [ $WAIT_COUNT -ge $MAX_WAIT ]; then - echo "ERROR: Timeout waiting for containers to start" - echo "Container status:" - docker exec ci-dind docker compose -f /workspace/docker-compose.test.yml ps - echo "Container logs:" - docker exec ci-dind docker compose -f /workspace/docker-compose.test.yml logs + echo "ERROR: Timeout waiting for pod to start" + echo "Pod status:" + docker exec ci-dind podman pod ps + echo "Pod logs:" + docker exec ci-dind podman logs ci-cd-test-pod-postgres || true exit 1 fi # Additional wait for PostgreSQL to be healthy echo "Waiting for PostgreSQL to be healthy..." - timeout 60 bash -c 'until docker exec ci-dind docker exec ci-cd-test-postgres pg_isready -h localhost -p 5432 -U postgres; do sleep 1; done' + timeout 60 bash -c 'until docker exec ci-dind podman exec ci-cd-test-pod-postgres pg_isready -h localhost -p 5432 -U postgres; do sleep 1; done' - # Verify all containers are running - echo "Final container status:" - docker exec ci-dind docker compose -f /workspace/docker-compose.test.yml ps + # Verify pod is running + echo "Final pod status:" + docker exec ci-dind podman pod ps - name: Install SQLx CLI in Rust container run: | - docker exec ci-dind docker exec ci-cd-test-rust cargo install sqlx-cli --no-default-features --features postgres + docker exec ci-dind podman exec ci-cd-test-pod-rust-toolchain cargo install sqlx-cli --no-default-features --features postgres - name: Validate migration files env: @@ -196,16 +199,16 @@ jobs: run: | # Wait for PostgreSQL to be ready echo "Waiting for PostgreSQL to be ready..." - timeout 60 bash -c 'until docker exec ci-dind docker exec ci-cd-test-postgres pg_isready -h localhost -p 5432 -U postgres; do sleep 1; done' + timeout 60 bash -c 'until docker exec ci-dind podman exec ci-cd-test-pod-postgres pg_isready -h localhost -p 5432 -U postgres; do sleep 1; done' # Create test database if it doesn't exist - docker exec ci-dind docker exec ci-cd-test-rust sqlx database create --database-url "$DATABASE_URL" || true + docker exec ci-dind podman exec ci-cd-test-pod-rust-toolchain sqlx database create --database-url "$DATABASE_URL" || true # Run initial migrations to set up the database - docker exec ci-dind docker exec ci-cd-test-rust sqlx migrate run --database-url "$DATABASE_URL" || true + docker exec ci-dind podman exec ci-cd-test-pod-rust-toolchain sqlx migrate run --database-url "$DATABASE_URL" || true # Validate migration files - docker exec ci-dind docker exec ci-cd-test-rust ./scripts/validate_migrations.sh --verbose + docker exec ci-dind podman exec ci-cd-test-pod-rust-toolchain ./scripts/validate_migrations.sh --verbose - name: Run backend tests working-directory: ./backend @@ -213,25 +216,26 @@ jobs: DATABASE_URL: postgres://postgres:password@localhost:5432/sharenet_test run: | # Run tests with increased parallelism for Rust - docker exec ci-dind docker exec ci-cd-test-rust cargo test --all --jobs 4 - docker exec ci-dind docker exec ci-cd-test-rust cargo clippy --all -- -D warnings - docker exec ci-dind docker exec ci-cd-test-rust cargo fmt --all -- --check + docker exec ci-dind podman exec ci-cd-test-pod-rust-toolchain cargo test --all --jobs 4 + docker exec ci-dind podman exec ci-cd-test-pod-rust-toolchain cargo clippy --all -- -D warnings + docker exec ci-dind podman exec ci-cd-test-pod-rust-toolchain cargo fmt --all -- --check - name: Install frontend dependencies run: | - docker exec ci-dind docker exec ci-cd-test-node npm ci + docker exec ci-dind podman exec ci-cd-test-pod-node-toolchain npm ci - name: Run frontend tests run: | - docker exec ci-dind docker exec ci-cd-test-node npm run lint - docker exec ci-dind docker exec ci-cd-test-node npm run type-check - docker exec ci-dind docker exec ci-cd-test-node npm run build + docker exec ci-dind podman exec ci-cd-test-pod-node-toolchain npm run lint + docker exec ci-dind podman exec ci-cd-test-pod-node-toolchain npm run type-check + docker exec ci-dind podman exec ci-cd-test-pod-node-toolchain npm run build - name: Cleanup Testing Environment if: always() run: | - # Stop and remove all testing containers (but keep DinD running) - docker exec ci-dind docker compose -f /workspace/docker-compose.test.yml down + # Stop and remove testing pod (but keep DinD running) + docker exec ci-dind podman pod stop ci-cd-test-pod || true + docker exec ci-dind podman pod rm ci-cd-test-pod || true # Job 2: Building - Build and push Docker images using same DinD build-and-push: @@ -253,37 +257,63 @@ jobs: # Verify we have the correct repository docker exec ci-dind sh -c "cd /workspace && git remote -v" + - name: Login to Forgejo registry + run: | + docker exec ci-dind docker login "${{ env.REGISTRY_HOST }}" \ + -u "${{ secrets.REGISTRY_USERNAME }}" \ + -p "${{ secrets.REGISTRY_TOKEN }}" + - name: Build and push backend image + env: + IMAGE: ${{ env.REGISTRY_HOST }}/${{ env.OWNER_REPO }}/backend + TAG: ${{ gitea.sha }} run: | # Build and push backend image using DinD docker exec ci-dind docker buildx build \ --platform linux/amd64 \ - --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/backend:${{ gitea.sha }} \ + --tag "${IMAGE}:${TAG}" \ --push \ --cache-from type=gha \ --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 }}" + # Sign the backend image with Cosign (optional) + if [ -n "${{ secrets.COSIGN_PRIVATE_KEY }}" ]; then + echo "Signing backend image with Cosign..." + echo "${{ secrets.COSIGN_PRIVATE_KEY }}" | docker exec -i ci-dind sh -c "cat > /tmp/cosign.key && chmod 600 /tmp/cosign.key" + DIGEST=$(docker exec ci-dind docker image inspect "${IMAGE}:${TAG}" --format '{{index .RepoDigests 0}}' | cut -d'@' -f2) + docker exec ci-dind sh -c "COSIGN_PASSWORD='${{ secrets.COSIGN_PASSWORD }}' cosign sign -y --key /tmp/cosign.key ${IMAGE}@${DIGEST}" + docker exec ci-dind rm -f /tmp/cosign.key + else + echo "Skipping Cosign signing (no private key provided)" + fi - name: Build and push frontend image + env: + IMAGE: ${{ env.REGISTRY_HOST }}/${{ env.OWNER_REPO }}/frontend + TAG: ${{ gitea.sha }} run: | # Build and push frontend image using DinD docker exec ci-dind docker buildx build \ --platform linux/amd64 \ - --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/frontend:${{ gitea.sha }} \ + --tag "${IMAGE}:${TAG}" \ --push \ --cache-from type=gha \ --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 }}" + # Sign the frontend image with Cosign (optional) + if [ -n "${{ secrets.COSIGN_PRIVATE_KEY }}" ]; then + echo "Signing frontend image with Cosign..." + echo "${{ secrets.COSIGN_PRIVATE_KEY }}" | docker exec -i ci-dind sh -c "cat > /tmp/cosign.key && chmod 600 /tmp/cosign.key" + DIGEST=$(docker exec ci-dind docker image inspect "${IMAGE}:${TAG}" --format '{{index .RepoDigests 0}}' | cut -d'@' -f2) + docker exec ci-dind sh -c "COSIGN_PASSWORD='${{ secrets.COSIGN_PASSWORD }}' cosign sign -y --key /tmp/cosign.key ${IMAGE}@${DIGEST}" + docker exec ci-dind rm -f /tmp/cosign.key + else + echo "Skipping Cosign signing (no private key provided)" + fi - name: Cleanup Testing Environment if: always() @@ -336,8 +366,8 @@ jobs: run: | # Create environment file for this deployment echo "IMAGE_TAG=${{ gitea.sha }}" > .env - echo "REGISTRY=${{ secrets.CI_HOST }}:443" >> .env - echo "IMAGE_NAME=${{ secrets.APP_NAME || 'sharenet' }}" >> .env + echo "REGISTRY_HOST=${{ secrets.REGISTRY_HOST }}" >> .env + echo "OWNER_REPO=${{ gitea.repository }}" >> .env echo "POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD || 'your_secure_password_here' }}" >> .env echo "POSTGRES_USER=${{ secrets.POSTGRES_USER || 'sharenet' }}" >> .env echo "POSTGRES_DB=${{ secrets.POSTGRES_DB || 'sharenet' }}" >> .env @@ -348,18 +378,22 @@ jobs: - name: Make scripts executable run: chmod +x scripts/*.sh - - name: Configure Docker for Docker Registry access + - name: Configure Docker for Forgejo Container Registry access run: | - # Configure Docker to access Docker Registry on CI Linode (using HTTPS) - # Since we're using Caddy with automatic HTTPS, no certificate configuration is needed + # Configure Docker to access Forgejo Container Registry + # Since we're using Forgejo's built-in registry, no certificate configuration is needed # 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 }} + # Verify signed images before deployment (if Cosign is configured) + if [ -n "${{ secrets.COSIGN_PRIVATE_KEY }}" ]; then + echo "Verifying signed images..." + cosign verify --key /etc/containers/keys/org-cosign.pub ${{ secrets.REGISTRY_HOST }}/${{ gitea.repository }}/backend:${{ gitea.sha }} + cosign verify --key /etc/containers/keys/org-cosign.pub ${{ secrets.REGISTRY_HOST }}/${{ gitea.repository }}/frontend:${{ gitea.sha }} + else + echo "Skipping image verification (no Cosign key configured)" + fi - name: Validate migration files run: | @@ -369,20 +403,29 @@ jobs: exit 1 } - - name: Pull and deploy application + - name: Deploy application using Kubernetes pod run: | - # Pull latest images from Docker Registry - echo "Pulling latest images from Docker Registry..." - docker compose -f docker-compose.prod.yml pull + # Set environment variables for the pod deployment + export IMAGE_TAG="${{ gitea.sha }}" + export REGISTRY_HOST="${{ secrets.REGISTRY_HOST }}" + export OWNER_REPO="${{ gitea.repository }}" + export POSTGRES_PASSWORD="${{ secrets.POSTGRES_PASSWORD || 'your_secure_password_here' }}" + export POSTGRES_USER="${{ secrets.POSTGRES_USER || 'sharenet' }}" + export POSTGRES_DB="${{ secrets.POSTGRES_DB || 'sharenet' }}" - # Deploy the application stack - echo "Deploying application stack..." - docker compose -f docker-compose.prod.yml up -d + # Stop any existing production pod + podman pod stop sharenet-production-pod || true + podman pod rm sharenet-production-pod || true - # Wait for all services to be healthy - echo "Waiting for all services to be healthy..." - timeout 120 bash -c 'until docker compose -f docker-compose.prod.yml ps | grep -q "healthy" && docker compose -f docker-compose.prod.yml ps | grep -q "Up"; do sleep 2; done' + # Deploy the application pod with environment substitution + echo "Deploying application pod..." + envsubst < prod-pod.yaml | podman play kube - + + # Wait for pod to be ready + echo "Waiting for pod to be ready..." + timeout 120 bash -c 'until podman pod ps --filter name=sharenet-production-pod --format "{{.Status}}" | grep -q "Running"; do sleep 2; done' # Verify deployment echo "Verifying deployment..." - docker compose -f docker-compose.prod.yml ps \ No newline at end of file + podman pod ps + podman pod logs sharenet-production-pod \ No newline at end of file diff --git a/CI_CD_PIPELINE_SETUP_GUIDE.md b/CI_CD_PIPELINE_SETUP_GUIDE.md index 88488f6..7ec92c7 100644 --- a/CI_CD_PIPELINE_SETUP_GUIDE.md +++ b/CI_CD_PIPELINE_SETUP_GUIDE.md @@ -8,7 +8,7 @@ This guide covers setting up a complete Continuous Integration/Continuous Deploy ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Forgejo Host │ │ CI/CD Linode │ │ Production Linode│ │ (Repository) │ │ (Actions Runner)│ │ (Podman Deploy) │ -│ │ │ + Podman Registry│ │ │ +│ │ │ + Forgejo Registry│ │ │ │ │ │ + PiP Container │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ @@ -23,7 +23,7 @@ This guide covers setting up a complete Continuous Integration/Continuous Deploy 1. **Code Push**: Developer pushes code to Forgejo repository 2. **Automated Testing**: CI/CD Linode runs tests in isolated DinD environment 3. **Image Building**: If tests pass, Docker images are built within DinD -4. **Registry Push**: Images are pushed to Docker Registry from DinD +4. **Registry Push**: Images are pushed to Forgejo Container Registry from DinD 5. **Production Deployment**: Production Linode pulls images and deploys 6. **Health Check**: Application is verified and accessible @@ -36,9 +36,9 @@ This guide covers setting up a complete Continuous Integration/Continuous Deploy - ✅ **Fast cleanup** - just restart PiP container ### **For CI/CD Operations:** -- ✅ **Zero resource contention** with Podman Registry +- ✅ **Zero resource contention** with Forgejo Container Registry - ✅ **Simple cleanup** - one-line container restart -- ✅ **Perfect isolation** - CI/CD can't affect Podman Registry +- ✅ **Perfect isolation** - CI/CD can't affect Forgejo Container Registry - ✅ **Consistent environment** - same setup every time ### **For Maintenance:** @@ -64,9 +64,9 @@ This guide covers setting up a complete Continuous Integration/Continuous Deploy ### CI/CD Linode Features - Forgejo Actions runner for automated builds - **Podman-in-Podman (PiP) container** for isolated CI operations -- **Rootless Docker Registry v2** with host TLS reverse proxy for image storage +- **Forgejo Container Registry** for secure image storage - **FHS-compliant directory structure** for data, certificates, and logs -- **Port 443 allows unauthenticated Docker Registry v2 pulls; Port 4443 is for authenticated pushes (mTLS)** +- **Secure registry access** via Forgejo authentication - Automatic HTTPS with nginx reverse proxy - Secure SSH communication with production - **Simplified cleanup** - just restart PiP container @@ -80,11 +80,11 @@ This guide covers setting up a complete Continuous Integration/Continuous Deploy ### Pipeline Features - **Automated testing** on every code push in isolated environment -- **Automated image building** and registry push from PiP +- **Automated image building** and push to Forgejo Container Registry from PiP - **Automated deployment** to production - **Rollback capability** with image versioning - **Health monitoring** and logging -- **Zero resource contention** between CI/CD and Docker Registry v2 +- **Zero resource contention** between CI/CD and Forgejo Container Registry - **Robust rootless services** via systemd user manager ## Security Model and User Separation @@ -650,21 +650,19 @@ ls -la /opt/APP_NAME/registry/ - CI_SERVICE_USER owns all the files for security - Registry configuration files are now available at `/opt/APP_NAME/registry/` -### Step 4: Install Docker Registry +### Step 4: Configure Forgejo Container Registry Access -**Note**: For complete Docker Registry installation and configuration, see the [Docker Registry Install Guide](Docker_Registry_Install_Guide.md). This guide covers: +**Note**: This project uses Forgejo's built-in Container Registry instead of a separate Docker Registry. The CI/CD pipeline is already configured to use Forgejo Container Registry. -- Rootless Docker Registry v2 with host TLS reverse proxy -- mTLS authentication for secure push operations -- Unauthenticated pulls for public read access -- FHS-compliant directory structure -- Systemd user manager for robust rootless services +**Configuration**: Registry access is handled through: +- **Authentication**: Forgejo Personal Access Tokens (PAT) +- **Registry URL**: Your Forgejo instance's registry endpoint +- **Security**: Built-in Forgejo authentication and authorization -**Quick Reference**: The Docker Registry will be accessible at: -- **Port 443**: Unauthenticated pulls (public read access) -- **Port 4443**: Authenticated pushes (mTLS client certificate required) - -**Port 443 allows unauthenticated Docker Registry v2 pulls; Port 4443 is for authenticated pushes (mTLS).** +**Quick Reference**: The Forgejo Container Registry will be accessible at: +- **Registry URL**: `${REGISTRY_HOST}` (configured in secrets) +- **Authentication**: PAT with `write:packages` scope for pushes +- **Public Access**: Available for pulls from public repositories ### Step 5: Install Forgejo Actions Runner @@ -899,7 +897,7 @@ sudo journalctl -u forgejo-runner.service -f --no-pager ### Step 6: Set Up Podman-in-Podman (PiP) for CI Operations -**Important**: This step sets up a Podman-in-Podman container that provides an isolated environment for CI/CD operations, eliminating resource contention with Podman Registry and simplifying cleanup. +**Important**: This step sets up a Podman-in-Podman container that provides an isolated environment for CI/CD operations, eliminating resource contention with Forgejo Container Registry and simplifying cleanup. #### 6.1 Create Containerized CI/CD Environment @@ -931,7 +929,7 @@ podman exec ci-pip podman version **Why CI_SERVICE_USER**: The CI_SERVICE_USER has Podman access and runs the CI pipeline, so it needs direct access to the PiP container for seamless CI/CD operations. -#### 6.2 Configure PiP for Docker Registry v2 +#### 6.2 Configure PiP for Forgejo Container Registry ```bash # Navigate to the application directory @@ -952,7 +950,7 @@ podman cp /etc/registry/certs/private/client.key ci-pip:/etc/registry/certs/priv podman exec ci-pip chmod 600 /etc/registry/certs/private/client.key podman exec ci-pip chmod 644 /etc/registry/certs/clients/client.crt -# Test Docker Registry v2 connectivity from PiP (using mTLS on port 4443) +# Test Forgejo Container Registry connectivity from PiP podman exec ci-pip podman pull alpine:latest podman exec ci-pip podman tag alpine:latest YOUR_CI_CD_IP:4443/APP_NAME/test:latest podman exec ci-pip podman push YOUR_CI_CD_IP:4443/APP_NAME/test:latest @@ -997,7 +995,7 @@ ls -la /tmp/ci-workspace ### FHS-Compliant Directory Structure -The Docker Registry setup follows the Filesystem Hierarchy Standard (FHS) for better organization and security. For complete details, see the [Docker Registry Install Guide](Docker_Registry_Install_Guide.md). +The Forgejo Container Registry setup uses the built-in registry functionality, providing secure and integrated container image storage. **Application Files** (in `/opt/APP_NAME/registry/`): - `containers-policy.json` - Container policy for image signature verification @@ -1027,9 +1025,9 @@ The Docker Registry setup follows the Filesystem Hierarchy Standard (FHS) for be - **Policy enforcement**: Container policies for image signature verification **What this does**: -- **Configures certificate trust**: Properly sets up Docker Registry certificate trust in DinD +- **Configures registry access**: Properly sets up Forgejo Container Registry access in DinD - **Fixes ownership issues**: Ensures certificate has correct ownership for CA trust -- **Tests connectivity**: Verifies DinD can pull, tag, and push images to Docker Registry +- **Tests connectivity**: Verifies DinD can pull, tag, and push images to Forgejo Container Registry - **Validates setup**: Ensures the complete CI/CD pipeline will work #### 6.4 CI/CD Workflow Architecture @@ -1045,32 +1043,32 @@ The CI/CD pipeline uses a three-stage approach with dedicated environments for e - Rust toolchain for backend testing and migrations - Node.js toolchain for frontend testing - **Network**: All containers communicate through `ci-cd-test-network` -- **Setup**: PiP container created, Docker Registry v2 login performed, code cloned into PiP from Forgejo +- **Setup**: PiP container created, Forgejo Container Registry login performed, code cloned into PiP from Forgejo - **Cleanup**: Testing containers removed, DinD container kept running **Job 2 (Building) - Direct Docker Commands:** -- **Purpose**: Image building and pushing to Docker Registry +- **Purpose**: Image building and pushing to Forgejo Container Registry - **Environment**: Same DinD container from Job 1 - **Code Access**: Reuses code from Job 1, updates to latest commit - **Process**: - Uses Docker Buildx for efficient building - Builds backend and frontend images separately - - Pushes images to Docker Registry -- **Registry Access**: Reuses Docker Registry authentication from Job 1 + - Pushes images to Forgejo Container Registry +- **Registry Access**: Reuses Forgejo Container Registry authentication from Job 1 - **Cleanup**: DinD container stopped and removed (clean slate for next run) **Job 3 (Deployment) - `docker-compose.prod.yml`:** - **Purpose**: Production deployment with pre-built images - **Environment**: Production runner on Production Linode - **Process**: - - Pulls images from Docker Registry + - Pulls images from Forgejo Container Registry - Deploys complete application stack - Verifies all services are healthy - **Services**: PostgreSQL, backend, frontend, Nginx **Key Benefits:** - **🧹 Complete Isolation**: Each job has its own dedicated environment -- **🚫 No Resource Contention**: Testing and building don't interfere with Docker Registry +- **🚫 No Resource Contention**: Testing and building don't interfere with Forgejo Container Registry - **⚡ Consistent Environment**: Same setup every time - **🎯 Purpose-Specific**: Each Docker Compose file serves a specific purpose - **🔄 Parallel Safety**: Jobs can run safely in parallel @@ -1081,7 +1079,7 @@ The CI/CD pipeline uses a three-stage approach with dedicated environments for e # Test DinD functionality docker exec ci-dind docker run --rm alpine:latest echo "DinD is working!" -# Test Docker Registry integration (using authenticated port for push) +# Test Forgejo Container Registry integration docker exec ci-dind docker pull alpine:latest docker exec ci-dind docker tag alpine:latest YOUR_CI_CD_IP:4443/APP_NAME/dind-test:latest docker exec ci-dind docker push YOUR_CI_CD_IP:4443/APP_NAME/dind-test:latest @@ -1097,11 +1095,11 @@ docker exec ci-dind docker rmi YOUR_CI_CD_IP/APP_NAME/dind-test:latest **Expected Output**: - DinD container should be running and accessible - Docker commands should work inside DinD -- Docker Registry push/pull should work from DinD +- Forgejo Container Registry push/pull should work from DinD #### 6.5 Production Deployment Architecture -The production deployment uses a separate Docker Compose file (`docker-compose.prod.yml`) that pulls built images from the Docker Registry and deploys the complete application stack. +The production deployment uses a separate Docker Compose file (`docker-compose.prod.yml`) that pulls built images from the Forgejo Container Registry and deploys the complete application stack. **Production Stack Components:** - **PostgreSQL**: Production database with persistent storage @@ -1111,7 +1109,7 @@ The production deployment uses a separate Docker Compose file (`docker-compose.p **Deployment Flow:** 1. **Production Runner**: Runs on Production Linode with `production` label -2. **Image Pull**: Pulls latest images from Docker Registry on CI Linode +2. **Image Pull**: Pulls latest images from Forgejo Container Registry 3. **Stack Deployment**: Uses `docker-compose.prod.yml` to deploy complete stack 4. **Health Verification**: Ensures all services are healthy before completion @@ -1471,22 +1469,18 @@ ls -la /opt/APP_NAME # Change to the PROD_SERVICE_USER sudo su - PROD_SERVICE_USER -# Test that Podman can pull images from the Docker Registry v2 (unauthenticated port 443) -podman pull YOUR_CI_CD_IP/APP_NAME/test:latest - -# If the pull succeeds, the Docker Registry v2 is accessible for production deployments +# Test Forgejo Container Registry access will be verified during deployment +# Images are pulled from the configured REGISTRY_HOST during the CI/CD process # Change back to PROD_DEPLOY_USER exit ``` -**Important**: Replace `YOUR_CI_CD_IP` with your actual CI/CD Linode IP address. - -**Note**: Production deployments use unauthenticated pulls from port 443, while CI/CD operations use authenticated pushes to port 4443. +**Note**: Production deployments pull images from Forgejo Container Registry using the configured authentication. **What this does**: -- **Tests Docker Registry v2 access**: Verifies that Podman can successfully pull images from the Docker Registry v2 -- **No certificate configuration needed**: nginx handles HTTPS automatically +- **Tests Forgejo Container Registry access**: Registry access is verified during the deployment process +- **Integrated authentication**: Forgejo handles authentication automatically - **Simple setup**: No complex certificate management required ### Step 14: Set Up Forgejo Runner for Production Deployment @@ -1958,6 +1952,123 @@ tail -f /tmp/monitor.log --- +## Forgejo Container Registry Setup + +This repository has been configured to use **Forgejo's built-in container registry** instead of a custom Docker Registry. This provides a simpler, more integrated solution while maintaining the same Podman Pods CI approach. + +### Prerequisites + +Before using this setup, ensure that: + +1. **Forgejo Container Registry (Packages)** is enabled on your Forgejo instance +2. **App repository (or its owner org)** is public so anonymous pulls work +3. **A PAT with `write:packages` scope** exists for CI pushes + +### Repository Secrets + +The CI pipeline expects the following secrets to be configured in your Forgejo repository: + +| Secret Name | Description | Example | +|-------------|-------------|---------| +| `REGISTRY_HOST` | Your Forgejo instance hostname | `forgejo.example.com` | +| `REGISTRY_USERNAME` | Bot or owner account username | `ci-bot` | +| `REGISTRY_TOKEN` | PAT with `write:packages` scope | `gto_...` | +| `COSIGN_PRIVATE_KEY` | (Optional) Private key for image signing | `-----BEGIN PRIVATE KEY-----...` | +| `COSIGN_PASSWORD` | (Optional) Password for Cosign private key | `your-password` | + +### How It Works + +#### CI Pipeline Changes + +The CI pipeline has been updated to: + +1. **Login to Forgejo Container Registry** using the provided credentials +2. **Build images** with tags like `REGISTRY_HOST/owner/repo/backend:GIT_SHA` +3. **Push images** to Forgejo's built-in registry +4. **Optionally sign images** with Cosign if keys are provided +5. **Deploy from Forgejo registry** instead of custom registry + +#### Anonymous Pulls + +Since the repository is public, applications can pull images anonymously: + +```bash +# Pull by tag +podman pull forgejo.example.com/owner/repo/backend:latest + +# Pull by digest +podman pull forgejo.example.com/owner/repo/backend@sha256:abc123... +``` + +#### Image Naming Convention + +Images are stored in Forgejo's registry using this format: +- `REGISTRY_HOST/OWNER_REPO/backend:TAG` +- `REGISTRY_HOST/OWNER_REPO/frontend:TAG` +- `REGISTRY_HOST/OWNER_REPO/base-image:TAG` + +For example: +- `forgejo.example.com/devteam/sharenet/backend:abc123` +- `forgejo.example.com/devteam/sharenet/frontend:abc123` + +### Migration from Custom Registry + +If you were previously using the custom Docker Registry setup: + +1. **Remove old registry artifacts**: + - Delete `Docker_Registry_Install_Guide.md` + - Remove `registry/` folder + - Update any references to old registry paths + +2. **Update secrets**: + - Replace `CI_HOST`, `REGISTRY_USER`, `REGISTRY_PASSWORD` with new Forgejo registry secrets + - Add `REGISTRY_HOST`, `REGISTRY_USERNAME`, `REGISTRY_TOKEN` + +3. **Update deployment**: + - Production environment will now pull from Forgejo registry + - No changes needed to Dockerfiles or application code + +### Benefits of Forgejo Container Registry + +- ✅ **Simplified setup** - No custom registry installation required +- ✅ **Integrated security** - Uses Forgejo's built-in authentication +- ✅ **Automatic HTTPS** - No certificate management needed +- ✅ **Built-in UI** - View and manage images through Forgejo web interface +- ✅ **Anonymous pulls** - Public repositories allow unauthenticated pulls +- ✅ **Same CI approach** - Maintains existing Podman Pods workflow + +### Troubleshooting + +#### Common Issues + +1. **Authentication failures**: + - Verify `REGISTRY_TOKEN` has `write:packages` scope + - Check `REGISTRY_HOST` is correct (no protocol, just hostname) + +2. **Pull failures**: + - Ensure repository/org is public for anonymous pulls + - Verify image tags exist in Forgejo registry + +3. **Cosign signing failures**: + - Check `COSIGN_PRIVATE_KEY` and `COSIGN_PASSWORD` are set + - Verify private key format is correct + +#### Verification Commands + +```bash +# Test registry access +podman login REGISTRY_HOST -u REGISTRY_USERNAME -p REGISTRY_TOKEN + +# List available images +podman search REGISTRY_HOST/OWNER_REPO + +# Pull and verify image +podman pull REGISTRY_HOST/OWNER_REPO/backend:TAG +podman image inspect REGISTRY_HOST/OWNER_REPO/backend:TAG +``` + +--- + ## 🎉 Congratulations! You have successfully set up a complete CI/CD pipeline with: diff --git a/Docker_Registry_Install_Guide.md b/Docker_Registry_Install_Guide.md deleted file mode 100644 index 33246bb..0000000 --- a/Docker_Registry_Install_Guide.md +++ /dev/null @@ -1,768 +0,0 @@ -# Docker Registry Install Guide - -This guide covers setting up a rootless Docker Registry v2 with host TLS reverse proxy for secure image storage. The registry runs rootless via Podman with loopback-only access, while a host nginx reverse proxy provides TLS termination and mTLS authentication. - -## Architecture Overview - -``` -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ External │ │ Host TLS │ │ Rootless │ -│ Clients │ │ Reverse Proxy │ │ Registry │ -│ │ │ (Nginx) │ │ (Podman) │ -│ │ │ │ │ │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ - │ │ │ - │ │ │ - └─── HTTPS :443 ────────┼───────────────────────┘ - (Unauthenticated pulls) │ - └─── HTTPS :4443 ───────┘ - (mTLS Authentication) -``` - -## Security Model - -- **Rootless Registry**: Runs as `CI_SERVICE_USER` via systemd user manager -- **Host TLS Proxy**: Runs as `registry-proxy` with minimal capabilities -- **Loopback Isolation**: Registry only accessible via `127.0.0.1:5000` -- **mTLS Authentication**: Client certificates required for push operations -- **No $HOME I/O**: All Podman state outside user home directory -- **Port 443**: Unauthenticated pulls only (GET requests) -- **Port 4443**: Authenticated pushes only (mTLS required) - -## Prerequisites - -- Ubuntu 24.04 LTS with root access -- Podman installed and configured for rootless operation -- `CI_SERVICE_USER` created and configured -- Basic familiarity with Linux commands and SSH - -## Step 1: Install Podman (if not already installed) - -### 1.1 Install Podman - -```bash -# Install Podman and related tools -sudo apt install -y podman slirp4netns fuse-overlayfs nginx - -# Disable stock nginx.service to avoid conflicts with hardened registry-proxy.service -sudo systemctl disable --now nginx.service - -# Verify installation -podman --version - -# Configure Podman for rootless operation (optional but recommended) -echo 'kernel.unprivileged_userns_clone=1' | sudo tee -a /etc/sysctl.conf -sudo sysctl -p - -# Configure subuid/subgid for CI_SERVICE_USER -sudo usermod --add-subuids 100000-165535 CI_SERVICE_USER -sudo usermod --add-subgids 100000-165535 CI_SERVICE_USER -``` - -## Step 2: Set Up Rootless Docker Registry v2 - -### 2.1 Create System-wide Podman Configuration - -```bash -# Create system-wide Podman configuration -sudo mkdir -p /etc/containers -sudo tee /etc/containers/registries.conf > /dev/null << 'EOF' -unqualified-search-registries = ["docker.io"] -short-name-mode = "enforcing" -EOF - -# Set proper permissions for system-wide Podman config (root-owned) -sudo chown root:root /etc/containers/registries.conf -sudo chmod 644 /etc/containers/registries.conf -``` - -### 2.2 Enable User Manager and Create Directories - -```bash -# Enable lingering for CI_SERVICE_USER to allow systemd user manager -sudo loginctl enable-linger CI_SERVICE_USER - -# Create Podman rootless directories outside home -sudo mkdir -p /var/tmp/podman-$(id -u CI_SERVICE_USER)/{root,run,tmp,xdg-data,xdg-config} -sudo chown -R CI_SERVICE_USER:CI_SERVICE_USER /var/tmp/podman-$(id -u CI_SERVICE_USER) -sudo chmod 750 /var/tmp/podman-$(id -u CI_SERVICE_USER) - -# Initialize Podman with rootless configuration (no home directory access) -sudo su - CI_SERVICE_USER -c "env PODMAN_ROOT=/var/tmp/podman-\$(id -u)/root PODMAN_RUNROOT=/run/user/\$(id -u)/podman-run PODMAN_TMPDIR=/var/tmp/podman-\$(id -u)/tmp XDG_DATA_HOME=/var/tmp/podman-\$(id -u)/xdg-data XDG_CONFIG_HOME=/var/tmp/podman-\$(id -u)/xdg-config podman system migrate" -sudo su - CI_SERVICE_USER -c "env PODMAN_ROOT=/var/tmp/podman-\$(id -u)/root PODMAN_RUNROOT=/run/user/\$(id -u)/podman-run PODMAN_TMPDIR=/var/tmp/podman-\$(id -u)/tmp XDG_DATA_HOME=/var/tmp/podman-\$(id -u)/xdg-data XDG_CONFIG_HOME=/var/tmp/podman-\$(id -u)/xdg-config podman info" -``` - -### 2.3 Create Registry User and Configuration Directories - -```bash -# Create registry-proxy user for TLS reverse proxy -sudo useradd -r -s /bin/false registry-proxy - -# Create registry configuration directories -sudo mkdir -p /etc/registry/certs/{private,clients,ca,requests} -sudo chown -R root:root /etc/registry/certs -sudo chmod 750 /etc/registry/certs/private -sudo chmod 755 /etc/registry/certs/{clients,ca,requests} - -# Create registry data directory -sudo mkdir -p /var/lib/registry -sudo chown CI_SERVICE_USER:CI_SERVICE_USER /var/lib/registry -sudo chmod 750 /var/lib/registry - -# Create log directory for nginx proxy -sudo install -d -o registry-proxy -g registry-proxy /var/log/registry-proxy - -# Create logrotate configuration for registry proxy logs -sudo tee /etc/logrotate.d/registry-proxy > /dev/null << 'EOF' -/var/log/registry-proxy/*.log { - daily - rotate 14 - compress - delaycompress - copytruncate - missingok - notifempty - create 640 registry-proxy registry-proxy -} -EOF -``` - -### 2.4 Install Systemd Services - -#### 2.4.1 Rootless Registry Service - -```bash -# Install systemd user service for rootless registry -sudo tee /etc/systemd/user/registry.service > /dev/null << 'EOF' -[Unit] -Description=Rootless Docker Registry v2 (loopback only) -After=network-online.target -Wants=network-online.target - -[Service] -Environment=PODMAN_ROOT=/var/tmp/podman-%U/root -Environment=PODMAN_RUNROOT=/run/user/%U/podman-run -Environment=PODMAN_TMPDIR=/var/tmp/podman-%U/tmp -Environment=XDG_DATA_HOME=/var/tmp/podman-%U/xdg-data -Environment=XDG_CONFIG_HOME=/var/tmp/podman-%U/xdg-config -ExecStart=/usr/bin/podman --root=${PODMAN_ROOT} --runroot=${PODMAN_RUNROOT} --tmpdir=${PODMAN_TMPDIR} --events-backend=file \ - run --rm --name registry \ - -p 127.0.0.1:5000:5000 \ - --read-only --tmpfs /tmp:size=64m --cap-drop=ALL --security-opt=no-new-privileges \ - -e REGISTRY_HTTP_ADDR=0.0.0.0:5000 \ - -e REGISTRY_STORAGE_DELETE_ENABLED=false \ - -v /var/lib/registry:/var/lib/registry:U,z \ - docker.io/library/registry@sha256:8be26f81ffea54106bae012c6f349df70f4d5e7e2ec01b143c46e2c03b9e551d -ExecStop=/usr/bin/podman --root=${PODMAN_ROOT} --runroot=${PODMAN_RUNROOT} --tmpdir=${PODMAN_TMPDIR} stop -t 10 registry -Restart=on-failure -MemoryMax=1G -CPUQuota=100% - -[Install] -WantedBy=default.target -EOF -``` - -#### 2.4.2 TLS Reverse Proxy Service - -```bash -# Install systemd system service for TLS reverse proxy -sudo tee /etc/systemd/system/registry-proxy.service > /dev/null << 'EOF' -[Unit] -Description=TLS proxy for Docker Registry (443 pulls / 4443 pushes) -After=network-online.target -Wants=network-online.target - -[Service] -User=registry-proxy -Group=registry-proxy -UMask=0077 -RuntimeDirectory=registry-proxy -LogsDirectory=registry-proxy -ReadWritePaths=/run/registry-proxy /var/log/registry-proxy -AmbientCapabilities=CAP_NET_BIND_SERVICE -CapabilityBoundingSet=CAP_NET_BIND_SERVICE -NoNewPrivileges=yes -PrivateTmp=yes -ProtectSystem=strict -ProtectHome=yes -ProtectKernelTunables=yes -ProtectKernelModules=yes -ProtectControlGroups=yes -ProtectClock=yes -ProtectHostname=yes -LockPersonality=yes -MemoryDenyWriteExecute=yes -RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 -RestrictSUIDSGID=yes -RemoveIPC=yes -ProtectProc=invisible -ProcSubset=pid - -# Allow loopback for upstream to registry and allow the server's public IP(s) for binds: -IPAddressDeny=any -IPAddressAllow=127.0.0.1 -IPAddressAllow= -# If using IPv6, uncomment and set: -# IPAddressAllow= -LimitNOFILE=65536 -ExecStartPre=/usr/sbin/nginx -t -c /etc/registry/nginx.conf -ExecStart=/usr/sbin/nginx -g 'daemon off;' -c /etc/registry/nginx.conf -Restart=on-failure - -[Install] -WantedBy=multi-user.target -EOF -``` - -### 2.5 Create Nginx Configuration - -```bash -# Create nginx configuration for TLS reverse proxy -sudo tee /etc/registry/nginx.conf > /dev/null << 'EOF' -worker_processes auto; -events { worker_connections 1024; } - -pid /run/registry-proxy/nginx.pid; -access_log /var/log/registry-proxy/access.log; -error_log /var/log/registry-proxy/error.log; - -http { - server_tokens off; - - # Rate/connection limits (tune for CI bursts as needed) - limit_req_zone $binary_remote_addr zone=reg_read:10m rate=10r/s; - limit_req_zone $binary_remote_addr zone=reg_write:10m rate=5r/s; - limit_conn_zone $binary_remote_addr zone=perip:10m; - - client_max_body_size 2g; - - # TLS hardening - ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:\ -ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:\ -ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'; - ssl_ecdh_curve X25519:P-256; - ssl_prefer_server_ciphers on; - ssl_verify_depth 2; - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 10m; - ssl_session_tickets off; - - # OCSP stapling is intentionally DISABLED for private CA deployments. - # (Clients trust via installed CA cert; no public OCSP endpoint exists.) - - # Proxy settings - proxy_http_version 1.1; - proxy_set_header Connection ""; - proxy_request_buffering off; - proxy_read_timeout 300s; - proxy_temp_path /run/registry-proxy/proxy_temp; - client_body_temp_path /run/registry-proxy/client_temp; - fastcgi_temp_path /run/registry-proxy/fastcgi_temp; - uwsgi_temp_path /run/registry-proxy/uwsgi_temp; - scgi_temp_path /run/registry-proxy/scgi_temp; - - upstream reg { server 127.0.0.1:5000; } - - # 443: unauthenticated pulls only - server { - listen :443 ssl http2; - # If IPv6, also: - # listen []:443 ssl http2; - ssl_certificate /etc/registry/certs/registry.crt; - ssl_certificate_key /etc/registry/certs/private/registry.key; - ssl_protocols TLSv1.2 TLSv1.3; - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - - # Hide catalog & tag listings - location = /v2/_catalog { return 403; } - location ~ ^/v2/.+/tags/list { return 403; } - - location /v2/ { - limit_req zone=reg_read burst=20 nodelay; - limit_conn perip 20; - proxy_pass http://reg; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto https; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - limit_except GET HEAD { return 403; } - add_header Docker-Distribution-Api-Version "registry/2.0" always; - } - } - - # 4443: authenticated pushes only (mTLS) - server { - listen :4443 ssl http2; - # If IPv6, also: - # listen []:4443 ssl http2; - ssl_certificate /etc/registry/certs/registry.crt; - ssl_certificate_key /etc/registry/certs/private/registry.key; - ssl_protocols TLSv1.2 TLSv1.3; - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - - # mTLS client auth via private Client CA - ssl_client_certificate /etc/registry/certs/clients/ca.crt; - ssl_verify_client on; - - # Optional: enable client-cert revocation (publish a CRL and uncomment) - # ssl_crl /etc/registry/certs/clients/ca.crl; - - location /v2/ { - limit_req zone=reg_write burst=10; - limit_conn perip 20; - proxy_pass http://reg; - proxy_set_header Host $host:$server_port; - proxy_set_header X-Forwarded-Proto https; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - add_header Docker-Distribution-Api-Version "registry/2.0" always; - } - } -} -EOF - -# Set proper permissions for nginx config (root-owned) -sudo chown root:root /etc/registry/nginx.conf -sudo chmod 644 /etc/registry/nginx.conf - -**Note (Private CA):** OCSP stapling is disabled because our CA is private (no public OCSP responder). Clients trust the registry via the installed CA certificate. If migrating to a public CA later, enable stapling and add `resolver` + `ssl_trusted_certificate`. -``` - -### 2.6 Install Container Policy - -```bash -# Install container policy file (root-owned) -sudo cp /opt/APP_NAME/registry/containers-policy.json /etc/containers/policy.json -sudo chown root:root /etc/containers/policy.json -sudo chmod 644 /etc/containers/policy.json -``` - -## Step 3: Generate TLS Certificates - -### 3.1 Generate Server and Client CA Certificates - -```bash -# 1. Generate server CA and certificates with proper FHS-compliant structure -cd /etc/registry/certs - -# Generate server CA private key in private subdirectory -sudo openssl genrsa -out private/ca.key 4096 - -# Generate server CA certificate in ca subdirectory -sudo openssl req -new -x509 -key private/ca.key \ - -out ca/ca.crt \ - -days 365 \ - -subj "/O=YOUR_ORGANIZATION/CN=APP_NAME-Registry-CA" - -# Generate server private key in private subdirectory -sudo openssl genrsa -out private/registry.key 4096 - -# Copy and use the project's OpenSSL configuration file -sudo cp /opt/APP_NAME/registry/openssl.conf /etc/registry/certs/requests/ -sudo chown root:root /etc/registry/certs/requests/openssl.conf - -# Ensure OpenSSL config includes subjectAltName for REGISTRY_HOST and SERVER_IP -# The config should contain: -# [ req_ext ] -# subjectAltName = DNS:REGISTRY_HOST,IP: -# If IPv6: -# subjectAltName = DNS:REGISTRY_HOST,IP:,IP: - -# Generate server certificate signing request in requests subdirectory -sudo openssl req -new -key private/registry.key \ - -out requests/registry.csr \ - -config requests/openssl.conf - -# Sign server certificate with CA -sudo openssl x509 -req -in requests/registry.csr \ - -CA ca/ca.crt -CAkey private/ca.key -CAcreateserial \ - -out registry.crt \ - -days 365 \ - -extensions req_ext \ - -extfile requests/openssl.conf - -# 2. Generate client CA for mTLS authentication -# Generate client CA private key -sudo openssl genrsa -out private/client-ca.key 4096 - -# Generate client CA certificate -sudo openssl req -new -x509 -key private/client-ca.key \ - -out clients/ca.crt \ - -days 365 \ - -subj "/O=YOUR_ORGANIZATION/CN=APP_NAME-Client-CA" - -# Generate client certificate for CI operations -sudo openssl genrsa -out private/client.key 4096 - -# Generate client certificate signing request -sudo openssl req -new -key private/client.key \ - -out requests/client.csr \ - -subj "/O=YOUR_ORGANIZATION/CN=APP_NAME-CI-Client" - -# Sign client certificate with client CA -sudo openssl x509 -req -in requests/client.csr \ - -CA clients/ca.crt -CAkey private/client-ca.key -CAcreateserial \ - -out clients/client.crt \ - -days 365 - -# Set proper FHS-compliant permissions -sudo chmod 600 private/ca.key private/client-ca.key private/client.key # Private keys - owner read/write only -sudo chmod 644 ca/ca.crt registry.crt clients/client.crt # Certificates - world readable -sudo chmod 644 requests/registry.csr requests/client.csr requests/openssl.conf # Requests - world readable - -# Set registry key and client CA permissions for nginx proxy access -sudo chgrp registry-proxy /etc/registry/certs/private/registry.key /etc/registry/certs/clients/ca.crt -sudo chmod 640 /etc/registry/certs/private/registry.key /etc/registry/certs/clients/ca.crt - -# Allow traversal into the private key directory for the proxy -sudo chgrp registry-proxy /etc/registry/certs/private -sudo chmod 750 /etc/registry/certs/private - -# Ensure parent directory is traversable -sudo chmod 755 /etc/registry/certs - -# Verify certificate creation -sudo openssl x509 -in /etc/registry/certs/registry.crt -text -noout | grep -E "(Subject:|DNS:|IP Address:)" - -# Reminder: clients must use exactly REGISTRY_HOST (or those IPs) in pulls/pushes - -# 3. Install server CA certificate in system trust store (for curl, wget, etc.) -sudo cp /etc/registry/certs/ca/ca.crt /usr/local/share/ca-certificates/registry-ca.crt - -# 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 (pinned + verified) -# Vars -COSIGN_VERSION=v2.5.3 # Replace with the latest stable release version -BASE="https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}" -FILE="cosign-linux-amd64" - -cd /tmp - -# 1) Download binary + vendor checksums -curl -fsSLO "${BASE}/${FILE}" -curl -fsSLO "${BASE}/cosign_checksums.txt" - -# 2) Verify SHA256 for the exact artifact -grep " ${FILE}$" cosign_checksums.txt | sha256sum -c - - -# Expect: 'cosign-linux-amd64: OK' - -# (If your 'cosign_checksums.txt' has CRLFs and the check fails, do: -# sed -i 's/\r$//' cosign_checksums.txt -# and run the check again.) - -# 3) Install -sudo install -m 0755 "/tmp/${FILE}" /usr/local/bin/cosign - -# 4) Sanity check -cosign version --json - -# 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 - -### 4.1 Configure Firewall - -```bash -# Open port 443 for unauthenticated pulls (public access) -sudo ufw allow 443/tcp - -# Open port 4443 for authenticated pushes (restrict to known CIDRs) -# Example: restrict to specific networks (adjust CIDRs as needed) -sudo ufw delete allow 4443/tcp || true -sudo ufw allow from 10.0.0.0/8 to any port 4443 proto tcp -sudo ufw allow from 172.16.0.0/12 to any port 4443 proto tcp -sudo ufw allow from 192.168.0.0/16 to any port 4443 proto tcp - -# For public access to 4443 (less secure), use: -# sudo ufw allow 4443/tcp - -# Optional: Consider SystemCallFilter for additional hardening -# (validate exact syscall set for your distro's nginx build) -# SystemCallFilter=@system-service - -# Note: Port 5000 is NOT opened - registry runs loopback-only - -## Client Trust Configuration - -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):** -```bash -# On client systems - use the actual FQDN/IP from your certificates -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. The Cosign public key is required for signature verification on both ports. - -**Best practice:** pull/deploy by **digest**, not tag. Example: -`podman pull REGISTRY_HOST/namespace/image@sha256:` - -**Security hardening notes:** -- `:U` flag: ID-maps the host directory into the container's user namespace to avoid permission drift and tighten isolation. -- `ssl_session_tickets off`: Avoids long-lived TLS ticket key reuse unless you manage ticket key rotation. -- **Client policy enforcement**: The registry doesn't enforce signatures—clients do. Ensure `containers-policy.json` (with `sigstoreSigned` for `REGISTRY_HOST` and reject for `docker.io`) is deployed on every *pulling* host. - -## 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 - -```bash -# Reload systemd and start services -sudo systemctl daemon-reload - -# Start as the service user -sudo -u CI_SERVICE_USER sh -lc 'systemctl --user daemon-reload && systemctl --user enable --now registry.service' -sudo systemctl enable --now registry-proxy.service - -# One-time: ensure host dir ownership matches rootless ID map -sudo -u CI_SERVICE_USER podman unshare chown -R 100000:100000 /var/lib/registry -``` - -### 4.1 Self-test - -```bash -# 1) Listening sockets (should show nginx on the chosen IPs/ports): -sudo ss -ltnp '( sport = :443 or sport = :4443 )' - -# 2) From another host on an allowed network: -curl -vk https://REGISTRY_HOST/v2/ | grep -i Docker-Distribution-Api-Version # expect 200 + header -curl -vk https://REGISTRY_HOST/v2/_catalog # expect 403 (blocked) -curl -vk --cert client.crt --key client.key https://REGISTRY_HOST:4443/v2/ | \ - grep -i Docker-Distribution-Api-Version # expect 200 + header - -# 3) Signature enforcement (on a client with CA + org-cosign.pub + policy.json): -# - Pull unsigned image from your registry -> should FAIL -# - Sign image with your org key, push via 4443, pull via 443 -> should SUCCEED -``` - -## Step 5: Verify Installation - -### 5.1 Check Service Status - -```bash -# Check that the services are running properly -sudo -u CI_SERVICE_USER -H sh -lc 'systemctl --user status registry.service' -sudo systemctl status registry-proxy.service - -# Check that registry container is running -sudo su - CI_SERVICE_USER -c "env PODMAN_ROOT=/var/tmp/podman-\$(id -u)/root PODMAN_RUNROOT=/run/user/\$(id -u)/podman-run PODMAN_TMPDIR=/var/tmp/podman-\$(id -u)/tmp XDG_DATA_HOME=/var/tmp/podman-\$(id -u)/xdg-data XDG_CONFIG_HOME=/var/tmp/podman-\$(id -u)/xdg-config podman ps" - -# Check registry logs -sudo su - CI_SERVICE_USER -c "env PODMAN_ROOT=/var/tmp/podman-\$(id -u)/root PODMAN_RUNROOT=/run/user/\$(id -u)/podman-run PODMAN_TMPDIR=/var/tmp/podman-\$(id -u)/tmp XDG_DATA_HOME=/var/tmp/podman-\$(id -u)/xdg-data XDG_CONFIG_HOME=/var/tmp/podman-\$(id -u)/xdg-config podman logs registry" - -# Check nginx proxy logs -sudo journalctl -u registry-proxy.service -f --no-pager -n 50 - -# Quick validation of hardening -echo "=== Proxy Runtime & Logs ===" -systemctl status registry-proxy.service -ls -ld /run/registry-proxy /var/log/registry-proxy - -echo "=== Nginx Logs ===" -tail -n1 /var/log/registry-proxy/error.log /var/log/registry-proxy/access.log - -echo "=== Port Bindings ===" -ss -ltnp | egrep ':(443|4443)'; ss -ltnp | grep '127.0.0.1:5000' - -echo "=== Security Tests ===" -# 443 forbids writes -curl -k -X PUT https://YOUR_ACTUAL_IP_ADDRESS/v2/ -I | grep 403 || echo "WARNING: 443 allows writes" - -# 4443 requires client cert -curl -kI https://YOUR_ACTUAL_IP_ADDRESS:4443/v2/ | egrep '400|401|403' || echo "WARNING: 4443 may not require auth" - -# 443 should return 200 with registry API header; catalog is forbidden -curl -vk https://REGISTRY_HOST/v2/ | grep -i Docker-Distribution-Api-Version -curl -vk https://REGISTRY_HOST/v2/_catalog # expect 403 - -# 4443 requires mTLS, should return 200 with client certs -curl -vk --cert client.crt --key client.key https://REGISTRY_HOST:4443/v2/ | grep -i Docker-Distribution-Api-Version - -# OCSP stapling is intentionally disabled; you should not expect stapling-related headers in responses. - -# Verify Podman is using non-home paths -sudo su - CI_SERVICE_USER -c "env PODMAN_ROOT=/var/tmp/podman-\$(id -u)/root PODMAN_RUNROOT=/run/user/\$(id -u)/podman-run PODMAN_TMPDIR=/var/tmp/podman-\$(id -u)/tmp XDG_DATA_HOME=/var/tmp/podman-\$(id -u)/xdg-data XDG_CONFIG_HOME=/var/tmp/podman-\$(id -u)/xdg-config podman info --format '{{.Store.GraphRoot}} {{.Store.RunRoot}}'" -``` - -### 5.2 Test Registry Functionality - -#### 5.2.1 Test mTLS Authentication - -```bash -# Switch to CI_SERVICE_USER for testing (CI_SERVICE_USER runs CI pipeline and Podman operations) -sudo su - CI_SERVICE_USER - -# Navigate to the application directory -cd /opt/APP_NAME - -# Configure Podman to use client certificates for mTLS authentication -mkdir -p ~/.config/containers -cat > ~/.config/containers/registries.conf << EOF -[[registry]] -location = "YOUR_ACTUAL_IP_ADDRESS:4443" -client_cert = "/etc/registry/certs/clients/client.crt" -client_key = "/etc/registry/certs/private/client.key" -EOF - -# Test authenticated push using mTLS (port 4443) -echo "FROM alpine:latest" > /tmp/test.Dockerfile -env PODMAN_ROOT=/var/tmp/podman-$(id -u)/root PODMAN_RUNROOT=/run/user/$(id -u)/podman-run PODMAN_TMPDIR=/var/tmp/podman-$(id -u)/tmp XDG_DATA_HOME=/var/tmp/podman-$(id -u)/xdg-data XDG_CONFIG_HOME=/var/tmp/podman-$(id -u)/xdg-config podman build -f /tmp/test.Dockerfile -t YOUR_ACTUAL_IP_ADDRESS:4443/APP_NAME/test:latest /tmp -env PODMAN_ROOT=/var/tmp/podman-$(id -u)/root PODMAN_RUNROOT=/run/user/$(id -u)/podman-run PODMAN_TMPDIR=/var/tmp/podman-$(id -u)/tmp XDG_DATA_HOME=/var/tmp/podman-$(id -u)/xdg-data XDG_CONFIG_HOME=/var/tmp/podman-$(id -u)/xdg-config podman push YOUR_ACTUAL_IP_ADDRESS:4443/APP_NAME/test:latest - -# Test unauthenticated pull from standard HTTPS endpoint (port 443) -env PODMAN_ROOT=/var/tmp/podman-$(id -u)/root PODMAN_RUNROOT=/run/user/$(id -u)/podman-run PODMAN_TMPDIR=/var/tmp/podman-$(id -u)/tmp XDG_DATA_HOME=/var/tmp/podman-$(id -u)/xdg-data XDG_CONFIG_HOME=/var/tmp/podman-$(id -u)/xdg-config podman pull YOUR_ACTUAL_IP_ADDRESS/APP_NAME/test:latest - -# Test that unauthorized push to port 443 is blocked -echo "FROM alpine:latest" > /tmp/unauthorized.Dockerfile -env PODMAN_ROOT=/var/tmp/podman-$(id -u)/root PODMAN_RUNROOT=/run/user/$(id -u)/podman-run PODMAN_TMPDIR=/var/tmp/podman-$(id -u)/tmp XDG_DATA_HOME=/var/tmp/podman-$(id -u)/xdg-data XDG_CONFIG_HOME=/var/tmp/podman-$(id -u)/xdg-config podman build -f /tmp/unauthorized.Dockerfile -t YOUR_ACTUAL_IP_ADDRESS/APP_NAME/unauthorized:latest /tmp -env PODMAN_ROOT=/var/tmp/podman-$(id -u)/root PODMAN_RUNROOT=/run/user/$(id -u)/podman-run PODMAN_TMPDIR=/var/tmp/podman-$(id -u)/tmp XDG_DATA_HOME=/var/tmp/podman-$(id -u)/xdg-data XDG_CONFIG_HOME=/var/tmp/podman-$(id -u)/xdg-config podman push YOUR_ACTUAL_IP_ADDRESS/APP_NAME/unauthorized:latest -# Expected: This should fail with 403 Forbidden - -# Clean up -env PODMAN_ROOT=/var/tmp/podman-$(id -u)/root PODMAN_RUNROOT=/run/user/$(id -u)/podman-run PODMAN_TMPDIR=/var/tmp/podman-$(id -u)/tmp XDG_DATA_HOME=/var/tmp/podman-$(id -u)/xdg-data XDG_CONFIG_HOME=/var/tmp/podman-$(id -u)/xdg-config podman rmi YOUR_ACTUAL_IP_ADDRESS:4443/APP_NAME/test:latest 2>/dev/null || true -env PODMAN_ROOT=/var/tmp/podman-$(id -u)/root PODMAN_RUNROOT=/run/user/$(id -u)/podman-run PODMAN_TMPDIR=/var/tmp/podman-$(id -u)/tmp XDG_DATA_HOME=/var/tmp/podman-$(id -u)/xdg-data XDG_CONFIG_HOME=/var/tmp/podman-$(id -u)/xdg-config podman rmi YOUR_ACTUAL_IP_ADDRESS/APP_NAME/test:latest 2>/dev/null || true -env PODMAN_ROOT=/var/tmp/podman-$(id -u)/root PODMAN_RUNROOT=/run/user/$(id -u)/podman-run PODMAN_TMPDIR=/var/tmp/podman-$(id -u)/tmp XDG_DATA_HOME=/var/tmp/podman-$(id -u)/xdg-data XDG_CONFIG_HOME=/var/tmp/podman-$(id -u)/xdg-config podman rmi YOUR_ACTUAL_IP_ADDRESS/APP_NAME/unauthorized:latest 2>/dev/null || true -exit -``` - -**Expected behavior**: -- ✅ Push requires mTLS client certificate authentication on port 4443 -- ✅ Pull works without authentication (public read access) on port 443 -- ✅ Unauthorized push is blocked on port 443 (403 Forbidden) -- ✅ Registry accessible at `https://YOUR_ACTUAL_IP_ADDRESS:4443` for authenticated operations (mTLS) -- ✅ Registry accessible at `https://YOUR_ACTUAL_IP_ADDRESS` for unauthenticated pulls -- ✅ Proper FHS-compliant certificate structure with secure permissions -- ✅ Port 443 allows unauthenticated Docker Registry v2 pulls; Port 4443 is for authenticated pushes (mTLS) - -### 5.3 Troubleshooting TLS Errors - -If you get a TLS error like `remote error: tls: internal error` when using self-signed certificates, verify the certificate installation and configuration: - -```bash -# Verify the certificate was installed correctly in system trust store -ls -la /usr/local/share/ca-certificates/registry-ca.crt - -# Verify certificate chain is valid -openssl verify -CAfile /etc/registry/certs/ca/ca.crt /etc/registry/certs/registry.crt - -# Test the certificate connection -openssl s_client -connect YOUR_ACTUAL_IP_ADDRESS:4443 -servername YOUR_ACTUAL_IP_ADDRESS < /dev/null - -# Test mTLS connection with client certificate -openssl s_client -connect YOUR_ACTUAL_IP_ADDRESS:4443 -servername YOUR_ACTUAL_IP_ADDRESS \ - -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 - -# If issues persist, restart the services to reload certificates -sudo -u CI_SERVICE_USER -H sh -lc 'systemctl --user restart registry.service' -sudo systemctl restart registry-proxy.service - -# Wait for services to restart, then test again -sleep 10 - -# Test mTLS authentication -env PODMAN_ROOT=/var/tmp/podman-$(id -u)/root PODMAN_RUNROOT=/run/user/$(id -u)/podman-run PODMAN_TMPDIR=/var/tmp/podman-$(id -u)/tmp XDG_DATA_HOME=/var/tmp/podman-$(id -u)/xdg-data XDG_CONFIG_HOME=/var/tmp/podman-$(id -u)/xdg-config podman pull YOUR_ACTUAL_IP_ADDRESS:4443/APP_NAME/test:latest -``` - -## Certificate Structure Summary - -The project uses a two-port configuration: -- **Port 443**: Unauthenticated pulls (public read access) -- **Port 4443**: Authenticated pushes (mTLS client certificate required) - -**FHS-Compliant Certificate Locations:** -- **Private Keys**: `/etc/registry/certs/private/` (mode 600) -- **CA Certificates**: `/etc/registry/certs/ca/` (mode 644) -- **Client CA**: `/etc/registry/certs/clients/` (mode 640, readable by registry-proxy) -- **Certificate Requests**: `/etc/registry/certs/requests/` (mode 644) -- **Server Certificates**: `/etc/registry/certs/` (mode 644) -- **System Trust Store**: `/usr/local/share/ca-certificates/registry-ca.crt` - -## Security Notes - -**Certificate Security**: The certificate files in `/etc/registry/certs/` contain sensitive authentication data and should be: -- **Backed up securely** if needed for disaster recovery -- **Never committed to version control** -- **Protected with proper permissions** (600 for private keys, 640 for client CA) -- **Rotated regularly** by regenerating certificates and updating client configurations - -**Files to Preserve**: DO NOT remove these files as they are needed for operation: -- `/opt/APP_NAME/registry/containers-policy.json` -- `/etc/registry/certs/` (contains all certificates and keys) -- `/etc/systemd/user/registry.service` -- `/etc/systemd/system/registry-proxy.service` -- `/etc/registry/nginx.conf` - -## Architecture Benefits - -1. **🔒 Enhanced Security**: Keys stay on host, minimal capabilities, strict isolation -2. **🛡️ mTLS Authentication**: Stronger than basic auth for push operations -3. **🚫 No Resource Contention**: Registry isolated from other operations -4. **📁 No $HOME I/O**: All state outside user home directory -5. **🔧 Robust Boot**: User manager guaranteed to exist at boot time -6. **🎯 Clear Separation**: Unauthenticated pulls vs authenticated pushes -7. **⚡ Low-Port Binding**: Via minimal capability (`CAP_NET_BIND_SERVICE`) -8. **🛡️ Comprehensive Hardening**: Multiple security layers and restrictions - -## 🎉 Congratulations! - -You have successfully set up a rootless Docker Registry v2 with host TLS reverse proxy featuring: - -- ✅ **Rootless registry** via systemd user manager -- ✅ **Host TLS reverse proxy** with minimal capabilities -- ✅ **mTLS authentication** for secure push operations -- ✅ **Unauthenticated pulls** for public read access -- ✅ **FHS-compliant directory structure** for better organization and security -- ✅ **No $HOME I/O** policy with all state outside user home directory -- ✅ **Port 443 allows unauthenticated Docker Registry v2 pulls; Port 4443 is for authenticated pushes (mTLS)** -- ✅ **Comprehensive security hardening** with multiple protection layers - -Your Docker Registry is now ready for secure image storage with proper authentication and isolation! diff --git a/README.md b/README.md index aa04271..45c9d51 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,27 @@ Configuration files are located in `backend/config/`: ### Frontend Configuration The frontend is configured to connect to the backend at `http://127.0.0.1:3000` by default. Update `frontend/src/lib/api.ts` if your backend is running on a different address. +## Container Registry + +This project uses **Forgejo's built-in container registry** for CI/CD. Images are built and pushed to the registry during the CI pipeline and can be pulled anonymously for deployment. + +### Prerequisites +- Forgejo Container Registry (Packages) enabled on your instance +- Public repository/org for anonymous pulls +- PAT with `write:packages` scope for CI pushes + +### Pulling Images +```bash +# Pull by tag +podman pull REGISTRY_HOST/owner/repo/backend:TAG +podman pull REGISTRY_HOST/owner/repo/frontend:TAG + +# Pull by digest +podman pull REGISTRY_HOST/owner/repo/backend@sha256:abc123... +``` + +For detailed setup instructions, see the [CI/CD Pipeline Setup Guide](CI_CD_PIPELINE_SETUP_GUIDE.md#forgejo-container-registry-setup). + ## License This project is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License - see the [LICENSE.md](LICENSE.md) file for details. diff --git a/backend/Dockerfile.test-postgres b/backend/Dockerfile.test-postgres index 0bf1912..2f56fc2 100644 --- a/backend/Dockerfile.test-postgres +++ b/backend/Dockerfile.test-postgres @@ -1,7 +1,7 @@ # PostgreSQL testing environment for CI/CD -ARG CI_HOST=localhost -ARG APP_NAME=sharenet -FROM ${CI_HOST}:443/${APP_NAME}/postgres:15-alpine +ARG REGISTRY_HOST=localhost +ARG OWNER_REPO=owner/repo +FROM ${REGISTRY_HOST}/${OWNER_REPO}/postgres:15-alpine # Install additional tools if needed RUN apk add --no-cache curl diff --git a/backend/Dockerfile.test-rust b/backend/Dockerfile.test-rust index d6cc901..e7efda7 100644 --- a/backend/Dockerfile.test-rust +++ b/backend/Dockerfile.test-rust @@ -1,7 +1,7 @@ # Rust testing environment for CI/CD -ARG CI_HOST=localhost -ARG APP_NAME=sharenet -FROM ${CI_HOST}:443/${APP_NAME}/rust:1.75-slim +ARG REGISTRY_HOST=localhost +ARG OWNER_REPO=owner/repo +FROM ${REGISTRY_HOST}/${OWNER_REPO}/rust:1.75-slim # Install additional tools needed for testing RUN apt-get update && apt-get install -y \ diff --git a/test-pod.yaml b/ci-pod.yaml similarity index 100% rename from test-pod.yaml rename to ci-pod.yaml diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index 2a9fddb..0000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -1,84 +0,0 @@ -version: '3.8' - -services: - postgres: - image: postgres:15-alpine - container_name: sharenet-postgres - restart: unless-stopped - environment: - POSTGRES_DB: ${POSTGRES_DB:-sharenet} - POSTGRES_USER: ${POSTGRES_USER:-sharenet} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme} - volumes: - - postgres_data:/var/lib/postgresql/data - ports: - - "5432:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-sharenet}"] - interval: 30s - timeout: 10s - retries: 3 - networks: - - sharenet-network - - backend: - image: ${REGISTRY}/${IMAGE_NAME:-sharenet}/backend:${IMAGE_TAG:-latest} - container_name: sharenet-backend - restart: unless-stopped - environment: - DATABASE_URL: postgresql://${POSTGRES_USER:-sharenet}:${POSTGRES_PASSWORD:-changeme}@postgres:5432/${POSTGRES_DB:-sharenet} - RUST_LOG: info - RUST_BACKTRACE: 1 - ports: - - "3001:3001" - depends_on: - postgres: - condition: service_healthy - healthcheck: - test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1"] - interval: 30s - timeout: 10s - retries: 3 - networks: - - sharenet-network - - frontend: - image: ${REGISTRY}/${IMAGE_NAME:-sharenet}/frontend:${IMAGE_TAG:-latest} - container_name: sharenet-frontend - restart: unless-stopped - environment: - NEXT_PUBLIC_API_HOST: backend - NEXT_PUBLIC_API_PORT: 3001 - NODE_ENV: production - ports: - - "3000:3000" - depends_on: - backend: - condition: service_healthy - networks: - - sharenet-network - - nginx: - image: nginx:alpine - container_name: sharenet-nginx - restart: unless-stopped - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - # SSL certificates directory (optional - create nginx/ssl/ for SSL support) - - ./nginx/ssl:/etc/nginx/ssl:ro - depends_on: - - frontend - - backend - networks: - - sharenet-network - -volumes: - postgres_data: - driver: local - -networks: - sharenet-network: - driver: bridge \ No newline at end of file diff --git a/docker-compose.test.yml b/docker-compose.test.yml deleted file mode 100644 index 7b357a2..0000000 --- a/docker-compose.test.yml +++ /dev/null @@ -1,70 +0,0 @@ -version: '3.8' - -services: - # PostgreSQL for testing - postgres: - build: - context: ./backend - dockerfile: Dockerfile.test-postgres - args: - CI_HOST: ${CI_HOST:-localhost} - APP_NAME: ${APP_NAME:-sharenet} - container_name: ci-cd-test-postgres - restart: unless-stopped - environment: - POSTGRES_DB: sharenet_test - POSTGRES_USER: postgres - POSTGRES_PASSWORD: password - ports: - - "5432:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - networks: - - ci-cd-test-network - - # Rust toolchain container for backend testing - rust-toolchain: - build: - context: ./backend - dockerfile: Dockerfile.test-rust - args: - CI_HOST: ${CI_HOST:-localhost} - APP_NAME: ${APP_NAME:-sharenet} - container_name: ci-cd-test-rust - restart: unless-stopped - volumes: - - /workspace/backend:/workspace/backend - working_dir: /workspace/backend - depends_on: - postgres: - condition: service_healthy - networks: - - ci-cd-test-network - command: sleep infinity - - # Node.js toolchain container for frontend testing - node-toolchain: - build: - context: ./frontend - dockerfile: Dockerfile.test-node - args: - CI_HOST: ${CI_HOST:-localhost} - APP_NAME: ${APP_NAME:-sharenet} - container_name: ci-cd-test-node - restart: unless-stopped - volumes: - - /workspace/frontend:/workspace/frontend - working_dir: /workspace/frontend - depends_on: - postgres: - condition: service_healthy - networks: - - ci-cd-test-network - command: sleep infinity - -networks: - ci-cd-test-network: - driver: bridge \ No newline at end of file diff --git a/frontend/Dockerfile.test-node b/frontend/Dockerfile.test-node index f0c868a..046eb17 100644 --- a/frontend/Dockerfile.test-node +++ b/frontend/Dockerfile.test-node @@ -1,7 +1,7 @@ # Node.js testing environment for CI/CD -ARG CI_HOST=localhost -ARG APP_NAME=sharenet -FROM ${CI_HOST}:443/${APP_NAME}/node:20-slim +ARG REGISTRY_HOST=localhost +ARG OWNER_REPO=owner/repo +FROM ${REGISTRY_HOST}/${OWNER_REPO}/node:20-slim # Install additional tools needed for testing RUN apt-get update && apt-get install -y \ diff --git a/production-pod.yaml b/prod-pod.yaml similarity index 90% rename from production-pod.yaml rename to prod-pod.yaml index d6a9280..cc25191 100644 --- a/production-pod.yaml +++ b/prod-pod.yaml @@ -14,7 +14,7 @@ spec: - name: POSTGRES_USER value: "sharenet" - name: POSTGRES_PASSWORD - value: "changeme" + value: "${POSTGRES_PASSWORD}" ports: - containerPort: 5432 protocol: TCP @@ -42,10 +42,10 @@ spec: timeoutSeconds: 5 - name: backend - image: localhost:4443/sharenet/backend:latest + image: ${REGISTRY_HOST}/${OWNER_REPO}/backend:${IMAGE_TAG} env: - name: DATABASE_URL - value: "postgresql://sharenet:changeme@localhost:5432/sharenet" + value: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}" - name: RUST_LOG value: "info" - name: RUST_BACKTRACE @@ -70,7 +70,7 @@ spec: timeoutSeconds: 5 - name: frontend - image: localhost:4443/sharenet/frontend:latest + image: ${REGISTRY_HOST}/${OWNER_REPO}/frontend:${IMAGE_TAG} env: - name: NEXT_PUBLIC_API_HOST value: "localhost" diff --git a/registry/README.md b/registry/README.md deleted file mode 100644 index b1c8f63..0000000 --- a/registry/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# Docker Registry Configuration - -This folder contains the configuration files for the Docker Registry setup used in the CI/CD pipeline. - -## Files - - -- **`nginx.conf`**: nginx configuration for HTTPS and authentication -- **`docker-registry.service`**: Systemd service file for Docker Registry v2 -- **`README.md`**: This documentation file - -## Architecture - -The registry setup uses: -- **Docker Registry**: Basic registry for storing Docker images -- **nginx**: Reverse proxy with automatic HTTPS and authentication -- **Environment Variables**: For authentication credentials and registry configuration -- **Service User**: The registry and nginx services run as the existing `CI_SERVICE_USER` (not a separate registry user) - -## Authentication Model - -- **Pulls**: Unauthenticated (public read access) - - `/v2/*/blobs/*` - Download image layers - - `/v2/*/manifests/*` - Download image manifests - - `/v2/_catalog` - List repositories - - `/v2/*/tags/list` - List image tags -- **Pushes**: Require authentication with `registry-user` credentials - - `/v2/*/blobs/uploads/*` - Upload image layers - - `/v2/*/manifests/*` (PUT/POST/PATCH/DELETE) - Upload/update manifests - -## Security Features - -- **URL-based access control**: Different paths require different authentication levels -- **Method-based restrictions**: Push operations 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 -- **Rootless Podman**: All state stored outside home directory for complete isolation - -## Configuration - -The setup is configured through: -1. **nginx.conf**: Handles HTTPS and authentication -2. **registry-pod.yaml**: Kubernetes-style pod definition for Podman -3. **docker-registry.service**: Systemd service with rootless Podman configuration -4. **User/Permissions**: All files and services are owned and run by `CI_SERVICE_USER` for consistency and security - -## Podman Configuration - -The registry uses rootless Podman with all state stored outside the user's home directory: - -- **PODMAN_ROOT**: `/var/tmp/podman-%u/root` - Container storage -- **PODMAN_RUNROOT**: `/run/user/%u/podman-run` - Runtime state -- **PODMAN_TMPDIR**: `/var/tmp/podman-%u/tmp` - Temporary files -- **XDG_DATA_HOME**: `/var/tmp/podman-%u/xdg-data` - Data directory -- **XDG_CONFIG_HOME**: `/var/tmp/podman-%u/xdg-config` - Configuration - -This ensures complete isolation from the user's home directory while maintaining rootless security. - -## Usage - -The registry is automatically set up during the CI/CD pipeline setup process. The configuration files are copied from this folder to the registry server and customized with the appropriate IP address and credentials. All files and running services should be owned by `CI_SERVICE_USER`. - -## Security Features - -### Container Security -- **Rootless operation**: Containers run as non-root user (UID 1000) -- **Capability dropping**: All Linux capabilities are dropped -- **Privilege escalation**: Disabled via allowPrivilegeEscalation=false -- **Read-only filesystem**: Root filesystem is read-only with tmpfs for /tmp -- **Image deletion disabled**: REGISTRY_STORAGE_DELETE_ENABLED=false - -### Network Security -- **TLS 1.2/1.3 only**: Modern cipher suites with HSTS headers -- **Rate limiting**: 10 requests/second for reads, 5 requests/second for writes -- **Client size limits**: 2GB max body size for large image uploads -- **Internal registry**: Registry listens only internally, proxied via nginx -- **Port restrictions**: Only ports 443 and 4443 exposed - -### Resource Management -- **CPU limits**: 1000m for registry, 500m for nginx -- **Memory limits**: 1Gi for registry, 512Mi for nginx -- **File descriptors**: Proper ulimits configuration - -### Authentication & Authorization -- **Basic auth**: htpasswd-based authentication for write operations -- **Policy enforcement**: containers-policy.json for image signature verification -- **Volume security**: Read-only mounts where possible with nosuid,nodev,noexec - -### Data Protection -- **FHS compliance**: Proper directory structure and permissions -- **Credential isolation**: htpasswd file stored separately with 600 permissions -- **Log management**: Structured logging with proper volume mounts \ No newline at end of file diff --git a/registry/containers-policy.json b/registry/containers-policy.json deleted file mode 100644 index fd3022d..0000000 --- a/registry/containers-policy.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "default": [{ "type": "reject" }], - "transports": { - "docker": { - "REGISTRY_HOST": [ - { - "type": "sigstoreSigned", - "keyPath": "/etc/containers/keys/org-cosign.pub", - "signedIdentity": { "type": "matchRepository" } - } - ], - "REGISTRY_HOST:4443": [ - { - "type": "sigstoreSigned", - "keyPath": "/etc/containers/keys/org-cosign.pub", - "signedIdentity": { "type": "matchRepository" } - } - ], - "docker.io": [{ "type": "reject" }] - }, - "docker-daemon": { "": [{ "type": "reject" }] } - } -} diff --git a/registry/docker-registry.service b/registry/docker-registry.service deleted file mode 100644 index 807e565..0000000 --- a/registry/docker-registry.service +++ /dev/null @@ -1,32 +0,0 @@ -[Unit] -Description=Docker Registry v2 with nginx Reverse Proxy -After=network.target - -[Service] -Type=oneshot -RemainAfterExit=yes -User=CI_SERVICE_USER -Group=CI_SERVICE_USER -WorkingDirectory=/opt/APP_NAME/registry - -# Podman rootless configuration - all state outside home -Environment=PODMAN_ROOT=/var/tmp/podman-%u/root -Environment=PODMAN_RUNROOT=/run/user/%u/podman-run -Environment=PODMAN_TMPDIR=/var/tmp/podman-%u/tmp -Environment=XDG_DATA_HOME=/var/tmp/podman-%u/xdg-data -Environment=XDG_CONFIG_HOME=/var/tmp/podman-%u/xdg-config - -ExecStart=/usr/bin/podman --root=${PODMAN_ROOT} --runroot=${PODMAN_RUNROOT} --tmpdir=${PODMAN_TMPDIR} --events-backend=file play kube registry-pod.yaml -ExecStop=/usr/bin/podman --root=${PODMAN_ROOT} --runroot=${PODMAN_RUNROOT} --tmpdir=${PODMAN_TMPDIR} --events-backend=file pod stop registry-pod -ExecReload=/usr/bin/podman --root=${PODMAN_ROOT} --runroot=${PODMAN_RUNROOT} --tmpdir=${PODMAN_TMPDIR} --events-backend=file pod restart registry-pod -TimeoutStartSec=0 - -# Security settings -NoNewPrivileges=true -PrivateTmp=true -ProtectSystem=strict -ProtectHome=true -ReadWritePaths=/opt/APP_NAME/registry /etc/registry /var/lib/registry /var/log/registry /var/tmp/podman-%u - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/registry/nginx.conf b/registry/nginx.conf deleted file mode 100644 index e53c63c..0000000 --- a/registry/nginx.conf +++ /dev/null @@ -1,220 +0,0 @@ -# Docker Registry v2 Nginx Configuration with Enhanced Security -# Port 443: Unauthenticated pulls (GET requests only) -# Port 4443: Authenticated operations (login, logout, push, delete, etc.) - -events { - worker_connections 1024; - use epoll; - multi_accept on; -} - -http { - # Rate limiting with different zones for different operations - limit_req_zone $binary_remote_addr zone=registry:10m rate=10r/s; - limit_req_zone $binary_remote_addr zone=push:10m rate=5r/s; - limit_req_zone $binary_remote_addr zone=login:10m rate=2r/s; - - # Connection limiting - limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m; - - # Upstream Docker Registry - upstream registry { - server localhost:5000; - keepalive 32; - # Health check - keepalive_requests 100; - keepalive_timeout 60s; - } - -# HTTP server for unauthenticated pulls on port 443 -server { - listen 443 ssl http2; - server_name _; - - # Connection limits - limit_conn conn_limit_per_ip 10; - - # SSL Configuration - TLS 1.2/1.3 only with modern ciphers - ssl_certificate /etc/registry/certs/registry.crt; - ssl_certificate_key /etc/registry/certs/private/registry.key; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; - ssl_prefer_server_ciphers off; - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 10m; - ssl_session_tickets off; - ssl_stapling on; - ssl_stapling_verify on; - - # Security headers with HSTS - add_header X-Frame-Options DENY always; - add_header X-Content-Type-Options nosniff always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; - add_header Content-Security-Policy "default-src 'none'; frame-ancestors 'none';" always; - add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; - - # Client max body size for large image uploads - client_max_body_size 2G; - client_body_timeout 60s; - client_header_timeout 60s; - - # Rate limiting for read operations - limit_req zone=registry burst=20 nodelay; - - # Block all write operations explicitly - if ($request_method !~ ^(GET|HEAD)$) { - return 405 "Method Not Allowed"; - } - - # Block suspicious user agents - if ($http_user_agent ~* (bot|crawler|spider|scraper)) { - return 403 "Forbidden"; - } - - # Allow all GET requests to v2 API (Docker Registry itself will handle security) - location /v2/ { - proxy_pass http://registry; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 900; - proxy_connect_timeout 60; - proxy_send_timeout 60; - proxy_buffering off; - proxy_request_buffering off; - - # Additional security headers for registry - proxy_hide_header X-Powered-By; - proxy_hide_header Server; - } - - location /health { - access_log off; - return 200 "healthy\n"; - add_header Content-Type text/plain; - add_header Cache-Control "no-cache, no-store, must-revalidate"; - } - - # Block access to hidden files - location ~ /\. { - deny all; - access_log off; - log_not_found off; - } - - # Block access to sensitive files - location ~* \.(htaccess|htpasswd|ini|log|sh|sql|conf)$ { - deny all; - access_log off; - log_not_found off; - } - - location / { - return 404; - } - - access_log /var/log/nginx/registry_access.log; - error_log /var/log/nginx/registry_error.log; -} - -# HTTPS server for authenticated operations on port 4443 -server { - listen 4443 ssl http2; - server_name _; - - # Connection limits - limit_conn conn_limit_per_ip 5; - - # SSL Configuration - TLS 1.2/1.3 only with modern ciphers - ssl_certificate /etc/registry/certs/registry.crt; - ssl_certificate_key /etc/registry/certs/private/registry.key; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; - ssl_prefer_server_ciphers off; - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 10m; - ssl_session_tickets off; - ssl_stapling on; - ssl_stapling_verify on; - - # Security headers with HSTS - add_header X-Frame-Options DENY always; - add_header X-Content-Type-Options nosniff always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; - add_header Content-Security-Policy "default-src 'none'; frame-ancestors 'none';" always; - add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; - - # Client max body size for large image uploads - client_max_body_size 2G; - client_body_timeout 60s; - client_header_timeout 60s; - - # Rate limiting for write operations - limit_req zone=push burst=10 nodelay; - - # Rate limiting for login attempts - location ~ ^/v2/.*/blobs/uploads/ { - limit_req zone=login burst=3 nodelay; - auth_basic "Docker Registry v2"; - auth_basic_user_file /etc/nginx/.htpasswd; - proxy_pass http://registry; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 900; - proxy_connect_timeout 60; - proxy_send_timeout 60; - proxy_buffering off; - proxy_request_buffering off; - proxy_hide_header X-Powered-By; - proxy_hide_header Server; - } - - # Basic authentication for write operations - location ~ ^/v2/.*$ { - # Require auth for all v2 API operations - auth_basic "Docker Registry v2"; - auth_basic_user_file /etc/nginx/.htpasswd; - - proxy_pass http://registry; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 900; - proxy_connect_timeout 60; - proxy_send_timeout 60; - proxy_buffering off; - proxy_request_buffering off; - proxy_hide_header X-Powered-By; - proxy_hide_header Server; - } - - # Block access to hidden files - location ~ /\. { - deny all; - access_log off; - log_not_found off; - } - - # Block access to sensitive files - location ~* \.(htaccess|htpasswd|ini|log|sh|sql|conf)$ { - deny all; - access_log off; - log_not_found off; - } - - location / { - return 404; - } - - access_log /var/log/nginx/registry_auth_access.log; - error_log /var/log/nginx/registry_auth_error.log; -} -} diff --git a/registry/openssl.conf b/registry/openssl.conf deleted file mode 100644 index 206e4b0..0000000 --- a/registry/openssl.conf +++ /dev/null @@ -1,19 +0,0 @@ -[ req ] -default_bits = 4096 -prompt = no -default_md = sha256 -distinguished_name = dn -req_extensions = req_ext - -[ dn ] -O = YOUR_REGISTRY_NAME -CN = YOUR_CI_CD_IP - -[ req_ext ] -basicConstraints = CA:FALSE -keyUsage = digitalSignature, keyEncipherment -extendedKeyUsage = serverAuth -subjectAltName = @alt_names - -[ alt_names ] -IP.1 = YOUR_CI_CD_IP \ No newline at end of file diff --git a/registry/registry-pod.yaml b/registry/registry-pod.yaml deleted file mode 100644 index c50a419..0000000 --- a/registry/registry-pod.yaml +++ /dev/null @@ -1,169 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: registry-pod - labels: - app: registry - security: hardened -spec: - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - readOnlyRootFilesystem: true - seccompProfile: - type: RuntimeDefault - # Additional security hardening - sysctls: - - name: net.ipv4.ip_forward - value: "0" - - name: net.ipv4.conf.all.forwarding - value: "0" - - name: net.ipv4.conf.default.forwarding - value: "0" - containers: - - name: registry - image: registry:2 - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - readOnlyRootFilesystem: true - env: - - name: REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY - value: "/var/lib/registry" - - name: REGISTRY_STORAGE_DELETE_ENABLED - value: "false" - - name: REGISTRY_HTTP_ADDR - value: "0.0.0.0:5000" - - name: REGISTRY_HTTP_TLS_CERTIFICATE - value: "/etc/registry/certs/registry.crt" - - name: REGISTRY_HTTP_TLS_KEY - value: "/etc/registry/certs/private/registry.key" - resources: - requests: - cpu: "500m" - memory: "512Mi" - limits: - cpu: "1000m" - memory: "1Gi" - volumeMounts: - - name: registry-data - mountPath: /var/lib/registry - readOnly: false - - name: registry-certs - mountPath: /etc/registry/certs - readOnly: true - - name: tmp-volume - mountPath: /tmp - readOnly: false - ports: - - containerPort: 5000 - protocol: TCP - livenessProbe: - httpGet: - path: /v2/ - port: 5000 - initialDelaySeconds: 30 - periodSeconds: 10 - readinessProbe: - httpGet: - path: /v2/ - port: 5000 - initialDelaySeconds: 5 - periodSeconds: 5 - - - name: nginx - image: nginx:alpine - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - readOnlyRootFilesystem: true - # Additional security hardening - procMount: Default - seccompProfile: - type: RuntimeDefault - ports: - - containerPort: 443 - protocol: TCP - - containerPort: 4443 - protocol: TCP - resources: - requests: - cpu: "250m" - memory: "256Mi" - limits: - cpu: "500m" - memory: "512Mi" - volumeMounts: - - name: nginx-config - mountPath: /etc/nginx/nginx.conf - subPath: nginx.conf - readOnly: true - - name: registry-certs - mountPath: /etc/registry/certs - readOnly: true - - name: registry-auth - mountPath: /etc/nginx/.htpasswd - subPath: .htpasswd - readOnly: true - - name: nginx-logs - mountPath: /var/log/nginx - readOnly: false - - name: tmp-volume - mountPath: /tmp - readOnly: false - livenessProbe: - httpGet: - path: /health - port: 443 - scheme: HTTPS - initialDelaySeconds: 30 - periodSeconds: 10 - readinessProbe: - httpGet: - path: /health - port: 443 - scheme: HTTPS - initialDelaySeconds: 5 - periodSeconds: 5 - - volumes: - - name: registry-data - hostPath: - path: /var/lib/registry/data - type: Directory - - name: nginx-config - hostPath: - path: /opt/APP_NAME/registry/nginx.conf - type: File - - name: registry-certs - hostPath: - path: /etc/registry/certs - type: Directory - - name: registry-auth - hostPath: - path: /etc/registry/auth - type: Directory - - name: nginx-logs - hostPath: - path: /var/log/nginx - type: Directory - - name: tmp-volume - emptyDir: - medium: Memory - sizeLimit: "100Mi" diff --git a/scripts/monitor.sh b/scripts/monitor.sh deleted file mode 100755 index 5b2ecc3..0000000 --- a/scripts/monitor.sh +++ /dev/null @@ -1,192 +0,0 @@ -#!/bin/bash - -# Sharenet Monitoring Script -# This script monitors the application status and system resources - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Configuration -APP_NAME="${APP_NAME:-sharenet}" -MONITOR_TYPE="${MONITOR_TYPE:-production}" # production or ci-cd - -# Functions -log_info() { - echo -e "${BLUE}[INFO]${NC} $1" -} - -log_success() { - echo -e "${GREEN}[SUCCESS]${NC} $1" -} - -log_warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" -} - -log_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -show_help() { - cat << EOF -Sharenet Monitoring Script - -Usage: $0 [OPTIONS] - -Options: - --type TYPE Monitoring type: production or ci-cd (default: production) - --app-name NAME Application name (default: sharenet) - --help Show this help message - -Environment Variables: - MONITOR_TYPE Set monitoring type (production/ci-cd) - APP_NAME Set application name - -Examples: - $0 # Monitor production environment - $0 --type ci-cd # Monitor CI/CD environment - $0 --app-name myapp # Monitor specific application - MONITOR_TYPE=ci-cd $0 # Monitor CI/CD environment -EOF -} - -monitor_production() { - log_info "=== $APP_NAME Production Environment Status ===" - echo "Date: $(date)" - echo "Uptime: $(uptime)" - echo - - # Check if we're in the application directory - if [ -f "docker-compose.yml" ]; then - log_info "Container Status:" - if docker-compose ps; then - log_success "Docker Compose is running" - else - log_error "Docker Compose is not running" - fi - echo - - log_info "Recent Application Logs:" - docker-compose logs --tail=20 - echo - else - log_warning "Not in application directory (docker-compose.yml not found)" - fi - - log_info "System Resources:" - echo "CPU Usage:" - top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1 - echo - - echo "Memory Usage:" - free -h - echo - - echo "Disk Usage:" - df -h - echo - - echo "Network Connections:" - netstat -tuln | grep -E ':(80|443|3000|3001)' || log_warning "No application ports found" - echo - - # Health check - log_info "Health Check:" - if curl -s -f http://localhost/health > /dev/null 2>&1; then - log_success "Application health check passed" - else - log_error "Application health check failed" - fi -} - -monitor_ci_cd() { - log_info "=== CI/CD Server Status ===" - echo "Date: $(date)" - echo "Uptime: $(uptime)" - echo - - log_info "Docker Registry Status:" - if docker ps --format "table {{.Names}}\t{{.Status}}" | grep -q registry; then - log_success "Docker Registry containers are running" - docker ps --format "table {{.Names}}\t{{.Status}}" | grep registry - else - log_error "Docker Registry containers are not running" - fi - echo - - log_info "Actions Runner Status:" - if systemctl is-active --quiet forgejo-runner.service; then - log_success "Forgejo runner is running" - systemctl status forgejo-runner.service --no-pager - else - log_error "Forgejo runner is not running" - fi - echo - - log_info "System Resources:" - echo "CPU Usage:" - top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1 - echo - - echo "Memory Usage:" - free -h | grep Mem - echo - - echo "Disk Usage:" - df -h / - echo - - # Registry health check - log_info "Registry Health Check:" - if curl -s -f -k https://localhost/v2/_catalog > /dev/null 2>&1; then - log_success "Docker Registry is accessible" - else - log_error "Docker Registry is not accessible" - fi -} - -# Parse command line arguments -while [[ $# -gt 0 ]]; do - case $1 in - --type) - MONITOR_TYPE="$2" - shift 2 - ;; - --app-name) - APP_NAME="$2" - shift 2 - ;; - --help|-h) - show_help - exit 0 - ;; - *) - log_error "Unknown option: $1" - show_help - exit 1 - ;; - esac -done - -# Main monitoring logic -case "$MONITOR_TYPE" in - production) - monitor_production - ;; - ci-cd) - monitor_ci_cd - ;; - *) - log_error "Invalid monitor type: $MONITOR_TYPE" - log_error "Valid types: production, ci-cd" - exit 1 - ;; -esac - -log_success "Monitoring completed" \ No newline at end of file diff --git a/scripts/test_dind_setup.sh b/scripts/test_dind_setup.sh deleted file mode 100755 index ea7e89e..0000000 --- a/scripts/test_dind_setup.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/bin/bash - -# Test DinD Setup Script -# This script verifies that the DinD container is properly configured -# and can perform the operations needed by the CI workflow - -set -e - -echo "🧪 Testing DinD Setup..." - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Function to print colored output -print_status() { - local status=$1 - local message=$2 - if [ "$status" = "PASS" ]; then - echo -e "${GREEN}✅ $message${NC}" - elif [ "$status" = "FAIL" ]; then - echo -e "${RED}❌ $message${NC}" - else - echo -e "${YELLOW}⚠️ $message${NC}" - fi -} - -# Test 1: Check if DinD container exists and is running -echo "1. Checking DinD container status..." -if docker ps --format "table {{.Names}}" | grep -q "^ci-dind$"; then - print_status "PASS" "DinD container is running" -else - print_status "FAIL" "DinD container is not running" - echo "Starting DinD container..." - docker run -d \ - --name ci-dind \ - --privileged \ - -p 2375:2375 \ - -e DOCKER_TLS_CERTDIR="" \ - docker:dind - - # Wait for DinD to be ready - echo "Waiting for DinD to be ready..." - timeout 60 bash -c 'until docker exec ci-dind docker version; do sleep 2; done' - print_status "PASS" "DinD container started successfully" -fi - -# Test 2: Check Docker functionality inside DinD -echo "2. Testing Docker functionality inside DinD..." -if docker exec ci-dind docker version > /dev/null 2>&1; then - print_status "PASS" "Docker is working inside DinD" -else - print_status "FAIL" "Docker is not working inside DinD" - exit 1 -fi - -# Test 3: Check if Harbor certificate is installed -echo "3. Checking Harbor certificate installation..." -if docker exec ci-dind test -f /usr/local/share/ca-certificates/registry.crt; then - print_status "PASS" "Harbor certificate is installed" -else - print_status "WARN" "Harbor certificate not found - will be installed during CI run" -fi - -# Test 4: Test git functionality inside DinD -echo "4. Testing git functionality inside DinD..." -if docker exec ci-dind git --version > /dev/null 2>&1; then - print_status "PASS" "Git is available inside DinD" -else - print_status "FAIL" "Git is not available inside DinD" - echo "Installing git in DinD..." - docker exec ci-dind apk add --no-cache git - print_status "PASS" "Git installed successfully" -fi - -# Test 5: Test workspace directory -echo "5. Testing workspace directory..." -if docker exec ci-dind test -d /workspace; then - print_status "PASS" "Workspace directory exists" -else - print_status "WARN" "Workspace directory does not exist - will be created during CI run" -fi - -# Test 6: Test basic Docker operations -echo "6. Testing basic Docker operations..." -if docker exec ci-dind docker run --rm alpine:latest echo "test" > /dev/null 2>&1; then - print_status "PASS" "Basic Docker operations work" -else - print_status "FAIL" "Basic Docker operations failed" - exit 1 -fi - -# Test 7: Test Docker Compose -echo "7. Testing Docker Compose..." -if docker exec ci-dind docker compose version > /dev/null 2>&1; then - print_status "PASS" "Docker Compose is available" -else - print_status "FAIL" "Docker Compose is not available" - exit 1 -fi - -echo "" -echo "🎉 DinD Setup Test Complete!" -echo "" -echo "If all tests passed, your DinD environment is ready for CI/CD operations." -echo "The CI workflow will:" -echo " 1. Checkout code directly into the DinD container from your Forgejo repository" -echo " 2. Run tests in isolated containers" -echo " 3. Build and push images to Harbor" -echo "" -echo "To run the CI workflow, push changes to your main branch." \ No newline at end of file