security hardening

This commit is contained in:
Gerhard Scheikl
2026-05-31 09:35:31 +02:00
parent d7d437a871
commit 01b4734477
31 changed files with 1234 additions and 238 deletions
+16
View File
@@ -16,8 +16,24 @@ ALLOWED_SHOP=linumiq-dev.myshopify.com
# Must match `scopes` in shopify.app.dev.toml.
SCOPES=read_orders,write_orders,read_all_orders,read_customers,read_companies,read_files,write_files
# --- Secrets at rest ---
# Field-level encryption key for secrets stored in the DB (SMTP password,
# Shopify session access/refresh tokens). Must be base64 of exactly 32 bytes.
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
DATA_ENCRYPTION_KEY=REPLACE_ME_BASE64_32_BYTES
# Dedicated HMAC key for signing public GiroCode URLs. base64 of 32 bytes.
# If unset, the app falls back to SHOPIFY_API_SECRET (kept for backward compat).
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
GIROCODE_SIGNING_KEY=REPLACE_ME_BASE64_32_BYTES
# --- Runtime ---
NODE_ENV=production
PORT=3000
# DATABASE_URL is set in docker-compose.dev.yml (file:/data/prod.sqlite on the bind mount).
# --- Email (optional) ---
# Archival BCC for every invoice email. Off by default for privacy/GDPR.
# Set to a single address or a comma-separated list to opt in.
# INVOICE_BCC=archive@example.com
+9
View File
@@ -8,6 +8,15 @@
# DEV — installed on linumiq-dev.myshopify.com
invoice-app-dev.linumiq.com {
encode zstd gzip
# Security response headers. NOTE: deliberately no X-Frame-Options here —
# this is an embedded Shopify app, and framing is governed by the
# Content-Security-Policy `frame-ancestors` directive that the Shopify
# library injects via addDocumentResponseHeaders (see app/entry.server.tsx).
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin
}
reverse_proxy linumiq-invoice-dev:3000
}
+19
View File
@@ -48,6 +48,25 @@ docker compose up -d --build
Append `Caddyfile.snippet` to your Caddy config and `docker exec caddy caddy reload --config /etc/caddy/Caddyfile`.
## Container runs as a non-root user (uid 1000)
The image runs as the unprivileged `node` user (uid/gid **1000**), not root. The
SQLite database is written to the `/data` bind mount, so the **host** directory
mounted at `/data` (e.g. `/docker/linumiq-invoice/dev/data` and
`…/prod/data`) must be writable by uid 1000, otherwise `prisma migrate deploy`
and DB writes fail on startup:
```bash
sudo chown -R 1000:1000 /docker/linumiq-invoice/dev/data
sudo chown -R 1000:1000 /docker/linumiq-invoice/prod/data
```
The dev container additionally runs with a **read-only root filesystem**
(`read_only: true` + `tmpfs: /tmp`), `no-new-privileges`, all Linux capabilities
dropped, and memory/pids/cpu limits. The app only writes to the `/data` bind
mount and the tmpfs `/tmp`, so this is safe. (The prod compose is intentionally
left unchanged.)
## Day-to-day redeploy
```bash
+18
View File
@@ -6,6 +6,24 @@ services:
image: linumiq-invoice:dev
container_name: linumiq-invoice-dev
restart: unless-stopped
# --- Container hardening (DEV) ---------------------------------------
# Prevent privilege escalation and drop all Linux capabilities (the app
# is a plain Node HTTP server — it needs none).
security_opt:
- "no-new-privileges:true"
cap_drop:
- ALL
# Read-only root filesystem: the app never writes to the image at runtime
# (Prisma client is baked at build; the SQLite DB lives on the /data bind
# mount; logo/image caches live in the DB or in-memory). npm/Prisma
# incidental writes are redirected to the tmpfs /tmp (see Dockerfile env).
read_only: true
tmpfs:
- /tmp
# Resource limits (Compose v2 / docker compose, non-swarm).
mem_limit: 512m
pids_limit: 256
cpus: 1.5
env_file:
- .env.dev
environment: