304 lines
11 KiB
YAML
304 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 (optional - enables automatic IP blocking)
|
|
# To enable: docker exec crowdsec cscli bouncers add traefik-bouncer
|
|
# Then add CROWDSEC_API_KEY to environment variables
|
|
# crowdsec-bouncer-traefik:
|
|
# image: crowdsecurity/traefik-bouncer:latest
|
|
# 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"
|