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"