diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 68341b9..b05780e 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -31,6 +31,18 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Resolve Podman user socket for this runner + run: | + SVC_USER="ci-service" # CI runner user on the CI host + uid=$(id -u "$SVC_USER") + echo "PODMAN_SOCK=/run/user/${uid}/podman/podman.sock" >> "$GITHUB_ENV" + + - name: Verify runner wiring to Podman + run: | + podman --version + test -S "${PODMAN_SOCK}" || { echo "Missing socket ${PODMAN_SOCK}"; exit 1; } + # Optional: sanity poke of the service via PiP later + - name: Network/DNS sanity from job container run: | getent hosts git.gcdo.org || true @@ -46,6 +58,7 @@ jobs: - name: Setup ephemeral PiP container env: PODMAN_CLIENT_IMG_DIGEST: ${{ env.PODMAN_CLIENT_IMG_DIGEST }} + SOCKET_PATH: ${{ env.PODMAN_SOCK }} run: | chmod +x ./secure_pip_setup.sh ./secure_pip_setup.sh @@ -57,6 +70,9 @@ jobs: chmod +x ./pip_ready.sh ./pip_ready.sh + - name: Network sanity from PiP + run: podman exec ci-pip-${{ env.RUN_ID }} sh -lc 'getent hosts git.gcdo.org && curl -sS -o /dev/null -w "status=%{http_code}\n" https://git.gcdo.org/api/healthz' + - name: Configure Git for token-based authentication run: | git config --global url."https://${{ secrets.REGISTRY_USERNAME }}:${{ secrets.REGISTRY_TOKEN }}@${{ env.REGISTRY }}".insteadOf "https://${{ env.REGISTRY }}" @@ -85,7 +101,7 @@ jobs: env: WORKSPACE: ${{ github.workspace }} run: | - podman exec ci-pip-${{ env.RUN_ID }} sh -lc ' + podman exec -e WORKSPACE="${WORKSPACE}" ci-pip-${{ env.RUN_ID }} sh -lc ' podman run --rm \ -v "$WORKSPACE":/workspace \ -w /workspace \ @@ -95,11 +111,10 @@ jobs: - name: Run backend integration tests env: WORKSPACE: ${{ github.workspace }} - RUN_ID: ${{ env.RUN_ID }} run: | - podman exec ci-pip-${{ env.RUN_ID }} sh -lc ' + podman exec -e WORKSPACE="${WORKSPACE}" ci-pip-${{ env.RUN_ID }} sh -lc ' podman run --rm \ - --network integ-'"$RUN_ID"' \ + --network integ-${{ env.RUN_ID }} \ -v "$WORKSPACE":/workspace \ -w /workspace \ -e DATABASE_URL=postgres://postgres:password@test-postgres:5432/sharenet_test \ @@ -122,6 +137,18 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Resolve Podman user socket for this runner + run: | + SVC_USER="ci-service" # CI runner user on the CI host + uid=$(id -u "$SVC_USER") + echo "PODMAN_SOCK=/run/user/${uid}/podman/podman.sock" >> "$GITHUB_ENV" + + - name: Verify runner wiring to Podman + run: | + podman --version + test -S "${PODMAN_SOCK}" || { echo "Missing socket ${PODMAN_SOCK}"; exit 1; } + # Optional: sanity poke of the service via PiP later + - name: Verify pinned digests provided run: | for v in NODE_IMG_DIGEST PODMAN_CLIENT_IMG_DIGEST; do @@ -132,6 +159,7 @@ jobs: - name: Setup ephemeral PiP container env: PODMAN_CLIENT_IMG_DIGEST: ${{ env.PODMAN_CLIENT_IMG_DIGEST }} + SOCKET_PATH: ${{ env.PODMAN_SOCK }} run: | chmod +x ./secure_pip_setup.sh ./secure_pip_setup.sh @@ -143,11 +171,14 @@ jobs: chmod +x ./pip_ready.sh ./pip_ready.sh + - name: Network sanity from PiP + run: podman exec ci-pip-${{ env.RUN_ID }} sh -lc 'getent hosts git.gcdo.org && curl -sS -o /dev/null -w "status=%{http_code}\n" https://git.gcdo.org/api/healthz' + - name: Run frontend tests (digest-pinned) env: WORKSPACE: ${{ github.workspace }} run: | - podman exec ci-pip-${{ env.RUN_ID }} sh -lc ' + podman exec -e WORKSPACE="${WORKSPACE}" ci-pip-${{ env.RUN_ID }} sh -lc ' podman run --rm \ -v "$WORKSPACE":/workspace \ -w /workspace \ @@ -164,9 +195,22 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Resolve Podman user socket for this runner + run: | + SVC_USER="ci-service" # CI runner user on the CI host + uid=$(id -u "$SVC_USER") + echo "PODMAN_SOCK=/run/user/${uid}/podman/podman.sock" >> "$GITHUB_ENV" + + - name: Verify runner wiring to Podman + run: | + podman --version + test -S "${PODMAN_SOCK}" || { echo "Missing socket ${PODMAN_SOCK}"; exit 1; } + # Optional: sanity poke of the service via PiP later + - name: Setup ephemeral PiP container env: PODMAN_CLIENT_IMG_DIGEST: ${{ env.PODMAN_CLIENT_IMG_DIGEST }} + SOCKET_PATH: ${{ env.PODMAN_SOCK }} run: | chmod +x ./secure_pip_setup.sh ./secure_pip_setup.sh @@ -178,6 +222,9 @@ jobs: chmod +x ./pip_ready.sh ./pip_ready.sh + - name: Network sanity from PiP + run: podman exec ci-pip-${{ env.RUN_ID }} sh -lc 'getent hosts git.gcdo.org && curl -sS -o /dev/null -w "status=%{http_code}\n" https://git.gcdo.org/api/healthz' + - name: Login to Forgejo Container Registry securely env: REGISTRY_HOST: ${{ env.REGISTRY }} @@ -215,9 +262,22 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Resolve Podman user socket for this runner + run: | + SVC_USER="ci-service" # CI runner user on the CI host + uid=$(id -u "$SVC_USER") + echo "PODMAN_SOCK=/run/user/${uid}/podman/podman.sock" >> "$GITHUB_ENV" + + - name: Verify runner wiring to Podman + run: | + podman --version + test -S "${PODMAN_SOCK}" || { echo "Missing socket ${PODMAN_SOCK}"; exit 1; } + # Optional: sanity poke of the service via PiP later + - name: Setup ephemeral PiP container env: PODMAN_CLIENT_IMG_DIGEST: ${{ env.PODMAN_CLIENT_IMG_DIGEST }} + SOCKET_PATH: ${{ env.PODMAN_SOCK }} run: | chmod +x ./secure_pip_setup.sh ./secure_pip_setup.sh @@ -229,6 +289,9 @@ jobs: chmod +x ./pip_ready.sh ./pip_ready.sh + - name: Network sanity from PiP + run: podman exec ci-pip-${{ env.RUN_ID }} sh -lc 'getent hosts git.gcdo.org && curl -sS -o /dev/null -w "status=%{http_code}\n" https://git.gcdo.org/api/healthz' + - name: Login to Forgejo Container Registry securely env: REGISTRY_HOST: ${{ env.REGISTRY }} @@ -292,11 +355,31 @@ jobs: fi } - # Substitute environment variables in prod-pod.yml - envsubst < deploy/prod-pod.yml > prod-pod-deploy.yml + - name: Resolve Podman user socket (prod) + run: | + SVC_USER="prod-service" + uid=$(id -u "$SVC_USER") + echo "PODMAN_SOCK=/run/user/${uid}/podman/podman.sock" >> "$GITHUB_ENV" + + - name: Verify runner wiring to Podman + run: | + podman --version + test -S "${PODMAN_SOCK}" || { echo "Missing socket ${PODMAN_SOCK}"; exit 1; } + # Optional: sanity poke of the service via PiP later + + - name: Render prod spec + run: envsubst < deploy/prod-pod.yml > prod-pod-deploy.yml + + - name: Apply with Podman (no registry login needed) + env: + PODMAN_CLIENT_IMG_DIGEST: ${{ env.PODMAN_CLIENT_IMG_DIGEST }} + run: | + cat prod-pod-deploy.yml | \ + podman run --rm -i \ + -v "${PODMAN_SOCK}:${PODMAN_SOCK}" \ + -e "CONTAINER_HOST=unix://${PODMAN_SOCK}" \ + "${PODMAN_CLIENT_IMG_DIGEST}" \ + sh -lc 'cat >/tmp/spec.yml; podman play kube --replace /tmp/spec.yml' - # Deploy the pod (replace existing if it exists) - podman play kube --replace prod-pod-deploy.yml - - # Clean up generated file - rm -f prod-pod-deploy.yml \ No newline at end of file + - name: Clean up generated file + run: rm -f prod-pod-deploy.yml \ No newline at end of file diff --git a/deploy/prod-pod.yml b/deploy/prod-pod.yml index feeb599..fdf598c 100644 --- a/deploy/prod-pod.yml +++ b/deploy/prod-pod.yml @@ -8,18 +8,15 @@ metadata: io.containers.no-new-privileges: "true" spec: hostname: sharenet-production-pod - # Security: run as non-root user with specific UID/GID (matches PROD_SERVICE_USER) - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 containers: - name: postgres image: ${REGISTRY_HOST}/${APP_NAME}/postgres:${IMAGE_TAG} # Security: drop all capabilities, read-only root filesystem except data volume securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: @@ -29,26 +26,27 @@ spec: value: ${POSTGRES_DATABASE_NAME} - name: POSTGRES_USER value: ${POSTGRES_USERNAME} + - name: POSTGRES_PASSWORD + value: ${POSTGRES_PASSWORD} - name: PGDATA value: /var/lib/postgresql/data/pgdata ports: - containerPort: ${POSTGRES_PORT} protocol: TCP volumeMounts: - - name: pgdata - mountPath: /var/lib/postgresql/data - readOnly: false + - { name: pgdata, mountPath: /var/lib/postgresql/data, readOnly: false } + - { name: postgres-run, mountPath: /var/run/postgresql, readOnly: false } # Health checks livenessProbe: exec: - command: ["pg_isready", "-U", "sharenet"] + command: ["pg_isready", "-U", "${POSTGRES_USERNAME}"] initialDelaySeconds: 30 periodSeconds: 30 timeoutSeconds: 10 failureThreshold: 3 readinessProbe: exec: - command: ["pg_isready", "-U", "sharenet"] + command: ["pg_isready", "-U", "${POSTGRES_USERNAME}"] initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 5 @@ -65,23 +63,29 @@ spec: image: ${REGISTRY_HOST}/${APP_NAME}/backend:${IMAGE_TAG} # Security: drop all capabilities, read-only root filesystem securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: ["ALL"] env: - name: DATABASE_URL - value: postgres://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@{POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DATABASE_NAME}?sslmode=disable + value: "postgres://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@localhost:${POSTGRES_PORT}/${POSTGRES_DATABASE_NAME}?sslmode=disable" - name: PORT - value: ${PROD_BACKEND_PORT} + value: "${PROD_BACKEND_PORT}" ports: - containerPort: ${PROD_BACKEND_PORT} protocol: TCP + volumeMounts: + - { name: backend-tmp, mountPath: /tmp } + - { name: backend-tmp, mountPath: /run } # Health checks livenessProbe: httpGet: path: /health - port: ${PROD_BACKEND_PORT} + port: "${PROD_BACKEND_PORT}" scheme: HTTP initialDelaySeconds: 30 periodSeconds: 30 @@ -90,7 +94,7 @@ spec: readinessProbe: httpGet: path: /health - port: ${PROD_BACKEND_PORT} + port: "${PROD_BACKEND_PORT}" scheme: HTTP initialDelaySeconds: 10 periodSeconds: 10 @@ -108,6 +112,9 @@ spec: image: ${REGISTRY_HOST}/${APP_NAME}/frontend:${IMAGE_TAG} # Security: drop all capabilities, read-only root filesystem securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: @@ -116,12 +123,15 @@ spec: - name: NEXT_PUBLIC_API_HOST value: ${PROD_BACKEND_HOST} - name: NEXT_PUBLIC_API_PORT - value: ${PROD_BACKEND_PORT} + value: "${PROD_BACKEND_PORT}" - name: PORT - value: ${PROD_FRONTEND_PORT} + value: "${PROD_FRONTEND_PORT}" ports: - containerPort: ${PROD_FRONTEND_PORT} protocol: TCP + volumeMounts: + - { name: frontend-tmp, mountPath: /tmp } + - { name: frontend-tmp, mountPath: /run } # Resource limits resources: requests: @@ -141,25 +151,21 @@ spec: drop: ["ALL"] ports: - containerPort: 80 + hostPort: 8080 protocol: TCP - containerPort: 443 + hostPort: 8443 protocol: TCP volumeMounts: - - name: nginx-conf - mountPath: /etc/nginx/nginx.conf - subPath: nginx.conf - readOnly: true - - name: nginx-cache - mountPath: /var/cache/nginx - readOnly: false - - name: letsencrypt - mountPath: /etc/letsencrypt - readOnly: true + - { name: nginx-run, mountPath: /var/run, readOnly: false } + - { name: nginx-conf, mountPath: /etc/nginx/nginx.conf, readOnly: true, subPath: nginx.conf } + - { name: nginx-cache, mountPath: /var/cache/nginx, readOnly: false } + - { name: letsencrypt, mountPath: /etc/letsencrypt, readOnly: true } # Health check livenessProbe: httpGet: path: /healthz - port: 80 + port: 8090 scheme: HTTP initialDelaySeconds: 10 periodSeconds: 30 @@ -179,6 +185,14 @@ spec: hostPath: path: /opt/sharenet/volumes/pgdata type: DirectoryOrCreate + - name: postgres-run + emptyDir: {} + - name: backend-tmp + emptyDir: { medium: Memory } + - name: frontend-tmp + emptyDir: { medium: Memory } + - name: nginx-run + emptyDir: {} - name: nginx-conf hostPath: path: /opt/sharenet/nginx