LemonSec/docker-compose.yml

302 lines
11 KiB
YAML

version: "3.8"
# ============================================================================
# LEMONSEC - Security Stack for LemonLink
# Deploy via Portainer: Git Repository
# URL: https://git.lemonlink.eu/impulsivefps/LemonSec
# ============================================================================
#
# ENVIRONMENT VARIABLES (set in Portainer UI):
# Required:
# - CF_API_EMAIL Cloudflare account email
# - CF_API_KEY Cloudflare Global API Key
# - TRUENAS_IP TrueNAS Scale VM IP (e.g., 192.168.1.100)
# - TRUENAS_NEXTCLOUD_PORT TrueNAS Nextcloud port (e.g., 9001)
#
# Optional:
# - TZ Timezone (default: Europe/Stockholm)
# - CROWDSEC_API_KEY Generated after first CrowdSec start
# - TAILSCALE_IP For internal entrypoint binding
#
# Secrets (generate once, store securely):
# - AUTHELIA_JWT_SECRET
# - AUTHELIA_SESSION_SECRET
# - AUTHELIA_STORAGE_KEY
#
# ============================================================================
networks:
# External-facing network (Cloudflare → Traefik)
traefik-external:
driver: bridge
# Internal network (Tailscale/VPN → Traefik)
traefik-internal:
driver: bridge
internal: true
# Services network (isolated)
services:
driver: bridge
# CrowdSec network
crowdsec:
driver: bridge
volumes:
traefik-certs:
authelia-data:
crowdsec-data:
crowdsec-config:
adguard-work:
adguard-conf:
redis-data:
services:
# ============================================================================
# REVERSE PROXY - Traefik
# ============================================================================
traefik:
image: traefik:v3.1
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
networks:
- traefik-external
- traefik-internal
- services
- crowdsec
ports:
- "80:80"
- "443:443"
# Internal entrypoint - uncomment and set TAILSCALE_IP for Tailscale-only access
# - "${TAILSCALE_IP}:8443:8443"
environment:
- CF_API_EMAIL=${CF_API_EMAIL:?CF_API_EMAIL not set}
- CF_API_KEY=${CF_API_KEY:?CF_API_KEY not set}
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik/traefik.yml:/traefik.yml:ro
- ./traefik/dynamic:/dynamic:ro
- traefik-certs:/letsencrypt
labels:
- "traefik.enable=true"
# Dashboard - INTERNAL ONLY
- "traefik.http.routers.traefik.rule=Host(`traefik.local.lemonlink.eu`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
- "traefik.http.routers.traefik.middlewares=authelia@docker"
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8082/ping"]
interval: 10s
timeout: 5s
retries: 3
# ============================================================================
# SSO & AUTHENTICATION - Authelia
# ============================================================================
authelia:
image: authelia/authelia:4.38
container_name: authelia
restart: unless-stopped
networks:
- services
- traefik-external
- traefik-internal
expose:
- 9091
volumes:
- authelia-data:/config
- ./authelia/configuration.yml:/configuration.yml:ro
- ./authelia/users_database.yml:/users_database.yml:ro
environment:
- TZ=${TZ:-Europe/Stockholm}
# Secrets - passed via environment variables for Portainer
- AUTHELIA_JWT_SECRET=${AUTHELIA_JWT_SECRET:?AUTHELIA_JWT_SECRET not set}
- AUTHELIA_SESSION_SECRET=${AUTHELIA_SESSION_SECRET:?AUTHELIA_SESSION_SECRET not set}
- AUTHELIA_STORAGE_ENCRYPTION_KEY=${AUTHELIA_STORAGE_KEY:?AUTHELIA_STORAGE_KEY not set}
labels:
- "traefik.enable=true"
- "traefik.http.routers.authelia.rule=Host(`auth.lemonlink.eu`)"
- "traefik.http.routers.authelia.entrypoints=websecure"
- "traefik.http.routers.authelia.tls.certresolver=letsencrypt"
- "traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/verify?rd=https://auth.lemonlink.eu/"
- "traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true"
- "traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email"
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:9091/api/health"]
interval: 10s
timeout: 5s
retries: 3
# ============================================================================
# THREAT DETECTION - CrowdSec
# ============================================================================
crowdsec:
image: crowdsecurity/crowdsec:v1.6.0
container_name: crowdsec
restart: unless-stopped
networks:
- crowdsec
- services
environment:
- TZ=${TZ:-Europe/Stockholm}
- COLLECTIONS=crowdsecurity/linux crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/whitelist-good-actors
- GID=1000
volumes:
- crowdsec-data:/var/lib/crowdsec/data
- crowdsec-config:/etc/crowdsec
- ./crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml:ro
labels:
- "traefik.enable=false"
healthcheck:
test: ["CMD", "cscli", "machines", "list"]
interval: 30s
timeout: 10s
retries: 3
# CrowdSec bouncer for Traefik
crowdsec-bouncer-traefik:
image: crowdsecurity/traefik-bouncer:v0.1.0
container_name: crowdsec-bouncer-traefik
restart: unless-stopped
networks:
- crowdsec
environment:
- CROWDSEC_BOUNCER_API_KEY=${CROWDSEC_API_KEY:-}
- CROWDSEC_AGENT_HOST=crowdsec:8080
- CROWDSEC_BOUNCER_LOG_LEVEL=1
labels:
- "traefik.enable=false"
depends_on:
- crowdsec
# ============================================================================
# INTERNAL DNS - AdGuard Home
# ============================================================================
adguard:
image: adguard/adguardhome:v0.107.52
container_name: adguard
restart: unless-stopped
networks:
- services
- traefik-internal
ports:
- "53:53/tcp"
- "53:53/udp"
expose:
- 3000
- 80
volumes:
- adguard-work:/opt/adguardhome/work
- adguard-conf:/opt/adguardhome/conf
labels:
- "traefik.enable=true"
- "traefik.http.routers.adguard.rule=Host(`dns.local.lemonlink.eu`)"
- "traefik.http.routers.adguard.entrypoints=websecure"
- "traefik.http.routers.adguard.service=adguard"
- "traefik.http.routers.adguard.tls.certresolver=letsencrypt"
- "traefik.http.routers.adguard.middlewares=authelia@docker"
- "traefik.http.services.adguard.loadbalancer.server.port=80"
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:80"]
interval: 30s
timeout: 5s
retries: 3
# ============================================================================
# CACHE - Redis (for Authelia sessions)
# ============================================================================
redis:
image: redis:7-alpine
container_name: redis
restart: unless-stopped
networks:
- services
volumes:
- redis-data:/data
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
labels:
- "traefik.enable=false"
# ============================================================================
# EXTERNAL SERVICES ROUTING
# ============================================================================
# TrueNAS Nextcloud Router
# This container doesn't run anything - it's just configuration for Traefik
nextcloud-router:
image: alpine:latest
container_name: nextcloud-router
restart: "no"
entrypoint: ["echo", "Nextcloud routing configured"]
networks:
- services
labels:
- "traefik.enable=true"
# Main Nextcloud route - EXTERNAL ACCESS
- "traefik.http.routers.nextcloud.rule=Host(`cloud.lemonlink.eu`)"
- "traefik.http.routers.nextcloud.entrypoints=websecure"
- "traefik.http.routers.nextcloud.tls.certresolver=letsencrypt"
- "traefik.http.routers.nextcloud.service=nextcloud"
# Point to TrueNAS Nextcloud
# Uses environment variables: TRUENAS_IP and TRUENAS_NEXTCLOUD_PORT
- "traefik.http.services.nextcloud.loadbalancer.server.url=http://${TRUENAS_IP:?TRUENAS_IP not set}:${TRUENAS_NEXTCLOUD_PORT:?TRUENAS_NEXTCLOUD_PORT not set}"
# Security middlewares (NO Authelia - Nextcloud handles auth)
# This allows family to login directly to Nextcloud
- "traefik.http.routers.nextcloud.middlewares=security-headers@file,rate-limit@file,nextcloud-headers"
# Nextcloud-specific headers
- "traefik.http.middlewares.nextcloud-headers.headers.customRequestHeaders.X-Forwarded-Proto=https"
- "traefik.http.middlewares.nextcloud-headers.headers.customResponseHeaders.X-Frame-Options=SAMEORIGIN"
# WebDAV/CalDAV routes (needed for mobile apps)
- "traefik.http.routers.nextcloud-dav.rule=Host(`cloud.lemonlink.eu`) && PathPrefix(`/.well-known/carddav`,`/.well-known/caldav`,`/remote.php`)"
- "traefik.http.routers.nextcloud-dav.entrypoints=websecure"
- "traefik.http.routers.nextcloud-dav.tls.certresolver=letsencrypt"
- "traefik.http.routers.nextcloud-dav.service=nextcloud"
# TrueNAS Web UI Router (internal access only)
truenas-router:
image: alpine:latest
container_name: truenas-router
restart: "no"
entrypoint: ["echo", "TrueNAS routing configured"]
networks:
- services
labels:
- "traefik.enable=true"
- "traefik.http.routers.truenas.rule=Host(`nas.local.lemonlink.eu`)"
- "traefik.http.routers.truenas.entrypoints=websecure"
- "traefik.http.routers.truenas.tls.certresolver=letsencrypt"
- "traefik.http.routers.truenas.middlewares=authelia@docker"
- "traefik.http.services.truenas.loadbalancer.server.url=https://${TRUENAS_IP:?TRUENAS_IP not set}:443"
- "traefik.http.services.truenas.loadbalancer.serversTransport=insecureTransport@file"
# ============================================================================
# PORTAINER ROUTER (if Portainer is on host, not in this stack)
# ============================================================================
portainer-router:
image: alpine:latest
container_name: portainer-router
restart: "no"
entrypoint: ["echo", "Portainer routing configured"]
networks:
- services
labels:
- "traefik.enable=true"
- "traefik.http.routers.portainer.rule=Host(`docker.local.lemonlink.eu`)"
- "traefik.http.routers.portainer.entrypoints=websecure"
- "traefik.http.routers.portainer.tls.certresolver=letsencrypt"
- "traefik.http.routers.portainer.middlewares=authelia@docker"
- "traefik.http.services.portainer.loadbalancer.server.url=http://portainer:9000"