Improve security for production podman
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

This commit is contained in:
continuist 2025-09-06 13:15:05 -04:00
parent 1ea4dc32e5
commit 411d9f3f35
3 changed files with 192 additions and 128 deletions

View file

@ -1359,7 +1359,7 @@ The CI/CD pipeline uses ephemeral Podman-in-Podman containers with a secure four
#### 7.5 Production Deployment Architecture #### 7.5 Production Deployment Architecture
The production deployment uses a separate pod configuration (`prod-pod.yaml`) that pulls built images from the Forgejo Container Registry and deploys the complete application stack. The production deployment uses a separate pod configuration (`prod-pod.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
@ -1370,7 +1370,7 @@ The production deployment uses a separate pod configuration (`prod-pod.yaml`) th
**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 Forgejo Container Registry 2. **Image Pull**: Pulls latest images from Forgejo Container Registry
3. **Stack Deployment**: Uses `prod-pod.yaml` to deploy complete stack 3. **Stack Deployment**: Uses `prod-pod.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
**Key Benefits:** **Key Benefits:**
@ -1992,7 +1992,7 @@ sudo journalctl -u forgejo-runner.service -f --no-pager
When the workflow runs, it will: When the workflow runs, it will:
1. Pull the latest Docker images from Forgejo Container Registry 1. Pull the latest Docker images from Forgejo Container Registry
2. Use the `prod-pod.yaml` file to deploy the application stack 2. Use the `prod-pod.yml` file to deploy the application stack
3. Create the necessary environment variables for production deployment 3. Create the necessary environment variables for production deployment
4. Verify that all services are healthy after deployment 4. Verify that all services are healthy after deployment
@ -2000,7 +2000,7 @@ The production runner will automatically handle the deployment process when you
#### 12.6 Understanding the Production Pod Setup #### 12.6 Understanding the Production Pod Setup
The `prod-pod.yaml` file is specifically designed for production deployment and uses Kubernetes pod specifications: The `prod-pod.yml` file is specifically designed for production deployment and uses Kubernetes pod specifications:
**Key Features**: **Key Features**:
- **Image-based deployment**: Uses pre-built images from Forgejo Container Registry instead of building from source - **Image-based deployment**: Uses pre-built images from Forgejo Container Registry instead of building from source
@ -2019,7 +2019,7 @@ The `prod-pod.yaml` file is specifically designed for production deployment and
**Deployment Process**: **Deployment Process**:
1. The production runner pulls the latest images from Forgejo Container Registry 1. The production runner pulls the latest images from Forgejo Container Registry
2. Creates environment variables for the deployment 2. Creates environment variables for the deployment
3. Runs `podman play kube prod-pod.yaml` 3. Runs `podman play kube prod-pod.yml`
4. Waits for all services to be healthy 4. Waits for all services to be healthy
5. Verifies the deployment was successful 5. Verifies the deployment was successful
@ -2424,12 +2424,12 @@ After successful setup, you can clean up the installation files to remove sensit
|---------|--------------------------|--------------------------| |---------|--------------------------|--------------------------|
| Test | All integration/unit tests| `ci-pod.yaml` | | Test | All integration/unit tests| `ci-pod.yaml` |
| Build | Build & push images | Podman build & push | | Build | Build & push images | Podman build & push |
| Deploy | Deploy to production | `prod-pod.yaml` | | Deploy | Deploy to production | `prod-pod.yml` |
**How it works:** **How it works:**
- **Test:** The workflow spins up a full test environment using `ci-pod.yaml` (Postgres, backend, frontend, etc.) and runs all tests inside containers. - **Test:** The workflow spins up a full test environment using `ci-pod.yaml` (Postgres, backend, frontend, etc.) and runs all tests inside containers.
- **Build:** If tests pass, the workflow uses Podman to build backend and frontend images and push them to Forgejo Container Registry. - **Build:** If tests pass, the workflow uses Podman to build backend and frontend images and push them to Forgejo Container Registry.
- **Deploy:** The production runner pulls images from Forgejo Container Registry and deploys the stack using `prod-pod.yaml`. - **Deploy:** The production runner pulls images from Forgejo Container Registry and deploys the stack using `prod-pod.yml`.
**Expected Output:** **Expected Output:**
- Each stage runs in its own isolated environment. - Each stage runs in its own isolated environment.

185
deploy/prod-pod.yml Normal file
View file

@ -0,0 +1,185 @@
apiVersion: v1
kind: Pod
metadata:
name: prod-pod
labels:
app: sharenet-production
annotations:
io.containers.no-new-privileges: "true"
spec:
hostname: prod-pod
# Security: run as non-root user with specific UID/GID
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: postgres
image: localhost/postgres:deployed
# Security: drop all capabilities, read-only root filesystem except data volume
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
envFrom:
- secretRef:
name: postgres-secrets
ports:
- containerPort: 5432
protocol: TCP
volumeMounts:
- name: pgdata
mountPath: /var/lib/postgresql/data
readOnly: false
# Health checks
livenessProbe:
exec:
command: ["pg_isready", "-U", "sharenet"]
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
exec:
command: ["pg_isready", "-U", "sharenet"]
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
# Resource limits
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
- name: backend
image: localhost/backend:deployed
# Security: drop all capabilities, read-only root filesystem
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
envFrom:
- secretRef:
name: backend-secrets
ports:
- containerPort: 3001
protocol: TCP
# Health checks
livenessProbe:
httpGet:
path: /health
port: 3001
scheme: HTTP
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 3001
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
# Resource limits
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "250m"
- name: frontend
image: localhost/frontend:deployed
# Security: drop all capabilities, read-only root filesystem
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
envFrom:
- secretRef:
name: frontend-secrets
ports:
- containerPort: 3000
protocol: TCP
# Resource limits
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "250m"
- name: nginx
image: localhost/nginx:deployed
# Security: drop all capabilities, read-only root filesystem
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
ports:
- containerPort: 80
protocol: TCP
hostPort: 80
- containerPort: 443
protocol: TCP
hostPort: 443
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
# Health check
livenessProbe:
httpGet:
path: /healthz
port: 80
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
# Resource limits
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
volumes:
- name: pgdata
hostPath:
path: /opt/sharenet/volumes/pgdata
type: Directory
- name: nginx-conf
hostPath:
path: /opt/sharenet/nginx
type: Directory
- name: nginx-cache
hostPath:
path: /opt/sharenet/volumes/nginx-cache
type: Directory
- name: letsencrypt
hostPath:
path: /etc/letsencrypt
type: Directory

View file

@ -1,121 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: sharenet-production-pod
labels:
app: sharenet-production
spec:
containers:
- name: postgres
image: postgres:15-alpine
env:
- name: POSTGRES_DB
value: "sharenet"
- name: POSTGRES_USER
value: "sharenet"
- name: POSTGRES_PASSWORD
value: "${POSTGRES_PASSWORD}"
ports:
- containerPort: 5432
protocol: TCP
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
livenessProbe:
exec:
command:
- pg_isready
- -U
- sharenet
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
exec:
command:
- pg_isready
- -U
- sharenet
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
- name: backend
image: ${REGISTRY_HOST}/${OWNER_REPO}/backend:${IMAGE_TAG}
env:
- name: DATABASE_URL
value: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}"
- name: RUST_LOG
value: "info"
- name: RUST_BACKTRACE
value: "1"
ports:
- containerPort: 3001
protocol: TCP
livenessProbe:
httpGet:
path: /health
port: 3001
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 3001
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
- name: frontend
image: ${REGISTRY_HOST}/${OWNER_REPO}/frontend:${IMAGE_TAG}
env:
- name: NEXT_PUBLIC_API_HOST
value: "localhost"
- name: NEXT_PUBLIC_API_PORT
value: "3001"
- name: NODE_ENV
value: "production"
ports:
- containerPort: 3000
protocol: TCP
dependsOn:
- name: backend
condition: Ready
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
protocol: TCP
- containerPort: 443
protocol: TCP
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
readOnly: true
- name: nginx-ssl
mountPath: /etc/nginx/ssl
readOnly: true
dependsOn:
- name: frontend
condition: Ready
- name: backend
condition: Ready
volumes:
- name: postgres-data
hostPath:
path: /var/lib/postgresql/data
type: Directory
- name: nginx-config
hostPath:
path: /opt/sharenet/nginx/nginx.conf
type: File
- name: nginx-ssl
hostPath:
path: /opt/sharenet/nginx/ssl
type: Directory