Change to using Forgejo Container Registry
Some checks are pending
CI/CD Pipeline (Forgejo Container Registry) / Run Tests (DinD) (push) Waiting to run
CI/CD Pipeline (Forgejo Container Registry) / Build and Push Docker Images (DinD) (push) Blocked by required conditions
CI/CD Pipeline (Forgejo Container Registry) / Deploy to Production (push) Blocked by required conditions

This commit is contained in:
continuist 2025-08-30 19:38:54 -04:00
parent 1fb32f3366
commit eb6e373981
19 changed files with 332 additions and 1939 deletions

View file

@ -1,4 +1,4 @@
name: CI/CD Pipeline (Fully Isolated DinD) name: CI/CD Pipeline (Forgejo Container Registry)
on: on:
push: push:
@ -7,8 +7,8 @@ on:
branches: [ main ] branches: [ main ]
env: env:
REGISTRY: ${{ secrets.CI_HOST }}:443 REGISTRY_HOST: ${{ secrets.REGISTRY_HOST }}
IMAGE_NAME: ${{ secrets.APP_NAME || 'sharenet' }} OWNER_REPO: ${{ gitea.repository }}
jobs: jobs:
# Job 1: Testing - Uses DinD with multiple containers for comprehensive testing # Job 1: Testing - Uses DinD with multiple containers for comprehensive testing
@ -57,9 +57,6 @@ jobs:
echo "Installing Cosign..." 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" 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" echo "DinD container setup complete"
fi fi
@ -77,57 +74,63 @@ jobs:
docker exec ci-dind rm -rf /workspace/* /workspace/.* 2>/dev/null || true docker exec ci-dind rm -rf /workspace/* /workspace/.* 2>/dev/null || true
docker cp /tmp/ci-workspace/. ci-dind:/workspace/ docker cp /tmp/ci-workspace/. ci-dind:/workspace/
- name: Check and prepare Docker Registry base images - name: Check and prepare base images
run: | run: |
# Set environment variables # Set environment variables
export CI_HOST="${{ secrets.CI_HOST }}" export REGISTRY_HOST="${{ secrets.REGISTRY_HOST }}"
export APP_NAME="${{ secrets.APP_NAME || 'sharenet' }}" export OWNER_REPO="${{ gitea.repository }}"
export REGISTRY_USER="${{ secrets.REGISTRY_USER }}" export REGISTRY_USERNAME="${{ secrets.REGISTRY_USERNAME }}"
export REGISTRY_PASSWORD="${{ secrets.REGISTRY_PASSWORD }}" export REGISTRY_TOKEN="${{ secrets.REGISTRY_TOKEN }}"
# Login to Docker Registry # Login to Forgejo Container Registry
echo "Logging into Docker Registry..." echo "Logging into Forgejo Container Registry..."
echo "$REGISTRY_PASSWORD" | docker exec -i ci-dind docker login "$CI_HOST:443" -u "$REGISTRY_USER" --password-stdin 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") BASE_IMAGES=("rust:1.75-slim" "node:20-slim" "postgres:15-alpine")
for image in "${BASE_IMAGES[@]}"; do for image in "${BASE_IMAGES[@]}"; do
image_name=$(echo "$image" | cut -d: -f1) image_name=$(echo "$image" | cut -d: -f1)
image_tag=$(echo "$image" | cut -d: -f2) 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 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 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 # Pull from Docker Hub
if docker exec ci-dind docker pull "$image"; then if docker exec ci-dind docker pull "$image"; then
echo "✓ Successfully pulled $image from Docker Hub" 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" docker exec ci-dind docker tag "$image" "$registry_image"
# Push to Docker Registry # Push to Forgejo Container Registry
if docker exec ci-dind docker push "$registry_image"; then if docker exec ci-dind docker push "$registry_image"; then
echo "✓ Successfully pushed $registry_image to Docker Registry" echo "✓ Successfully pushed $registry_image to Forgejo Container Registry"
# Sign the image with Cosign (keyless OIDC) # Sign the image with Cosign (optional)
echo "Signing image with Cosign..." if [ -n "${{ secrets.COSIGN_PRIVATE_KEY }}" ]; then
if docker exec ci-dind sh -c "COSIGN_EXPERIMENTAL=1 cosign sign --keyless $registry_image"; then echo "Signing image with Cosign..."
echo "✓ Successfully signed $registry_image with Cosign" echo "${{ secrets.COSIGN_PRIVATE_KEY }}" | docker exec -i ci-dind sh -c "cat > /tmp/cosign.key && chmod 600 /tmp/cosign.key"
else if docker exec ci-dind sh -c "COSIGN_PASSWORD='${{ secrets.COSIGN_PASSWORD }}' cosign sign -y --key /tmp/cosign.key $registry_image"; then
echo "✗ Failed to sign $registry_image with Cosign" echo "✓ Successfully signed $registry_image with Cosign"
exit 1 else
fi echo "✗ Failed to sign $registry_image with Cosign"
else exit 1
echo "✗ Failed to push $registry_image to Docker Registry" fi
exit 1 docker exec ci-dind rm -f /tmp/cosign.key
fi 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 else
echo "✗ Failed to pull $image from Docker Hub" echo "✗ Failed to pull $image from Docker Hub"
exit 1 exit 1
@ -135,19 +138,20 @@ jobs:
fi fi
done done
echo "All base images are ready in Docker Registry!" echo "All base images are ready in Forgejo Container Registry!"
- name: Start testing environment - name: Start testing environment
run: | run: |
# Start testing environment using dedicated compose file inside DinD # Start testing environment using Kubernetes pod inside DinD
echo "Starting testing environment..." echo "Starting testing environment..."
# Set environment variables for docker-compose # Set environment variables
export CI_HOST="${{ secrets.CI_HOST }}" export CI_HOST="${{ secrets.CI_HOST }}"
export APP_NAME="${{ secrets.APP_NAME || 'sharenet' }}" export APP_NAME="${{ secrets.APP_NAME || 'sharenet' }}"
# Start the testing environment with build args # Create workspace directory and start pod
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" 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 # Wait for all services to be ready with better error handling
echo "Waiting for testing environment to be ready..." echo "Waiting for testing environment to be ready..."
@ -155,40 +159,39 @@ jobs:
WAIT_COUNT=0 WAIT_COUNT=0
while [ $WAIT_COUNT -lt $MAX_WAIT ]; do while [ $WAIT_COUNT -lt $MAX_WAIT ]; do
# Check if all containers are running # Check if pod is running and ready
RUNNING_CONTAINERS=$(docker exec ci-dind docker compose -f /workspace/docker-compose.test.yml ps -q | wc -l) POD_STATUS=$(docker exec ci-dind podman pod ps --filter name=ci-cd-test-pod --format "{{.Status}}" 2>/dev/null || echo "")
EXPECTED_CONTAINERS=3 # postgres, rust-toolchain, node-toolchain
if [ "$RUNNING_CONTAINERS" -eq "$EXPECTED_CONTAINERS" ]; then if [ "$POD_STATUS" = "Running" ]; then
echo "All containers are running" echo "Pod is running"
break break
else else
echo "Waiting for containers to start... ($RUNNING_CONTAINERS/$EXPECTED_CONTAINERS running)" echo "Waiting for pod to start... (Status: $POD_STATUS)"
sleep 2 sleep 2
WAIT_COUNT=$((WAIT_COUNT + 2)) WAIT_COUNT=$((WAIT_COUNT + 2))
fi fi
done done
if [ $WAIT_COUNT -ge $MAX_WAIT ]; then if [ $WAIT_COUNT -ge $MAX_WAIT ]; then
echo "ERROR: Timeout waiting for containers to start" echo "ERROR: Timeout waiting for pod to start"
echo "Container status:" echo "Pod status:"
docker exec ci-dind docker compose -f /workspace/docker-compose.test.yml ps docker exec ci-dind podman pod ps
echo "Container logs:" echo "Pod logs:"
docker exec ci-dind docker compose -f /workspace/docker-compose.test.yml logs docker exec ci-dind podman logs ci-cd-test-pod-postgres || true
exit 1 exit 1
fi fi
# Additional wait for PostgreSQL to be healthy # Additional wait for PostgreSQL to be healthy
echo "Waiting 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 # Verify pod is running
echo "Final container status:" echo "Final pod status:"
docker exec ci-dind docker compose -f /workspace/docker-compose.test.yml ps docker exec ci-dind podman pod ps
- name: Install SQLx CLI in Rust container - name: Install SQLx CLI in Rust container
run: | 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 - name: Validate migration files
env: env:
@ -196,16 +199,16 @@ jobs:
run: | run: |
# Wait for PostgreSQL to be ready # Wait for PostgreSQL to be ready
echo "Waiting 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 # 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 # 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 # 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 - name: Run backend tests
working-directory: ./backend working-directory: ./backend
@ -213,25 +216,26 @@ jobs:
DATABASE_URL: postgres://postgres:password@localhost:5432/sharenet_test DATABASE_URL: postgres://postgres:password@localhost:5432/sharenet_test
run: | run: |
# Run tests with increased parallelism for Rust # 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 podman exec ci-cd-test-pod-rust-toolchain cargo test --all --jobs 4
docker exec ci-dind docker exec ci-cd-test-rust cargo clippy --all -- -D warnings docker exec ci-dind podman exec ci-cd-test-pod-rust-toolchain 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 fmt --all -- --check
- name: Install frontend dependencies - name: Install frontend dependencies
run: | 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 - name: Run frontend tests
run: | run: |
docker exec ci-dind docker exec ci-cd-test-node npm run lint docker exec ci-dind podman exec ci-cd-test-pod-node-toolchain npm run lint
docker exec ci-dind docker exec ci-cd-test-node npm run type-check docker exec ci-dind podman exec ci-cd-test-pod-node-toolchain 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 build
- name: Cleanup Testing Environment - name: Cleanup Testing Environment
if: always() if: always()
run: | run: |
# Stop and remove all testing containers (but keep DinD running) # Stop and remove testing pod (but keep DinD running)
docker exec ci-dind docker compose -f /workspace/docker-compose.test.yml down 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 # Job 2: Building - Build and push Docker images using same DinD
build-and-push: build-and-push:
@ -253,37 +257,63 @@ jobs:
# Verify we have the correct repository # Verify we have the correct repository
docker exec ci-dind sh -c "cd /workspace && git remote -v" 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 - name: Build and push backend image
env:
IMAGE: ${{ env.REGISTRY_HOST }}/${{ env.OWNER_REPO }}/backend
TAG: ${{ gitea.sha }}
run: | run: |
# Build and push backend image using DinD # Build and push backend image using DinD
docker exec ci-dind docker buildx build \ docker exec ci-dind docker buildx build \
--platform linux/amd64 \ --platform linux/amd64 \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/backend:${{ gitea.sha }} \ --tag "${IMAGE}:${TAG}" \
--push \ --push \
--cache-from type=gha \ --cache-from type=gha \
--cache-to type=gha,mode=max \ --cache-to type=gha,mode=max \
-f /workspace/backend/Dockerfile \ -f /workspace/backend/Dockerfile \
/workspace/backend /workspace/backend
# Sign the backend image with Cosign (keyless OIDC) # Sign the backend image with Cosign (optional)
echo "Signing backend image with Cosign..." if [ -n "${{ secrets.COSIGN_PRIVATE_KEY }}" ]; then
docker exec ci-dind sh -c "COSIGN_EXPERIMENTAL=1 cosign sign --keyless ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/backend:${{ gitea.sha }}" 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 - name: Build and push frontend image
env:
IMAGE: ${{ env.REGISTRY_HOST }}/${{ env.OWNER_REPO }}/frontend
TAG: ${{ gitea.sha }}
run: | run: |
# Build and push frontend image using DinD # Build and push frontend image using DinD
docker exec ci-dind docker buildx build \ docker exec ci-dind docker buildx build \
--platform linux/amd64 \ --platform linux/amd64 \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/frontend:${{ gitea.sha }} \ --tag "${IMAGE}:${TAG}" \
--push \ --push \
--cache-from type=gha \ --cache-from type=gha \
--cache-to type=gha,mode=max \ --cache-to type=gha,mode=max \
-f /workspace/frontend/Dockerfile \ -f /workspace/frontend/Dockerfile \
/workspace/frontend /workspace/frontend
# Sign the frontend image with Cosign (keyless OIDC) # Sign the frontend image with Cosign (optional)
echo "Signing frontend image with Cosign..." if [ -n "${{ secrets.COSIGN_PRIVATE_KEY }}" ]; then
docker exec ci-dind sh -c "COSIGN_EXPERIMENTAL=1 cosign sign --keyless ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/frontend:${{ gitea.sha }}" 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 - name: Cleanup Testing Environment
if: always() if: always()
@ -336,8 +366,8 @@ jobs:
run: | run: |
# Create environment file for this deployment # Create environment file for this deployment
echo "IMAGE_TAG=${{ gitea.sha }}" > .env echo "IMAGE_TAG=${{ gitea.sha }}" > .env
echo "REGISTRY=${{ secrets.CI_HOST }}:443" >> .env echo "REGISTRY_HOST=${{ secrets.REGISTRY_HOST }}" >> .env
echo "IMAGE_NAME=${{ secrets.APP_NAME || 'sharenet' }}" >> .env echo "OWNER_REPO=${{ gitea.repository }}" >> .env
echo "POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD || 'your_secure_password_here' }}" >> .env echo "POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD || 'your_secure_password_here' }}" >> .env
echo "POSTGRES_USER=${{ secrets.POSTGRES_USER || 'sharenet' }}" >> .env echo "POSTGRES_USER=${{ secrets.POSTGRES_USER || 'sharenet' }}" >> .env
echo "POSTGRES_DB=${{ secrets.POSTGRES_DB || 'sharenet' }}" >> .env echo "POSTGRES_DB=${{ secrets.POSTGRES_DB || 'sharenet' }}" >> .env
@ -348,18 +378,22 @@ jobs:
- name: Make scripts executable - name: Make scripts executable
run: chmod +x scripts/*.sh run: chmod +x scripts/*.sh
- name: Configure Docker for Docker Registry access - name: Configure Docker for Forgejo Container Registry access
run: | run: |
# Configure Docker to access Docker Registry on CI Linode (using HTTPS) # Configure Docker to access Forgejo Container Registry
# Since we're using Caddy with automatic HTTPS, no certificate configuration is needed # Since we're using Forgejo's built-in registry, no certificate configuration is needed
# Wait for Docker to be ready # Wait for Docker to be ready
timeout 30 bash -c 'until docker info; do sleep 1; done' timeout 30 bash -c 'until docker info; do sleep 1; done'
# Verify signed images before deployment # Verify signed images before deployment (if Cosign is configured)
echo "Verifying signed images..." if [ -n "${{ secrets.COSIGN_PRIVATE_KEY }}" ]; then
cosign verify --key /etc/containers/keys/org-cosign.pub ${{ secrets.CI_HOST }}:443/${{ secrets.APP_NAME || 'sharenet' }}/backend:${{ gitea.sha }} echo "Verifying signed images..."
cosign verify --key /etc/containers/keys/org-cosign.pub ${{ secrets.CI_HOST }}:443/${{ secrets.APP_NAME || 'sharenet' }}/frontend:${{ gitea.sha }} 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 - name: Validate migration files
run: | run: |
@ -369,20 +403,29 @@ jobs:
exit 1 exit 1
} }
- name: Pull and deploy application - name: Deploy application using Kubernetes pod
run: | run: |
# Pull latest images from Docker Registry # Set environment variables for the pod deployment
echo "Pulling latest images from Docker Registry..." export IMAGE_TAG="${{ gitea.sha }}"
docker compose -f docker-compose.prod.yml pull 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 # Stop any existing production pod
echo "Deploying application stack..." podman pod stop sharenet-production-pod || true
docker compose -f docker-compose.prod.yml up -d podman pod rm sharenet-production-pod || true
# Wait for all services to be healthy # Deploy the application pod with environment substitution
echo "Waiting for all services to be healthy..." echo "Deploying application pod..."
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' 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 # Verify deployment
echo "Verifying deployment..." echo "Verifying deployment..."
docker compose -f docker-compose.prod.yml ps podman pod ps
podman pod logs sharenet-production-pod

View file

@ -8,7 +8,7 @@ This guide covers setting up a complete Continuous Integration/Continuous Deploy
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Forgejo Host │ │ CI/CD Linode │ │ Production Linode│ │ Forgejo Host │ │ CI/CD Linode │ │ Production Linode│
│ (Repository) │ │ (Actions Runner)│ │ (Podman Deploy) │ │ (Repository) │ │ (Actions Runner)│ │ (Podman Deploy) │
│ │ │ + Podman Registry│ │ │ │ │ │ + Forgejo Registry│ │ │
│ │ │ + PiP Container │ │ │ │ │ │ + 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 1. **Code Push**: Developer pushes code to Forgejo repository
2. **Automated Testing**: CI/CD Linode runs tests in isolated DinD environment 2. **Automated Testing**: CI/CD Linode runs tests in isolated DinD environment
3. **Image Building**: If tests pass, Docker images are built within DinD 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 5. **Production Deployment**: Production Linode pulls images and deploys
6. **Health Check**: Application is verified and accessible 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 - ✅ **Fast cleanup** - just restart PiP container
### **For CI/CD Operations:** ### **For CI/CD Operations:**
- ✅ **Zero resource contention** with Podman Registry - ✅ **Zero resource contention** with Forgejo Container Registry
- ✅ **Simple cleanup** - one-line container restart - ✅ **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 - ✅ **Consistent environment** - same setup every time
### **For Maintenance:** ### **For Maintenance:**
@ -64,9 +64,9 @@ This guide covers setting up a complete Continuous Integration/Continuous Deploy
### CI/CD Linode Features ### CI/CD Linode Features
- Forgejo Actions runner for automated builds - Forgejo Actions runner for automated builds
- **Podman-in-Podman (PiP) container** for isolated CI operations - **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 - **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 - Automatic HTTPS with nginx reverse proxy
- Secure SSH communication with production - Secure SSH communication with production
- **Simplified cleanup** - just restart PiP container - **Simplified cleanup** - just restart PiP container
@ -80,11 +80,11 @@ This guide covers setting up a complete Continuous Integration/Continuous Deploy
### Pipeline Features ### Pipeline Features
- **Automated testing** on every code push in isolated environment - **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 - **Automated deployment** to production
- **Rollback capability** with image versioning - **Rollback capability** with image versioning
- **Health monitoring** and logging - **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 - **Robust rootless services** via systemd user manager
## Security Model and User Separation ## Security Model and User Separation
@ -650,21 +650,19 @@ ls -la /opt/APP_NAME/registry/
- CI_SERVICE_USER owns all the files for security - CI_SERVICE_USER owns all the files for security
- Registry configuration files are now available at `/opt/APP_NAME/registry/` - 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 **Configuration**: Registry access is handled through:
- mTLS authentication for secure push operations - **Authentication**: Forgejo Personal Access Tokens (PAT)
- Unauthenticated pulls for public read access - **Registry URL**: Your Forgejo instance's registry endpoint
- FHS-compliant directory structure - **Security**: Built-in Forgejo authentication and authorization
- Systemd user manager for robust rootless services
**Quick Reference**: The Docker Registry will be accessible at: **Quick Reference**: The Forgejo Container Registry will be accessible at:
- **Port 443**: Unauthenticated pulls (public read access) - **Registry URL**: `${REGISTRY_HOST}` (configured in secrets)
- **Port 4443**: Authenticated pushes (mTLS client certificate required) - **Authentication**: PAT with `write:packages` scope for pushes
- **Public Access**: Available for pulls from public repositories
**Port 443 allows unauthenticated Docker Registry v2 pulls; Port 4443 is for authenticated pushes (mTLS).**
### Step 5: Install Forgejo Actions Runner ### 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 ### 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 #### 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. **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 ```bash
# Navigate to the application directory # 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 600 /etc/registry/certs/private/client.key
podman exec ci-pip chmod 644 /etc/registry/certs/clients/client.crt 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 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 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 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 ### 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/`): **Application Files** (in `/opt/APP_NAME/registry/`):
- `containers-policy.json` - Container policy for image signature verification - `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 - **Policy enforcement**: Container policies for image signature verification
**What this does**: **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 - **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 - **Validates setup**: Ensures the complete CI/CD pipeline will work
#### 6.4 CI/CD Workflow Architecture #### 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 - Rust toolchain for backend testing and migrations
- Node.js toolchain for frontend testing - Node.js toolchain for frontend testing
- **Network**: All containers communicate through `ci-cd-test-network` - **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 - **Cleanup**: Testing containers removed, DinD container kept running
**Job 2 (Building) - Direct Docker Commands:** **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 - **Environment**: Same DinD container from Job 1
- **Code Access**: Reuses code from Job 1, updates to latest commit - **Code Access**: Reuses code from Job 1, updates to latest commit
- **Process**: - **Process**:
- Uses Docker Buildx for efficient building - Uses Docker Buildx for efficient building
- Builds backend and frontend images separately - Builds backend and frontend images separately
- Pushes images to Docker Registry - Pushes images to Forgejo Container Registry
- **Registry Access**: Reuses Docker Registry authentication from Job 1 - **Registry Access**: Reuses Forgejo Container Registry authentication from Job 1
- **Cleanup**: DinD container stopped and removed (clean slate for next run) - **Cleanup**: DinD container stopped and removed (clean slate for next run)
**Job 3 (Deployment) - `docker-compose.prod.yml`:** **Job 3 (Deployment) - `docker-compose.prod.yml`:**
- **Purpose**: Production deployment with pre-built images - **Purpose**: Production deployment with pre-built images
- **Environment**: Production runner on Production Linode - **Environment**: Production runner on Production Linode
- **Process**: - **Process**:
- Pulls images from Docker Registry - Pulls images from Forgejo Container Registry
- Deploys complete application stack - Deploys complete application stack
- Verifies all services are healthy - Verifies all services are healthy
- **Services**: PostgreSQL, backend, frontend, Nginx - **Services**: PostgreSQL, backend, frontend, Nginx
**Key Benefits:** **Key Benefits:**
- **🧹 Complete Isolation**: Each job has its own dedicated environment - **🧹 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 - **⚡ Consistent Environment**: Same setup every time
- **🎯 Purpose-Specific**: Each Docker Compose file serves a specific purpose - **🎯 Purpose-Specific**: Each Docker Compose file serves a specific purpose
- **🔄 Parallel Safety**: Jobs can run safely in parallel - **🔄 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 # Test DinD functionality
docker exec ci-dind docker run --rm alpine:latest echo "DinD is working!" 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 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 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 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**: **Expected Output**:
- DinD container should be running and accessible - DinD container should be running and accessible
- Docker commands should work inside DinD - 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 #### 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:** **Production Stack Components:**
- **PostgreSQL**: Production database with persistent storage - **PostgreSQL**: Production database with persistent storage
@ -1111,7 +1109,7 @@ The production deployment uses a separate Docker Compose file (`docker-compose.p
**Deployment Flow:** **Deployment Flow:**
1. **Production Runner**: Runs on Production Linode with `production` label 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 3. **Stack Deployment**: Uses `docker-compose.prod.yml` to deploy complete stack
4. **Health Verification**: Ensures all services are healthy before completion 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 # Change to the PROD_SERVICE_USER
sudo su - PROD_SERVICE_USER sudo su - PROD_SERVICE_USER
# Test that Podman can pull images from the Docker Registry v2 (unauthenticated port 443) # Test Forgejo Container Registry access will be verified during deployment
podman pull YOUR_CI_CD_IP/APP_NAME/test:latest # Images are pulled from the configured REGISTRY_HOST during the CI/CD process
# If the pull succeeds, the Docker Registry v2 is accessible for production deployments
# Change back to PROD_DEPLOY_USER # Change back to PROD_DEPLOY_USER
exit exit
``` ```
**Important**: Replace `YOUR_CI_CD_IP` with your actual CI/CD Linode IP address. **Note**: Production deployments pull images from Forgejo Container Registry using the configured authentication.
**Note**: Production deployments use unauthenticated pulls from port 443, while CI/CD operations use authenticated pushes to port 4443.
**What this does**: **What this does**:
- **Tests Docker Registry v2 access**: Verifies that Podman can successfully pull images from the Docker Registry v2 - **Tests Forgejo Container Registry access**: Registry access is verified during the deployment process
- **No certificate configuration needed**: nginx handles HTTPS automatically - **Integrated authentication**: Forgejo handles authentication automatically
- **Simple setup**: No complex certificate management required - **Simple setup**: No complex certificate management required
### Step 14: Set Up Forgejo Runner for Production Deployment ### 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! ## 🎉 Congratulations!
You have successfully set up a complete CI/CD pipeline with: You have successfully set up a complete CI/CD pipeline with:

View file

@ -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=<SERVER_IPV4>
# If using IPv6, uncomment and set:
# IPAddressAllow=<SERVER_IPV6>
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 <SERVER_IPV4>:443 ssl http2;
# If IPv6, also:
# listen [<SERVER_IPV6>]: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 <SERVER_IPV4>:4443 ssl http2;
# If IPv6, also:
# listen [<SERVER_IPV6>]: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:<SERVER_IPV4>
# If IPv6:
# subjectAltName = DNS:REGISTRY_HOST,IP:<SERVER_IPV4>,IP:<SERVER_IPV6>
# 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:<digest>`
**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!

View file

@ -144,6 +144,27 @@ Configuration files are located in `backend/config/`:
### Frontend Configuration ### 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. 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 ## 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. This project is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License - see the [LICENSE.md](LICENSE.md) file for details.

View file

@ -1,7 +1,7 @@
# PostgreSQL testing environment for CI/CD # PostgreSQL testing environment for CI/CD
ARG CI_HOST=localhost ARG REGISTRY_HOST=localhost
ARG APP_NAME=sharenet ARG OWNER_REPO=owner/repo
FROM ${CI_HOST}:443/${APP_NAME}/postgres:15-alpine FROM ${REGISTRY_HOST}/${OWNER_REPO}/postgres:15-alpine
# Install additional tools if needed # Install additional tools if needed
RUN apk add --no-cache curl RUN apk add --no-cache curl

View file

@ -1,7 +1,7 @@
# Rust testing environment for CI/CD # Rust testing environment for CI/CD
ARG CI_HOST=localhost ARG REGISTRY_HOST=localhost
ARG APP_NAME=sharenet ARG OWNER_REPO=owner/repo
FROM ${CI_HOST}:443/${APP_NAME}/rust:1.75-slim FROM ${REGISTRY_HOST}/${OWNER_REPO}/rust:1.75-slim
# Install additional tools needed for testing # Install additional tools needed for testing
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \

View file

@ -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

View file

@ -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

View file

@ -1,7 +1,7 @@
# Node.js testing environment for CI/CD # Node.js testing environment for CI/CD
ARG CI_HOST=localhost ARG REGISTRY_HOST=localhost
ARG APP_NAME=sharenet ARG OWNER_REPO=owner/repo
FROM ${CI_HOST}:443/${APP_NAME}/node:20-slim FROM ${REGISTRY_HOST}/${OWNER_REPO}/node:20-slim
# Install additional tools needed for testing # Install additional tools needed for testing
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \

View file

@ -14,7 +14,7 @@ spec:
- name: POSTGRES_USER - name: POSTGRES_USER
value: "sharenet" value: "sharenet"
- name: POSTGRES_PASSWORD - name: POSTGRES_PASSWORD
value: "changeme" value: "${POSTGRES_PASSWORD}"
ports: ports:
- containerPort: 5432 - containerPort: 5432
protocol: TCP protocol: TCP
@ -42,10 +42,10 @@ spec:
timeoutSeconds: 5 timeoutSeconds: 5
- name: backend - name: backend
image: localhost:4443/sharenet/backend:latest image: ${REGISTRY_HOST}/${OWNER_REPO}/backend:${IMAGE_TAG}
env: env:
- name: DATABASE_URL - name: DATABASE_URL
value: "postgresql://sharenet:changeme@localhost:5432/sharenet" value: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}"
- name: RUST_LOG - name: RUST_LOG
value: "info" value: "info"
- name: RUST_BACKTRACE - name: RUST_BACKTRACE
@ -70,7 +70,7 @@ spec:
timeoutSeconds: 5 timeoutSeconds: 5
- name: frontend - name: frontend
image: localhost:4443/sharenet/frontend:latest image: ${REGISTRY_HOST}/${OWNER_REPO}/frontend:${IMAGE_TAG}
env: env:
- name: NEXT_PUBLIC_API_HOST - name: NEXT_PUBLIC_API_HOST
value: "localhost" value: "localhost"

View file

@ -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

View file

@ -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" }] }
}
}

View file

@ -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

View file

@ -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;
}
}

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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."