Zum Inhalt

Security Hardening Guide

System-Level Hardening

SSH Configuration

# /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
X11Forwarding no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2

# Restart SSH
sudo systemctl restart sshd

Automatic Security Updates

# Ubuntu/Debian
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

# /etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
};
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "02:00";

Kernel Hardening

# /etc/sysctl.d/99-hardening.conf

# Disable IP forwarding
net.ipv4.ip_forward = 0
net.ipv6.conf.all.forwarding = 0

# Disable source packet routing
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0

# Enable TCP SYN cookie protection
net.ipv4.tcp_syncookies = 1

# Ignore ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0

# Ignore ICMP pings
net.ipv4.icmp_echo_ignore_all = 1

# Apply changes
sudo sysctl -p /etc/sysctl.d/99-hardening.conf

Docker Security Hardening

Docker Daemon Configuration

// /etc/docker/daemon.json
{
  "live-restore": true,
  "userland-proxy": false,
  "no-new-privileges": true,
  "icc": false,
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "userns-remap": "default"
}

Hardened docker-compose.yml

version: '3.8'

services:
  backend:
    image: solarlog-backend:latest

    # Security options
    security_opt:
      - no-new-privileges:true
      - apparmor=docker-default

    # Drop all capabilities, add only needed ones
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
      - CHOWN
      - SETGID
      - SETUID

    # Read-only root filesystem
    read_only: true

    # Tmpfs for temporary files
    tmpfs:
      - /tmp:noexec,nosuid,nodev,size=100m
      - /var/run:noexec,nosuid,nodev,size=10m

    # Resource limits
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 1G
          pids: 100
        reservations:
          cpus: '0.5'
          memory: 512M

    # Restart policy
    restart: unless-stopped

    # Network mode
    networks:
      - backend_network

    # Logging
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

    # Healthcheck
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  postgres:
    image: postgres:16

    security_opt:
      - no-new-privileges:true

    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - DAC_OVERRIDE
      - SETGID
      - SETUID

    read_only: true

    tmpfs:
      - /tmp:noexec,nosuid,nodev
      - /var/run/postgresql:noexec,nosuid,nodev

    volumes:
      - postgres-data:/var/lib/postgresql/data:rw

    environment:
      POSTGRES_DB: solarlog
      POSTGRES_USER: solarlog
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password

    secrets:
      - db_password

    networks:
      - backend_network

networks:
  backend_network:
    driver: bridge
    driver_opts:
      com.docker.network.bridge.name: br_solarlog
    ipam:
      config:
        - subnet: 172.20.0.0/24

volumes:
  postgres-data:
    driver: local

secrets:
  db_password:
    file: ./secrets/db_password.txt

Application Security

FastAPI Security Headers

# backend/app/middleware/security.py
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response

class SecurityHeadersMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        response = await call_next(request)

        # Security headers
        response.headers['X-Content-Type-Options'] = 'nosniff'
        response.headers['X-Frame-Options'] = 'DENY'
        response.headers['X-XSS-Protection'] = '1; mode=block'
        response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
        response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
        response.headers['Permissions-Policy'] = 'geolocation=(), microphone=(), camera=()'

        # CSP
        response.headers['Content-Security-Policy'] = (
            "default-src 'self'; "
            "script-src 'self' 'unsafe-inline'; "
            "style-src 'self' 'unsafe-inline'; "
            "img-src 'self' data: https:; "
            "font-src 'self'; "
            "connect-src 'self' https://solarlog-api.karma.organic; "
            "frame-ancestors 'none';"
        )

        return response

# Add to app
from app.middleware.security import SecurityHeadersMiddleware
app.add_middleware(SecurityHeadersMiddleware)

Input Validation

# backend/app/api/validators.py
from pydantic import BaseModel, validator, Field
import ipaddress

class InverterCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    ip_address: str
    port: int = Field(..., ge=1, le=65535)

    @validator('name')
    def name_alphanumeric(cls, v):
        if not v.replace(' ', '').replace('-', '').replace('_', '').isalnum():
            raise ValueError('Name must be alphanumeric')
        return v

    @validator('ip_address')
    def validate_ip(cls, v):
        try:
            ipaddress.ip_address(v)
        except ValueError:
            raise ValueError('Invalid IP address')
        return v

SQL Injection Prevention

# Always use SQLAlchemy ORM, never raw SQL
from sqlalchemy import select

# Good - parameterized query
stmt = select(Inverter).where(Inverter.id == inverter_id)
result = await db.execute(stmt)

# Bad - never do this!
# result = await db.execute(f"SELECT * FROM inverters WHERE id = {inverter_id}")

PostgreSQL Hardening

postgresql.conf

# /var/lib/postgresql/data/postgresql.conf

