Security Hardening
Advanced security measures for production environments
Security Hardening Guide
Advanced security measures for production environments
Target: Achieve defense-in-depth security posture with multiple layers of protection Compliance: GDPR-ready, SOC 2 preparation, industry best practices Prerequisites: Basic infrastructure deployed per DEPLOYMENT_CHECKLIST.md
Table of Contents
- Security Baseline Assessment
- Server Hardening
- Network Security
- Application Security
- Database Security
- Secrets Management
- Monitoring & Incident Response
- Compliance & Auditing
Security Baseline Assessment
Security Audit Checklist
Run Before Hardening:
#!/bin/bash
# security-audit.sh
echo "=========================================="
echo "Security Baseline Assessment"
echo "=========================================="
# Check SSH configuration
echo "✓ Checking SSH configuration..."
sshd -T | grep -E "passwordauthentication|permitrootlogin|pubkeyauthentication"
# Check firewall status
echo "✓ Checking firewall..."
ufw status verbose || echo "⚠️ UFW not installed"
# Check fail2ban
echo "✓ Checking fail2ban..."
fail2ban-client status || echo "⚠️ fail2ban not installed"
# Check for security updates
echo "✓ Checking security updates..."
apt list --upgradable 2>/dev/null | grep -i security
# Check open ports
echo "✓ Checking open ports..."
ss -tunlp
# Check Docker security
echo "✓ Checking Docker security..."
docker version
docker info | grep -i "security\|userns"
# Check SSL certificates
echo "✓ Checking SSL certificates..."
echo | openssl s_client -connect app.your-domain.com:443 2>/dev/null | openssl x509 -noout -dates
echo "=========================================="
echo "Audit Complete"
echo "=========================================="Server Hardening
Operating System Hardening
1. Automatic Security Updates:
# Install unattended-upgrades
apt install unattended-upgrades apt-listchanges
# Configure automatic updates
cat > /etc/apt/apt.conf.d/50unattended-upgrades <<EOF
Unattended-Upgrade::Allowed-Origins {
"\${distro_id}:\${distro_codename}-security";
"\${distro_id}ESMApps:\${distro_codename}-apps-security";
"\${distro_id}ESM:\${distro_codename}-infra-security";
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
EOF
# Enable automatic updates
systemctl enable unattended-upgrades
systemctl start unattended-upgrades2. Kernel Hardening (sysctl.conf):
# /etc/sysctl.d/99-security.conf
cat > /etc/sysctl.d/99-security.conf <<EOF
# IP Forwarding (disable if not routing)
net.ipv4.ip_forward = 0
# Ignore ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
# Ignore source-routed packets
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
# Log martian packets
net.ipv4.conf.all.log_martians = 1
# Ignore ICMP pings (optional - can break monitoring)
# net.ipv4.icmp_echo_ignore_all = 1
# SYN flood protection
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 2048
net.ipv4.tcp_synack_retries = 2
# Increase ephemeral port range
net.ipv4.ip_local_port_range = 10000 65000
# TCP hardening
net.ipv4.tcp_rfc1337 = 1
net.ipv4.tcp_timestamps = 0
# Kernel randomization
kernel.randomize_va_space = 2
# Restrict kernel logs to root only
kernel.dmesg_restrict = 1
# Restrict access to kernel pointers
kernel.kptr_restrict = 2
EOF
# Apply settings
sysctl -p /etc/sysctl.d/99-security.conf3. AppArmor Profiles (Ubuntu default):
# Verify AppArmor is running
aa-status
# Expected: Multiple Docker profiles in enforce mode
# Create custom profile for PostgreSQL container (optional)
cat > /etc/apparmor.d/docker-postgres <<EOF
#include <tunables/global>
profile docker-postgres flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
network inet tcp,
network inet udp,
network inet6 tcp,
network inet6 udp,
deny /proc/* r,
deny /sys/** r,
/var/lib/postgresql/data/** rw,
/tmp/** rw,
}
EOF
# Load profile
apparmor_parser -r /etc/apparmor.d/docker-postgresSSH Hardening (Beyond Basics)
Advanced SSH Configuration:
File: /etc/ssh/sshd_config
# Port (change from default 22 to reduce automated attacks)
Port 2222
# Protocol
Protocol 2
# Authentication
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
UsePAM yes
# Key Exchange Algorithms (strong only)
KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256
# Ciphers (strong only)
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
# MACs (strong only)
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256
# Limit login attempts
MaxAuthTries 3
MaxSessions 5
# Disconnect idle sessions (15 minutes)
ClientAliveInterval 300
ClientAliveCountMax 2
# Disable X11 forwarding
X11Forwarding no
# Disable TCP forwarding (if not needed)
AllowTcpForwarding no
AllowStreamLocalForwarding no
# Disable agent forwarding
AllowAgentForwarding no
# Only allow specific users/groups
AllowUsers deployer devops
# OR
AllowGroups ssh-users
# Log verbosely
LogLevel VERBOSE
# Use strong host keys only
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_keyRestart SSH:
# IMPORTANT: Test new SSH connection BEFORE restarting
ssh -p 2222 root@server-ip
# If test succeeds, restart SSH
systemctl restart sshdUpdate Firewall for New SSH Port:
# Hetzner Cloud Firewall (via Console)
# Change SSH rule: Port 22 → Port 2222
# UFW (if using)
ufw allow 2222/tcp comment 'SSH'
ufw delete allow 22/tcpfail2ban Advanced Configuration
Enhanced fail2ban Setup:
File: /etc/fail2ban/jail.local
[DEFAULT]
# Ban for 1 hour
bantime = 3600
# Find time window: 10 minutes
findtime = 600
# Max retries before ban
maxretry = 3
# Ignore localhost and office IPs
ignoreip = 127.0.0.1/8 ::1 YOUR_OFFICE_IP/32
# Send email on ban (optional)
destemail = security@your-domain.com
sender = fail2ban@your-domain.com
action = %(action_mwl)s
[sshd]
enabled = true
port = 2222
logpath = /var/log/auth.log
maxretry = 3
bantime = 7200
[nginx-http-auth]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 5
[nginx-limit-req]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 10
[docker-auth]
enabled = true
port = 2375,2376
logpath = /var/log/docker.log
maxretry = 3
bantime = 86400Restart fail2ban:
systemctl restart fail2ban
# Check status
fail2ban-client status
# View banned IPs
fail2ban-client status sshdDocker Security
1. Docker Daemon Hardening:
File: /etc/docker/daemon.json
{
"icc": false,
"live-restore": true,
"userland-proxy": false,
"no-new-privileges": true,
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"default-ulimits": {
"nofile": {
"Name": "nofile",
"Hard": 64000,
"Soft": 64000
}
}
}2. Run Containers as Non-Root:
# Dockerfile example
FROM node:20-alpine
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
# Switch to non-root user
USER nextjs
WORKDIR /app
COPY --chown=nextjs:nodejs . .
CMD ["node", "server.js"]3. Docker Security Scanning (Trivy):
# Install Trivy
apt install wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | apt-key add -
echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | tee -a /etc/apt/sources.list.d/trivy.list
apt update && apt install trivy
# Scan Docker images
trivy image ghcr.io/your-org/ripplecore-app:latest
# Scan for high/critical vulnerabilities only
trivy image --severity HIGH,CRITICAL ghcr.io/your-org/ripplecore-app:latest
# Auto-scan on image pull (in CI/CD)
# See CI_CD_PIPELINE.md for GitHub Actions integration4. Resource Limits (prevent DoS):
# CPU limits
docker update --cpus="1.5" ripplecore-app
# Memory limits
docker update --memory="3g" --memory-reservation="2g" ripplecore-app
# Restart policies
docker update --restart=unless-stopped ripplecore-appNetwork Security
Firewall Configuration (UFW)
Install and Configure UFW:
# Install UFW
apt install ufw
# Default policies
ufw default deny incoming
ufw default allow outgoing
# Allow SSH (use your custom port)
ufw allow 2222/tcp comment 'SSH'
# Allow HTTP/HTTPS
ufw allow 80/tcp comment 'HTTP'
ufw allow 443/tcp comment 'HTTPS'
# Allow Netdata (from office IP only)
ufw allow from YOUR_OFFICE_IP to any port 19999 comment 'Netdata'
# Enable UFW
ufw enable
# Verify status
ufw status verboseRate Limiting (UFW built-in):
# Rate limit SSH (max 6 connections per 30 seconds from same IP)
ufw limit 2222/tcp comment 'SSH rate limit'
# Rate limit HTTP/HTTPS
ufw limit 80/tcp comment 'HTTP rate limit'
ufw limit 443/tcp comment 'HTTPS rate limit'Network Segmentation
Hetzner Cloud Private Network (already configured):
-
Production Network: 10.0.1.0/24
- App Server: 10.0.1.2
- DB Server: 10.0.1.3
- CI/CD Server: 10.0.1.4
-
Staging Network: 10.0.2.0/24
- Staging Server: 10.0.2.2
Database Isolation:
# Verify PostgreSQL only listens on private network
docker exec ripplecore-postgres psql -U ripplecore -c "SHOW listen_addresses;"
# Expected: 0.0.0.0 or 10.0.1.3
# Test from app server (should work)
psql -h 10.0.1.3 -U ripplecore -d ripplecore -c "SELECT 1"
# Test from internet (should fail - timeout)
psql -h PUBLIC_DB_IP -U ripplecore -d ripplecore -c "SELECT 1"
# Expected: Connection timeout (firewall blocks)DDoS Protection
Cloudflare DDoS Protection (Free Plan):
-
Enable Cloudflare (see PERFORMANCE_OPTIMIZATION.md)
-
Firewall Rules (Block malicious traffic):
Cloudflare Dashboard → Security → WAF Rules: - Block known bots (Challenge): Browser Integrity Check - Rate limiting: 100 requests/minute per IP - Block countries: (optional) Block high-risk countries - Challenge suspicious traffic: CAPTCHA for security_level = high -
Under Attack Mode (when under DDoS):
Cloudflare Dashboard → Overview → Under Attack Mode: ON # Forces JavaScript challenge for all visitors # Use only during active attack
Arcjet Rate Limiting (already configured in app):
// Verify rate limits in packages/security/arcjet.ts
import arcjet, { shield, tokenBucket } from "@arcjet/next";
const aj = arcjet({
key: process.env.ARCJET_KEY!,
rules: [
shield({ mode: "LIVE" }), // DDoS protection
// Token bucket rate limiting
tokenBucket({
mode: "LIVE",
refillRate: 10, // 10 tokens per interval
interval: 10, // 10 seconds
capacity: 20, // Burst capacity
}),
],
});Application Security
Content Security Policy (CSP)
Strict CSP Configuration:
File: packages/security/headers-config.ts
export const securityHeaders = [
{
key: "Content-Security-Policy",
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.jsdelivr.net", // Remove unsafe-* in production
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: https: blob:",
"connect-src 'self' https://api.sentry.io https://*.arcjet.io",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"upgrade-insecure-requests",
].join("; "),
},
{
key: "X-Frame-Options",
value: "DENY",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=(self), payment=()",
},
{
key: "Strict-Transport-Security",
value: "max-age=31536000; includeSubDomains; preload",
},
];Test CSP:
curl -I https://app.your-domain.com | grep -i "content-security-policy"
# Use Mozilla Observatory
# https://observatory.mozilla.orgInput Validation & Sanitization
Zod Validation (already used in packages):
// All API inputs MUST be validated
import { z } from "zod";
// ❌ BAD: No validation
export async function createKindness(data: any) {
return db.insert(kindness).values(data);
}
// ✅ GOOD: Strict validation
const createKindnessSchema = z.object({
companyId: z.string().uuid(),
userId: z.string().uuid(),
recipientName: z.string().min(1).max(100),
message: z.string().min(1).max(1000),
verificationLevel: z.number().int().min(1).max(5),
});
export async function createKindness(data: unknown) {
// Throws if validation fails
const validated = createKindnessSchema.parse(data);
return db.insert(kindness).values(validated);
}SQL Injection Prevention (Drizzle ORM):
// ✅ SAFE: Drizzle automatically parameterizes queries
const results = await db.query.users.findMany({
where: eq(users.email, userInput), // Automatically escaped
});
// ❌ UNSAFE: Raw SQL (use only when necessary)
const results = await db.execute(
sql`SELECT * FROM users WHERE email = ${userInput}`,
);
// Still safe with Drizzle's sql`` template, but avoid if possibleAuthentication Security
better-auth Security Configuration:
File: packages/auth/server.ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: {
// ... database config
},
// Session security
session: {
expiresIn: 60 * 60 * 8, // 8 hours (PRD requirement)
updateAge: 60 * 60, // Rotate session every 1 hour
cookieCache: {
enabled: true,
maxAge: 5 * 60, // Cache session for 5 minutes
},
},
// Strong password requirements
password: {
minLength: 12, // Minimum 12 characters
requireNumbers: true,
requireLowercase: true,
requireUppercase: true,
requireSpecialChars: true,
},
// Cookie security
advanced: {
useSecureCookies: process.env.NODE_ENV === "production",
cookieSameSite: "lax",
cookieSecure: true,
},
// Account lockout (prevent brute force)
rateLimit: {
enabled: true,
window: 15 * 60, // 15 minutes
max: 5, // Max 5 failed attempts
},
// Email verification required
emailVerification: {
required: true,
expiresIn: 24 * 60 * 60, // 24 hours
},
// TOTP 2FA (optional but recommended)
twoFactor: {
enabled: true,
issuer: "RippleCore",
},
});Password Hashing (Argon2 - automatic with better-auth):
// better-auth uses Argon2id by default (strongest algorithm)
// No manual hashing needed
// Verify password strength on client
import { zxcvbn } from "zxcvbn";
const passwordStrength = zxcvbn(password);
if (passwordStrength.score < 3) {
throw new Error("Password too weak");
}API Security
API Rate Limiting (per endpoint):
// apps/api/app/api/kindness/route.ts
import { rateLimitMiddleware } from "@repo/security/rate-limit";
export async function POST(request: Request) {
// Apply rate limit: 10 requests per minute
await rateLimitMiddleware(request, {
limit: 10,
window: 60,
});
// ... rest of handler
}API Authentication (Bearer tokens):
// apps/api/middleware.ts
import { auth } from "@repo/auth/server";
import { headers } from "next/headers";
export async function authenticateAPI(request: Request) {
const authHeader = request.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
throw new Error("Unauthorized");
}
const token = authHeader.substring(7);
// Verify token with better-auth
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session?.user) {
throw new Error("Invalid token");
}
return session;
}Database Security
PostgreSQL Security Hardening
1. Disable Remote Root Login:
-- Revoke superuser from remote users
REVOKE ALL ON DATABASE ripplecore FROM PUBLIC;
-- Create read-only user for reporting
CREATE USER ripplecore_readonly WITH PASSWORD 'strong-password';
GRANT CONNECT ON DATABASE ripplecore TO ripplecore_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO ripplecore_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ripplecore_readonly;2. Enable SSL/TLS:
# Generate self-signed certificate (or use Let's Encrypt)
openssl req -new -x509 -days 365 -nodes -text \
-out /etc/postgresql/server.crt \
-keyout /etc/postgresql/server.key \
-subj "/CN=ripplecore-db"
chmod 600 /etc/postgresql/server.key
chown postgres:postgres /etc/postgresql/server.*
# Enable SSL in postgresql.conf
docker run -d \
--name ripplecore-postgres \
-e POSTGRES_SSL_MODE=require \
-v /etc/postgresql/server.crt:/var/lib/postgresql/server.crt \
-v /etc/postgresql/server.key:/var/lib/postgresql/server.key \
postgres:18-alpine \
-c ssl=on \
-c ssl_cert_file=/var/lib/postgresql/server.crt \
-c ssl_key_file=/var/lib/postgresql/server.keyUpdate Connection String:
DATABASE_URL=postgresql://ripplecore:<secret>@10.0.1.3:5432/ripplecore?sslmode=require3. Audit Logging:
-- Enable query logging for security audit
ALTER SYSTEM SET log_statement = 'mod'; -- Log all modifications (INSERT, UPDATE, DELETE)
ALTER SYSTEM SET log_connections = on;
ALTER SYSTEM SET log_disconnections = on;
ALTER SYSTEM SET log_duration = on;
ALTER SYSTEM SET log_line_prefix = '%t [%p]: user=%u,db=%d,app=%a,client=%h ';
SELECT pg_reload_conf();
-- View logs
docker logs ripplecore-postgres --tail 100 | grep -E "INSERT|UPDATE|DELETE"Redis Security
1. Authentication (requirepass):
# Generate strong password
REDIS_PASSWORD=$(openssl rand -base64 32)
docker run -d \
--name ripplecore-redis \
--restart unless-stopped \
-p 6379:6379 \
-v redis-data:/data \
redis:7-alpine redis-server \
--requirepass "$REDIS_PASSWORD" \
--appendonly yes
# Update connection string
REDIS_URL=redis://:${REDIS_PASSWORD}@10.0.1.3:63792. Disable Dangerous Commands:
# Prevent FLUSHALL, FLUSHDB, CONFIG commands
docker run -d \
--name ripplecore-redis \
redis:7-alpine redis-server \
--requirepass "$REDIS_PASSWORD" \
--rename-command FLUSHDB "" \
--rename-command FLUSHALL "" \
--rename-command CONFIG "" \
--rename-command SHUTDOWN SHUTDOWN_SECRET_COMMAND3. Network Isolation (bind to private IP only):
docker run -d \
--name ripplecore-redis \
-p 10.0.1.3:6379:6379 \ # Bind to private IP only
redis:7-alpine redis-server \
--bind 10.0.1.3 \
--protected-mode yesSecrets Management
Environment Variable Security
1. Use 1Password CLI (for team secret management):
# Install 1Password CLI
curl -sS https://downloads.1password.com/linux/keys/1password.asc | gpg --dearmor --output /usr/share/keyrings/1password-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/1password-archive-keyring.gpg] https://downloads.1password.com/linux/debian/$(dpkg --print-architecture) stable main" | tee /etc/apt/sources.list.d/1password.list
apt update && apt install 1password-cli
# Authenticate
eval $(op signin)
# Load secrets from 1Password vault
export DATABASE_URL=$(op read "op://Production/Database/connection_string")
export REDIS_URL=$(op read "op://Production/Redis/connection_string")
export BETTER_AUTH_SECRET=$(op read "op://Production/Auth/secret")2. Encrypt Secrets at Rest (Dokploy):
# Dokploy encrypts environment variables by default
# Verify encryption in Dokploy database
docker exec dokploy-db sqlite3 /app/data/dokploy.db \
"SELECT key, substr(value, 1, 20) || '...' FROM environment_variables LIMIT 5;"
# Values should be encrypted (not readable)3. Rotate Secrets Quarterly:
#!/bin/bash
# rotate-secrets.sh
# Generate new secrets
NEW_BETTER_AUTH_SECRET=$(npx @better-auth/cli secret)
NEW_REDIS_PASSWORD=$(openssl rand -base64 32)
# Update 1Password vault
op item edit "Production/Auth" secret="$NEW_BETTER_AUTH_SECRET"
op item edit "Production/Redis" password="$NEW_REDIS_PASSWORD"
# Update Dokploy environment variables
# (Manual step via Dokploy UI or API)
# Restart applications
docker restart ripplecore-app ripplecore-api ripplecore-web ripplecore-redis
echo "✅ Secrets rotated successfully"
echo "⚠️ Update backup scripts with new credentials"Monitoring & Incident Response
Security Monitoring
1. Intrusion Detection (AIDE):
# Install AIDE
apt install aide
# Initialize database
aideinit
# Move database
mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
# Check for changes daily
cat > /etc/cron.daily/aide <<EOF
#!/bin/bash
/usr/bin/aide --check | mail -s "AIDE Report \$(hostname)" security@your-domain.com
EOF
chmod +x /etc/cron.daily/aide2. File Integrity Monitoring:
# Monitor critical files
cat > /etc/aide/aide.conf.d/99_custom <<EOF
# Monitor SSH configuration
/etc/ssh/sshd_config$ p+i+n+u+g+s+b+m+c+md5+sha256
# Monitor system binaries
/usr/bin$ p+i+n+u+g+s+b+m+c+md5+sha256
/usr/sbin$ p+i+n+u+g+s+b+m+c+md5+sha256
# Monitor Docker configuration
/etc/docker/daemon.json$ p+i+n+u+g+s+b+m+c+md5+sha256
EOF
# Update AIDE database
aideinit3. Log Monitoring (Logwatch):
# Install Logwatch
apt install logwatch
# Configure daily email reports
cat > /etc/cron.daily/00logwatch <<EOF
#!/bin/bash
/usr/sbin/logwatch --output mail --mailto security@your-domain.com --detail high
EOF
chmod +x /etc/cron.daily/00logwatchIncident Response Plan
Incident Response Phases:
-
Preparation (Proactive)
- Security monitoring in place
- Incident response team identified
- Communication channels established
-
Detection (Reactive)
- Alerts from Netdata, UptimeRobot, Sentry
- Log anomalies from fail2ban, Logwatch
- User reports of suspicious activity
-
Containment (Immediate)
# Isolate compromised server # Via Hetzner Console → Server → Network → Disable # Block attacker IP ufw insert 1 deny from ATTACKER_IP # Revoke compromised credentials # Update secrets in 1Password and Dokploy -
Eradication (Investigation)
- Identify attack vector
- Remove malware/backdoors
- Patch vulnerabilities
-
Recovery (Restoration)
- Restore from clean backup
- Verify integrity
- Resume normal operations
-
Lessons Learned (Post-Incident)
- Document incident
- Update security measures
- Train team on findings
Compliance & Auditing
GDPR Compliance
Data Protection Measures:
-
Data Encryption at Rest
- PostgreSQL: Enable pgcrypto extension
- Redis: AOF files on encrypted disk
- Backups: GPG encrypted before S3 upload
-
Data Encryption in Transit
- HTTPS enforced (HSTS preload)
- PostgreSQL SSL/TLS required
- Redis AUTH enabled
-
Data Minimization
- Only collect necessary user data
- Automatic deletion of old data (retention policies)
-
Right to Erasure
- Implement user data export API
- Implement user data deletion API
- Cascade deletes in database schema
-
Breach Notification
- 72-hour notification procedure
- Incident response plan documented
- Contact: dpo@your-domain.com
GDPR Checklist:
# Verify EU data residency
echo "Hetzner datacenter: Falkenstein, Germany (EU)"
# Verify encryption
curl -I https://app.your-domain.com | grep "Strict-Transport-Security"
# Verify data retention policies
docker exec ripplecore-postgres psql -U ripplecore -c "SELECT COUNT(*) FROM users WHERE created_at < NOW() - INTERVAL '2 years';"Security Audit Logging
Enable Comprehensive Audit Logs:
-- PostgreSQL audit extension (pgAudit)
CREATE EXTENSION IF NOT EXISTS pgaudit;
-- Log all DDL and user actions
ALTER SYSTEM SET pgaudit.log = 'ddl, role, write';
ALTER SYSTEM SET pgaudit.log_catalog = off;
ALTER SYSTEM SET pgaudit.log_parameter = on;
SELECT pg_reload_conf();
-- Query audit logs
SELECT
session_user_name,
command,
object_type,
object_name,
timestamp
FROM pgaudit.log
ORDER BY timestamp DESC
LIMIT 100;Security Checklist Summary
Critical (Immediate Implementation)
- SSH hardening (disable password auth, use keys only)
- fail2ban installed and configured
- Firewall enabled (UFW or Hetzner Cloud Firewall)
- Automatic security updates enabled
- PostgreSQL only accessible via private network
- Redis authentication enabled
- SSL/TLS on all services
- Security headers configured
- Secrets stored in 1Password (not Git)
Important (Within 1 Week)
- AppArmor profiles for containers
- Docker security scanning (Trivy)
- Kernel hardening (sysctl)
- Rate limiting (Arcjet + Cloudflare)
- Input validation on all APIs (Zod)
- Audit logging enabled
- Incident response plan documented
- Security monitoring (AIDE, Logwatch)
Recommended (Within 1 Month)
- PostgreSQL SSL/TLS encryption
- Encrypted backups (GPG)
- 2FA for admin users
- Security awareness training for team
- Quarterly security audits
- Penetration testing (external)
- GDPR compliance review
- SOC 2 preparation (if applicable)
Document Version: 1.0 Last Updated: 2025-01-23 Review Cycle: Quarterly or after security incidents Next Security Audit: [Schedule 3 months from now]