Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/container-security-scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Container Image Security Scan

on:
push:
branches: [main]
paths:
- 'apps/api/dockerfile'
- 'apps/api-service/Dockerfile.e2e'
- 'docker-compose.yml'
- '.github/workflows/container-security-scan.yml'
pull_request:
branches: [main]
paths:
- 'apps/api/dockerfile'
- 'apps/api-service/Dockerfile.e2e'
- 'docker-compose.yml'
- '.github/workflows/container-security-scan.yml'

permissions:
contents: read
security-events: write

jobs:
trivy-scan:
name: Build & Vulnerability Scan
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build API container image
uses: docker/build-push-action@v5
with:
context: .
file: apps/api/dockerfile
push: false
load: true
tags: gasguard-api:scan-target
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Run Trivy vulnerability scanner on built image
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: 'gasguard-api:scan-target'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
ignore-unfixed: true

- name: Upload Trivy SARIF report to GitHub Security
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-results.sarif'
48 changes: 40 additions & 8 deletions apps/api-service/Dockerfile.e2e
Original file line number Diff line number Diff line change
@@ -1,23 +1,55 @@
FROM node:18-alpine
# ── Build Stage ──────────────────────────────────────────────────────────
FROM node:20.18.0-alpine3.20 AS builder

WORKDIR /app

# Copy package files
# Copy dependency specifications
COPY package*.json ./
COPY tsconfig*.json ./

# Install dependencies
RUN npm ci --only=production
RUN npm ci && \
npm cache clean --force

# Copy source code
# Copy source and test definitions
COPY src/ ./src/
COPY test/ ./test/

# Build the application
# Compile application
RUN npm run build

# Expose port
# ── Hardened Test Runner Stage ───────────────────────────────────────────
FROM node:20.18.0-alpine3.20 AS runner

