Security improvements #1
Some checks are pending
CI/CD Pipeline with Secure Ephemeral PiP / test-backend (push) Waiting to run
CI/CD Pipeline with Secure Ephemeral PiP / test-frontend (push) Blocked by required conditions
CI/CD Pipeline with Secure Ephemeral PiP / build-backend (push) Blocked by required conditions
CI/CD Pipeline with Secure Ephemeral PiP / build-frontend (push) Blocked by required conditions
CI/CD Pipeline with Secure Ephemeral PiP / cleanup (push) Blocked by required conditions

This commit is contained in:
continuist 2025-09-04 22:15:49 -04:00
parent 4270c036f6
commit 6624c2a340
3 changed files with 58 additions and 71 deletions

View file

@ -1,4 +1,4 @@
name: CI/CD Pipeline with Ephemeral PiP name: CI/CD Pipeline with Secure Ephemeral PiP
on: on:
push: push:
@ -10,6 +10,7 @@ env:
REGISTRY: ${{ secrets.REGISTRY_HOST }} REGISTRY: ${{ secrets.REGISTRY_HOST }}
APP_NAME: ${{ secrets.APP_NAME }} APP_NAME: ${{ secrets.APP_NAME }}
IMAGE_TAG: ${{ github.sha }} IMAGE_TAG: ${{ github.sha }}
RUN_ID: ${{ github.run_id }}
jobs: jobs:
test-backend: test-backend:
@ -28,57 +29,55 @@ jobs:
chmod +x ./pip_ready.sh chmod +x ./pip_ready.sh
./pip_ready.sh ./pip_ready.sh
- name: Setup SSH for production deployment - name: Setup SSH with pinned known_hosts
run: | run: |
mkdir -p ~/.ssh mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H ${{ secrets.PRODUCTION_IP }} >> ~/.ssh/known_hosts echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
- name: Login to Forgejo Container Registry - name: Create integration test network
run: | run: |
podman exec ci-pip podman login ${{ secrets.REGISTRY_HOST }} \ podman exec ci-pip-$RUN_ID podman network create integ-$RUN_ID
-u ${{ secrets.REGISTRY_USERNAME }} \
-p ${{ secrets.REGISTRY_TOKEN }}
- name: Start PostgreSQL for integration tests - name: Start PostgreSQL on internal network
run: | run: |
podman exec ci-pip podman run -d \ podman exec ci-pip-$RUN_ID podman run -d \
--name test-postgres \ --name test-postgres \
--network integ-$RUN_ID \
-e POSTGRES_PASSWORD=testpassword \ -e POSTGRES_PASSWORD=testpassword \
-e POSTGRES_USER=testuser \ -e POSTGRES_USER=testuser \
-e POSTGRES_DB=testdb \ -e POSTGRES_DB=testdb \
-p 5432:5432 \
postgres:15-alpine postgres:15-alpine
- name: Wait for PostgreSQL to be ready - name: Wait for PostgreSQL to be ready
run: | run: |
podman exec ci-pip timeout 60 bash -c 'until podman exec test-postgres pg_isready -h localhost -p 5432 -U testuser; do sleep 1; done' podman exec ci-pip-$RUN_ID timeout 60 bash -c 'until podman exec test-postgres pg_isready -h test-postgres -p 5432 -U testuser; do sleep 1; done'
- name: Run backend unit tests - name: Run backend unit tests
run: | run: |
podman exec ci-pip podman run --rm \ podman exec ci-pip-$RUN_ID podman run --rm \
-v $(pwd):/workspace \ -v $(pwd):/workspace \
-w /workspace \ -w /workspace \
rust:latest \ rust:latest \
sh -c "cargo test --lib -- --test-threads=1" sh -c "cargo test --lib -- --test-threads=1"
- name: Run backend integration tests - name: Run backend integration tests
env:
DATABASE_URL: postgres://testuser:testpassword@localhost:5432/testdb
run: | run: |
podman exec ci-pip podman run --rm \ podman exec ci-pip-$RUN_ID podman run --rm \
-v $(pwd):/workspace \ -v $(pwd):/workspace \
-w /workspace \ -w /workspace \
-e DATABASE_URL="$DATABASE_URL" \ -e DATABASE_URL=postgres://testuser:testpassword@test-postgres:5432/testdb \
rust:latest \ rust:latest \
sh -c "cargo test --test '*' -- --test-threads=1" sh -c "cargo test --test '*' -- --test-threads=1"
- name: Cleanup test database - name: Cleanup test resources
if: always() if: always()
run: | run: |
podman exec ci-pip podman stop test-postgres 2>/dev/null || true podman exec ci-pip-$RUN_ID podman stop test-postgres 2>/dev/null || true
podman exec ci-pip podman rm test-postgres 2>/dev/null || true podman exec ci-pip-$RUN_ID podman rm test-postgres 2>/dev/null || true
podman exec ci-pip-$RUN_ID podman network rm integ-$RUN_ID 2>/dev/null || true
test-frontend: test-frontend:
runs-on: [self-hosted, ci] runs-on: [self-hosted, ci]
@ -99,7 +98,7 @@ jobs:
- name: Run frontend tests in PiP - name: Run frontend tests in PiP
run: | run: |
podman exec ci-pip podman run --rm \ podman exec ci-pip-$RUN_ID podman run --rm \
-v $(pwd):/workspace \ -v $(pwd):/workspace \
-w /workspace \ -w /workspace \
node:20 \ node:20 \
@ -122,21 +121,21 @@ jobs:
chmod +x ./pip_ready.sh chmod +x ./pip_ready.sh
./pip_ready.sh ./pip_ready.sh
- name: Login to Forgejo Container Registry - name: Login to Forgejo Container Registry securely
run: | run: |
podman exec ci-pip podman login ${{ secrets.REGISTRY_HOST }} \ echo "${{ secrets.REGISTRY_TOKEN }}" | podman exec -i ci-pip-$RUN_ID podman login ${{ secrets.REGISTRY_HOST }} \
-u ${{ secrets.REGISTRY_USERNAME }} \ -u ${{ secrets.REGISTRY_USERNAME }} \
-p ${{ secrets.REGISTRY_TOKEN }} --password-stdin
- name: Build backend image - name: Build backend image
run: | run: |
podman exec ci-pip podman build \ podman exec ci-pip-$RUN_ID podman build \
-t ${{ secrets.REGISTRY_HOST }}/${{ secrets.APP_NAME }}/backend:${{ github.sha }} \ -t ${{ secrets.REGISTRY_HOST }}/${{ secrets.APP_NAME }}/backend:${{ github.sha }} \
-f Dockerfile.backend . -f Dockerfile.backend .
- name: Push backend image - name: Push backend image
run: | run: |
podman exec ci-pip podman push \ podman exec ci-pip-$RUN_ID podman push \
${{ secrets.REGISTRY_HOST }}/${{ secrets.APP_NAME }}/backend:${{ github.sha }} ${{ secrets.REGISTRY_HOST }}/${{ secrets.APP_NAME }}/backend:${{ github.sha }}
build-frontend: build-frontend:
@ -156,21 +155,21 @@ jobs:
chmod +x ./pip_ready.sh chmod +x ./pip_ready.sh
./pip_ready.sh ./pip_ready.sh
- name: Login to Forgejo Container Registry - name: Login to Forgejo Container Registry securely
run: | run: |
podman exec ci-pip podman login ${{ secrets.REGISTRY_HOST }} \ echo "${{ secrets.REGISTRY_TOKEN }}" | podman exec -i ci-pip-$RUN_ID podman login ${{ secrets.REGISTRY_HOST }} \
-u ${{ secrets.REGISTRY_USERNAME }} \ -u ${{ secrets.REGISTRY_USERNAME }} \
-p ${{ secrets.REGISTRY_TOKEN }} --password-stdin
- name: Build frontend image - name: Build frontend image
run: | run: |
podman exec ci-pip podman build \ podman exec ci-pip-$RUN_ID podman build \
-t ${{ secrets.REGISTRY_HOST }}/${{ secrets.APP_NAME }}/frontend:${{ github.sha }} \ -t ${{ secrets.REGISTRY_HOST }}/${{ secrets.APP_NAME }}/frontend:${{ github.sha }} \
-f Dockerfile.frontend . -f Dockerfile.frontend .
- name: Push frontend image - name: Push frontend image
run: | run: |
podman exec ci-pip podman push \ podman exec ci-pip-$RUN_ID podman push \
${{ secrets.REGISTRY_HOST }}/${{ secrets.APP_NAME }}/frontend:${{ github.sha }} ${{ secrets.REGISTRY_HOST }}/${{ secrets.APP_NAME }}/frontend:${{ github.sha }}
cleanup: cleanup:
@ -178,7 +177,7 @@ jobs:
needs: [build-backend, build-frontend] needs: [build-backend, build-frontend]
if: always() if: always()
steps: steps:
- name: Cleanup PiP container - name: Cleanup PiP container and socket
run: | run: |
podman rm -f ci-pip 2>/dev/null || true podman rm -f ci-pip-$RUN_ID 2>/dev/null || true
rm -f /tmp/podman.sock 2>/dev/null || true rm -f ${XDG_RUNTIME_DIR}/podman-host/podman.sock 2>/dev/null || true