# SSL
ssl = on
ssl_cert_file = '/etc/ssl/certs/server.crt'
ssl_key_file = '/etc/ssl/private/server.key'

# Authentication
password_encryption = scram-sha-256

# Logging
log_connections = on
log_disconnections = on
log_duration = on
log_statement = 'mod'
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '

# Connection limits
max_connections = 100
superuser_reserved_connections = 3

# Resource limits
shared_buffers = 256MB
effective_cache_size = 1GB
maintenance_work_mem = 64MB
work_mem = 4MB

# Security
row_security = on

pg_hba.conf

# /var/lib/postgresql/data/pg_hba.conf

# TYPE  DATABASE        USER            ADDRESS                 METHOD

# Require SSL and SCRAM-SHA-256
hostssl all             all             172.20.0.0/24           scram-sha-256
hostssl all             all             0.0.0.0/0               scram-sha-256 clientcert=1

# Deny all other connections
host    all             all             all                     reject

Nginx Hardening

nginx.conf

# /etc/nginx/nginx.conf

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
    use epoll;
}

http {
    # Basic settings
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    server_tokens off;

    # Buffer overflow protection
    client_body_buffer_size 1K;
    client_header_buffer_size 1k;
    client_max_body_size 10M;
    large_client_header_buffers 2 1k;

    # Timeouts
    client_body_timeout 10;
    client_header_timeout 10;
    send_timeout 10;

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
    limit_conn_zone $binary_remote_addr zone=addr:10m;

    # SSL/TLS
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    ssl_stapling on;
    ssl_stapling_verify on;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Gzip
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml application/json application/javascript;

    include /etc/nginx/conf.d/*.conf;
}

Site configuration

# /etc/nginx/conf.d/solarlog.conf

# Redirect HTTP to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name solarlog.karma.organic;
    return 301 https://$server_name$request_uri;
}

# Main server
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name solarlog.karma.organic;

    # SSL certificates
    ssl_certificate /etc/letsencrypt/live/solarlog.karma.organic/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/solarlog.karma.organic/privkey.pem;

    # Rate limiting
    limit_req zone=api burst=20 nodelay;
    limit_conn addr 10;

    # Frontend
    location / {
        proxy_pass http://frontend:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }

    # API
    location /api {
        limit_req zone=api burst=5 nodelay;

        proxy_pass http://backend:8000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Disable buffering for SSE
        proxy_buffering off;
        proxy_cache off;
    }

    # Block common exploits
    location ~* "(eval\()|(\.\./)|(<|>|%0A|%0D)" {
        deny all;
    }

    # Block access to hidden files
    location ~ /\. {
        deny all;
    }
}

Secrets Management

Generate strong secrets

#!/bin/bash
# scripts/generate-secrets.sh

mkdir -p secrets

# Database password
openssl rand -base64 32 > secrets/db_password.txt

# API secret key
openssl rand -hex 32 > secrets/secret_key.txt

# JWT secret
openssl rand -hex 32 > secrets/jwt_secret.txt

# Set permissions
chmod 600 secrets/*
chown 1000:1000 secrets/*

echo "βœ… Secrets generated in ./secrets/"

Use Docker secrets

# docker-compose.yml
secrets:
  db_password:
    file: ./secrets/db_password.txt
  secret_key:
    file: ./secrets/secret_key.txt

services:
  backend:
    secrets:
      - db_password
      - secret_key
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password
      SECRET_KEY_FILE: /run/secrets/secret_key

Monitoring & Auditing

Audit logging

# backend/app/middleware/audit.py
import logging
from datetime import datetime

audit_logger = logging.getLogger("audit")
audit_logger.setLevel(logging.INFO)

handler = logging.FileHandler("/var/log/solarlog/audit.log")
handler.setFormatter(logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
))
audit_logger.addHandler(handler)

@app.middleware("http")
async def audit_middleware(request: Request, call_next):
    start_time = datetime.utcnow()

    response = await call_next(request)

    duration = (datetime.utcnow() - start_time).total_seconds()

    audit_logger.info({
        "timestamp": start_time.isoformat(),
        "method": request.method,
        "path": request.url.path,
        "client_ip": request.client.host,
        "status_code": response.status_code,
        "duration": duration,
        "user_agent": request.headers.get("user-agent")
    })

    return response

Compliance Checklist

  • All services run as non-root
  • File system is read-only where possible
  • Secrets stored securely (not in code)
  • SSL/TLS enabled everywhere
  • Security headers configured
  • Rate limiting enabled
  • Input validation on all endpoints
  • SQL injection prevention (ORM only)
  • XSS prevention (CSP headers)
  • CSRF protection enabled
  • Audit logging configured
  • Regular security updates
  • Backups encrypted
  • Monitoring & alerting active
  • Incident response plan documented

Resources