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
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:**
- **PostgreSQL**: Production database with persistent storage
@ -1370,7 +1370,7 @@ The production deployment uses a separate pod configuration (`prod-pod.yaml`) th
**Deployment Flow:**
1. **Production Runner**: Runs on Production Linode with `production` label
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
**Key Benefits:**
@ -1992,7 +1992,7 @@ sudo journalctl -u forgejo-runner.service -f --no-pager
When the workflow runs, it will:
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
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
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**:
- **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**:
1. The production runner pulls the latest images from Forgejo Container Registry
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
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` |
| Build | Build & push images | Podman build & push |
| Deploy | Deploy to production | `prod-pod.yaml` |
| Deploy | Deploy to production | `prod-pod.yml` |
**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.
- **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:**
- 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