View file

@ -1,16 +1,17 @@
#!/bin/bash #!/bin/bash
set -euo pipefail set -euo pipefail
# pip_ready.sh - Readiness probe for PiP container # pip_ready.sh - Socket-only readiness probe for PiP container
# Checks if the Podman-in-Podman container is ready for CI operations # Checks if PiP container can connect to host Podman via UNIX socket
PIP_CONTAINER_NAME="ci-pip" RUN_ID="${GITHUB_RUN_ID:-local}"
MAX_RETRIES=30 PIP_CONTAINER_NAME="ci-pip-${RUN_ID}"
MAX_RETRIES=15
RETRY_DELAY=2 RETRY_DELAY=2
# Function to check PiP readiness # Function to check PiP socket connectivity only
check_pip_ready() { check_pip_ready() {
echo "🔍 Checking PiP container readiness..." echo "🔍 Checking PiP container socket connectivity..."
# Check if container exists and is running # Check if container exists and is running
if ! podman inspect "${PIP_CONTAINER_NAME}" --format '{{.State.Status}}' 2>/dev/null | grep -q running; then if ! podman inspect "${PIP_CONTAINER_NAME}" --format '{{.State.Status}}' 2>/dev/null | grep -q running; then
@ -18,22 +19,19 @@ check_pip_ready() {
return 1 return 1
fi fi
# Test basic Podman command inside PiP # Test basic Podman command connectivity (socket only)
if ! podman exec "${PIP_CONTAINER_NAME}" podman info --format json >/dev/null 2>&1; then if ! podman exec "${PIP_CONTAINER_NAME}" podman info --format json >/dev/null 2>&1; then
echo "⚠️ PiP container running but Podman not responsive" echo "⚠️ PiP container running but socket connectivity failed"
return 1 return 1
fi fi
# Test image pulling capability (network test) # Test version command (socket connectivity verification)
if ! podman exec "${PIP_CONTAINER_NAME}" podman pull --quiet alpine:latest >/dev/null 2>&1; then if ! podman exec "${PIP_CONTAINER_NAME}" podman version >/dev/null 2>&1; then
echo "⚠️ PiP container ready but network access test failed" echo "⚠️ PiP container socket connection unstable"
return 1 return 1
fi fi
# Clean up test image echo "✅ PiP container ready with socket connectivity"
podman exec "${PIP_CONTAINER_NAME}" podman rmi alpine:latest 2>/dev/null || true
echo "✅ PiP container ready and fully operational"
return 0 return 0
} }
@ -45,13 +43,13 @@ while [[ ${attempt} -le ${MAX_RETRIES} ]]; do
exit 0 exit 0
fi fi
echo "⏳ PiP not ready yet (attempt ${attempt}/${MAX_RETRIES}), retrying in ${RETRY_DELAY}s..." echo "⏳ PiP socket connectivity not ready yet (attempt ${attempt}/${MAX_RETRIES}), retrying in ${RETRY_DELAY}s..."
sleep ${RETRY_DELAY} sleep ${RETRY_DELAY}
attempt=$((attempt + 1)) attempt=$((attempt + 1))
done done
# If we reach here, all retries failed # If we reach here, all retries failed
echo "❌ ERROR: PiP container failed to become ready after ${MAX_RETRIES} attempts" echo "❌ ERROR: PiP container failed to establish socket connectivity after ${MAX_RETRIES} attempts"
echo "📋 Container status:" echo "📋 Container status:"
podman ps -a --filter "name=${PIP_CONTAINER_NAME}" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" || true podman ps -a --filter "name=${PIP_CONTAINER_NAME}" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" || true

View file

@ -1,11 +1,12 @@
#!/bin/bash #!/bin/bash
set -euo pipefail set -euo pipefail
# secure_pip_setup.sh - Idempotent setup for ephemeral Podman-in-Podman container # secure_pip_setup.sh - Secure PiP client container setup
# This script creates a secure PiP container for CI operations with no network exposure # Creates ephemeral PiP container that connects to host Podman via UNIX socket
# Configuration # Configuration
PIP_CONTAINER_NAME="ci-pip" RUN_ID="${GITHUB_RUN_ID:-local}"
PIP_CONTAINER_NAME="ci-pip-${RUN_ID}"
SOCKET_DIR="${XDG_RUNTIME_DIR}/podman-host" SOCKET_DIR="${XDG_RUNTIME_DIR}/podman-host"
SOCKET_PATH="${SOCKET_DIR}/podman.sock" SOCKET_PATH="${SOCKET_DIR}/podman.sock"
PODMAN_IMAGE="quay.io/podman/stable:latest" PODMAN_IMAGE="quay.io/podman/stable:latest"
@ -38,8 +39,8 @@ fi
echo "🔒 Setting secure socket permissions..." echo "🔒 Setting secure socket permissions..."
chmod 660 "${SOCKET_PATH}" chmod 660 "${SOCKET_PATH}"
# Create ephemeral PiP container with maximum security # Create ephemeral PiP container as client only (no inner daemon)
echo "🐳 Creating secure PiP container..." echo "🐳 Creating secure PiP client container..."
podman run -d \ podman run -d \
--name "${PIP_CONTAINER_NAME}" \ --name "${PIP_CONTAINER_NAME}" \
--security-opt=no-new-privileges \ --security-opt=no-new-privileges \
@ -49,12 +50,13 @@ podman run -d \
--tmpfs /run:rw,size=64M \ --tmpfs /run:rw,size=64M \
--tmpfs /tmp:rw,size=256M \ --tmpfs /tmp:rw,size=256M \
-v "${SOCKET_PATH}:/var/run/podman.sock" \ -v "${SOCKET_PATH}:/var/run/podman.sock" \
-e CONTAINER_HOST="unix:///var/run/podman.sock" \
"${PODMAN_IMAGE}" \ "${PODMAN_IMAGE}" \
podman system service --time=0 unix:///var/run/podman.sock sleep infinity
# Wait for container to start # Wait for container to start
echo "⏳ Waiting for PiP container to start..." echo "⏳ Waiting for PiP container to start..."
sleep 5 sleep 3
# Verify container is running # Verify container is running
if ! podman inspect "${PIP_CONTAINER_NAME}" --format '{{.State.Status}}' | grep -q running; then if ! podman inspect "${PIP_CONTAINER_NAME}" --format '{{.State.Status}}' | grep -q running; then
@ -64,19 +66,7 @@ if ! podman inspect "${PIP_CONTAINER_NAME}" --format '{{.State.Status}}' | grep
exit 1 exit 1
fi fi
# Kill the background host service (PiP container now handles requests) echo "🎉 Secure PiP client container setup complete!"
echo "🔄 Switching to PiP container for Podman operations..."
kill ${HOST_PODMAN_PID} 2>/dev/null || true
# Test PiP connectivity
echo "✅ Testing PiP connectivity..."
if ! podman exec "${PIP_CONTAINER_NAME}" podman version >/dev/null 2>&1; then
echo "❌ ERROR: PiP container not responding to Podman commands"
podman logs "${PIP_CONTAINER_NAME}" || true
exit 1
fi
echo "🎉 Secure PiP container setup complete!"
echo " Container: ${PIP_CONTAINER_NAME}" echo " Container: ${PIP_CONTAINER_NAME}"
echo " Socket: ${SOCKET_PATH}" echo " Socket: ${SOCKET_PATH}"
echo " Security: No network, no capabilities, read-only rootfs" echo " Security: No network, no capabilities, read-only rootfs, client-only"