# Install tini for signal management
RUN apk add --no-cache tini ca-certificates && \
rm -rf /var/cache/apk/* /tmp/* /root/.npm

WORKDIR /app

# Create unprivileged non-root user
RUN addgroup -g 10001 -S gasguard && \
adduser -S -D -H -u 10001 -h /app -s /sbin/nologin -G gasguard -g "GasGuard Service Account" gasguard

# Copy compiled files and runtime assets with non-root ownership
COPY --from=builder --chown=gasguard:gasguard /app/node_modules ./node_modules
COPY --from=builder --chown=gasguard:gasguard /app/dist ./dist
COPY --from=builder --chown=gasguard:gasguard /app/package*.json ./
COPY --from=builder --chown=gasguard:gasguard /app/test ./test

# Environment defaults
ENV NODE_ENV=test \
PORT=3000

EXPOSE 3000

# Start the application
CMD ["npm", "run", "start:prod"]
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

# Enforce non-root execution
USER gasguard

ENTRYPOINT ["/sbin/tini", "--"]

CMD ["npm", "run", "start:prod"]
80 changes: 51 additions & 29 deletions apps/api/dockerfile
Original file line number Diff line number Diff line change
@@ -1,56 +1,78 @@
# Build stage
FROM node:18-alpine AS builder
# ── Build Stage ──────────────────────────────────────────────────────────
FROM node:20.18.0-alpine3.20 AS builder

WORKDIR /app

# Copy package files
# Install build dependencies
RUN apk add --no-cache python3 make g++

# Copy workspace and lock files
COPY package*.json ./
COPY pnpm-lock.yaml ./
COPY apps/api/package*.json ./apps/api/
COPY libs/engine/package*.json ./libs/engine/
COPY packages/rules/package*.json ./packages/rules/

# Install dependencies
RUN npm ci
# Install dependencies using pinned lockfile
RUN npm ci --ignore-scripts && \
npm cache clean --force

# Copy source code
COPY . .

# Build the API
# Build the API service
RUN npm run build --workspace=apps/api

# Production stage
FROM node:18-alpine
# Prune development dependencies for minimal production bundle
RUN npm prune --production && \
npm cache clean --force

# ── Production Hardened Stage ───────────────────────────────────────────
FROM node:20.18.0-alpine3.20 AS runner

RUN apk add --no-cache tini
# Metadata labels (OCI specification)
LABEL org.opencontainers.image.title="GasGuard API Service" \
org.opencontainers.image.description="Hardened, minimal container image for GasGuard backend API" \
org.opencontainers.image.vendor="MDTechLabs" \
org.opencontainers.image.version="1.0.0"

# Install tini init system and ca-certificates with clean package cache
RUN apk add --no-cache tini ca-certificates && \
rm -rf /var/cache/apk/* /tmp/* /root/.npm

WORKDIR /app

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Create dedicated unprivileged non-root user and group (no shell login)
RUN addgroup -g 10001 -S gasguard && \
adduser -S -D -H -u 10001 -h /app -s /sbin/nologin -G gasguard -g "GasGuard Service Account" gasguard

# Copy package files and installed node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./
COPY --from=builder --chown=nodejs:nodejs /app/apps/api/package*.json ./apps/api/
COPY --from=builder --chown=nodejs:nodejs /app/libs/engine/package*.json ./libs/engine/
COPY --from=builder --chown=nodejs:nodejs /app/packages/rules/package*.json ./packages/rules/
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
# Copy production artifacts with non-root ownership
COPY --from=builder --chown=gasguard:gasguard /app/package*.json ./
COPY --from=builder --chown=gasguard:gasguard /app/apps/api/package*.json ./apps/api/
COPY --from=builder --chown=gasguard:gasguard /app/libs/engine/package*.json ./libs/engine/
COPY --from=builder --chown=gasguard:gasguard /app/packages/rules/package*.json ./packages/rules/
COPY --from=builder --chown=gasguard:gasguard /app/node_modules ./node_modules
COPY --from=builder --chown=gasguard:gasguard /app/dist ./dist

# Copy built application
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
# Create logs directory owned by non-root user
RUN mkdir -p /app/logs && chown -R gasguard:gasguard /app/logs

# Set environment
ENV NODE_ENV=production
ENV PORT=3000
# Environment configuration
ENV NODE_ENV=production \
PORT=3000

# Expose port
# Expose internal listening port
EXPOSE 3000

# Switch to non-root user
USER nodejs
# Health check using non-root wget against internal endpoint
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

# Enforce non-root execution
USER gasguard

# Use tini as init process
# Use tini as PID 1 init process to handle zombie reap and signal forwarding
ENTRYPOINT ["/sbin/tini", "--"]

# Start the API
CMD ["node", "dist/apps/api/main.js"]
# Launch application
CMD ["node", "dist/apps/api/main.js"]
12 changes: 12 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ services:
image: postgres:15-alpine
container_name: gasguard-postgres
restart: unless-stopped
security_opt:
- "no-new-privileges:true"
environment:
POSTGRES_USER: ${DB_USER:-gasguard}
POSTGRES_PASSWORD: ${DB_PASSWORD:-gasguard123}
Expand Down Expand Up @@ -34,6 +36,8 @@ services:
image: redis:7-alpine
container_name: gasguard-redis
restart: unless-stopped
security_opt:
- "no-new-privileges:true"
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-redis123}
volumes:
- redis-data:/data
Expand All @@ -60,6 +64,9 @@ services:
dockerfile: apps/api/Dockerfile
container_name: gasguard-api
restart: unless-stopped
security_opt:
- "no-new-privileges:true"
user: "10001:10001"
depends_on:
postgres:
condition: service_healthy
Expand Down Expand Up @@ -114,6 +121,9 @@ services:
dockerfile: apps/api/Dockerfile
container_name: gasguard-api-canary
restart: unless-stopped
security_opt:
- "no-new-privileges:true"
user: "10001:10001"
profiles: ["canary"]
depends_on:
postgres:
Expand Down Expand Up @@ -163,6 +173,8 @@ services:
image: nginx:1.27-alpine
container_name: gasguard-nginx-canary
restart: unless-stopped
security_opt:
- "no-new-privileges:true"
profiles: ["canary"]
depends_on:
- api
Expand Down
53 changes: 53 additions & 0 deletions docs/CONTAINER_HARDENING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Container and Image Hardening Guide

## Overview

This document describes the security controls and hardening standards implemented for GasGuard container images and deployment manifests to ensure production readiness, minimal attack surfaces, and compliance with the principle of least privilege.

---

## 1. Minimal Base Images

All application containers utilize minimal Alpine Linux distributions pinned to explicit versions:
- Base image: `node:20.18.0-alpine3.20`
- Package managers and cache artifacts are purged immediately after build steps:
```dockerfile
rm -rf /var/cache/apk/* /tmp/* /root/.npm
```
- Development tools, compilers (`g++`, `make`, `python3`), and source files are quarantined strictly to the `builder` stage and omitted from the production `runner` image.

---

## 2. Non-Root Execution

Container processes never execute as the root user (`UID 0`):
- A dedicated unprivileged system account is created with fixed UID/GID:
- User: `gasguard` (`UID 10001`)
- Group: `gasguard` (`GID 10001`)
- Login shell: `/sbin/nologin`
- Home directory: `/app`
- All application binaries and runtime assets are owned by `gasguard:gasguard`.
- Containers switch to unprivileged execution via `USER gasguard` prior to entrypoint invocation.

---

## 3. Signal Handling and PID 1 Management

- `tini` is installed and declared as the container entrypoint (`ENTRYPOINT ["/sbin/tini", "--"]`).
- This ensures proper reaping of zombie processes and correct propagation of termination signals (`SIGTERM`, `SIGINT`) for graceful zero-downtime shutdowns.

---

## 4. Compose Hardening and Privilege Restrictions

In `docker-compose.yml`, services are configured with defensive security options:
- `security_opt: ["no-new-privileges:true"]`: Prevents child processes from gaining additional privileges via `setuid` or `setgid` binaries.
- Explicit non-root user enforcement (`user: "10001:10001"`).
- Health checks configured with non-root diagnostic commands (`wget --spider`) to continuously report readiness without requiring privileged probing tools.

---

## 5. Automated Vulnerability Scanning

- Container images are automatically scanned via GitHub Actions (`.github/workflows/container-security-scan.yml`) using Trivy.
- The pipeline scans for `CRITICAL` and `HIGH` severity CVEs, generating SARIF reports uploaded directly to GitHub Security Code Scanning.
Loading
Loading