diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index 561c64e..dccc2c6 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -38,9 +38,10 @@ jobs: - name: Configure SSH run: | mkdir -p ~/.ssh - echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 + printf '%s' "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 - ssh-keyscan -p 22 87.249.49.32 >> ~/.ssh/known_hosts + printf '%s' "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts + chmod 600 ~/.ssh/known_hosts - name: Write vault password run: | diff --git a/inventory/group_vars/all/main.yml b/inventory/group_vars/all/main.yml index fa81942..e576449 100644 --- a/inventory/group_vars/all/main.yml +++ b/inventory/group_vars/all/main.yml @@ -9,6 +9,7 @@ domain_plane: "plane.{{ domain_base }}" domain_sync: "sync.{{ domain_base }}" domain_traefik: "traefik.{{ domain_base }}" domain_dashboard: "dashboard.{{ domain_base }}" +domain_auth: "auth.{{ domain_base }}" # Service paths services_root: /opt/services @@ -28,6 +29,11 @@ forgejo_runner_token: "{{ vault_forgejo_runner_token }}" grafana_admin_password: "{{ vault_grafana_admin_password }}" alertmanager_telegram_token: "{{ vault_alertmanager_telegram_token }}" alertmanager_telegram_chat_id: "{{ vault_alertmanager_telegram_chat_id }}" +authelia_jwt_secret: "{{ vault_authelia_jwt_secret }}" +authelia_session_secret: "{{ vault_authelia_session_secret }}" +authelia_storage_key: "{{ vault_authelia_storage_key }}" +authelia_admin_password_hash: "{{ vault_authelia_admin_password_hash }}" +crowdsec_bouncer_key: "{{ vault_crowdsec_bouncer_key }}" # CI/CD deploy key (public key — not a secret) ci_deploy_pubkey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHdr9mRSSUqt7Ym4wA5RpVyz76wEXSOtVfh2/yCSMIbg ci-deploy@forgejo-runner" diff --git a/inventory/group_vars/all/vault.yml b/inventory/group_vars/all/vault.yml index f8d8971..90b46bf 100644 --- a/inventory/group_vars/all/vault.yml +++ b/inventory/group_vars/all/vault.yml @@ -1,50 +1,76 @@ $ANSIBLE_VAULT;1.1;AES256 -63336635333534363962303037316537376162313965663565623562613761653239313330303333 -6163643864313062373336313037383661636238313230650a383062386234313130633231663832 -64313337663263313362303964326231386236353339663638656235393633643265653338306139 -6463356531653538620a333037376239343430343335336635636361386335303963373364636162 -36333531323565616132643431633166633937616630313834623934646134386337393166393834 -64326237306432376638623833666638316564353134613365323162336136373335396534623838 -32306435383062393933666430386338376531663236323430366162613363666361353066383138 -39303864636333343235623136633132663230333433356638653237643635383061366537383462 -33616235613164323164643662363862656630393361633764313731323638396232353235313838 -62636231326533616538323734303039613238633533336363376639643630306439353838623966 -64343135386361663633333162653064306130633263653335313431616365666336613737326237 -38343731313234663263633431623561653932333161633731346632326262353565356637666230 -61666134613038313665333162653534313034643562353134356131353938393834633265336561 -62633063646237393966373334353437636136343938323132356432653664353062363630363633 -62613066383232353630373130353433356430356435396136373261653761386435636331643636 -37333964393461626461613662616235653036366434613432613864616633323436643331333866 -37393235343361363438326466633632323131313830663835663431393137336261616138646666 -65383166306638353838643933353065386661663833633666626263653135336631383332643939 -61656561666564643335383566363861303939346538333730636364343234306539343263383336 -32653439653937313434396466306265336231356437393965363366306134633062356637313464 -31386538353039316631613265386261303239653965303163646561316366336234333866653663 -30633666383338623661666232643166613638373763333666653963656165366432643665643838 -36313135396462303339323638326235666538333530323638336330343263383634643532623164 -63633633656630363665636664343863343836636134396634653661613633613165623039626466 -33333932386435333335346135663661313831306636623061396565643665396432343732383463 -34336333313261643531376432353036336639343834366631393234663064303865396438366432 -38313630623936393765393233633437386234316233393934316334663533656538383534623563 -66646466613233313234356166363539653239366433623864663239623964643062393535393038 -32303132626332303130643064633530313731653831656432323163643733383962343239633436 -37386561386637363331666336366539313538353838626364626635333432383936613664326338 -66656438656134323632366265383062336566393030386333646632653132623931663863323966 -34373232373361326466353066323436653738336431303663383835356436613331313064393835 -38636365643061626238356539313133383564393365373131383664326234326332633035373433 -36666533636461653638356637363830633464316139646336623636346563663439356630623534 -62396436306265336362303735383933356464363437633236363637343665333035633532356637 -65353634316365346235313138636461663830636232623534363164393965323265623262643934 -62306339653433353839643830653733343062396164383934393262303135666130643233366639 -30636565663235323333333933386432333430323433633335383638383632393832323839626437 -34636638303261616333653632313266336662366663303432616138636330323232353033326630 -34633366613265373864613238663533306334303164386566656133643535303431643938663135 -36373838316435613766633261613836353135373832656363643664323533623333336433383761 -62363532356563353535643531316338363231333331383236613838386266666139346334353434 -30323266616337643034393363333437393630636337303632303530653866376465343364313637 -38393863666366613130326165316236396136383132323238653962343734353666333662306163 -37643465336432623266623932356330326537316261646239623232396233303637353863633337 -38333665306665656431656262643861643435616538313530376438383738373530316565373835 -36346263656635363464636533663630363362313461363766373231326434346435386432343866 -62376262313065386130363533326133623165666633353264666463633062626434303362636434 -32623165363835623931386434663961343037373835346262663738373461633366 +61316166393964386231353533353731353730326134323862666166373430346531383435396264 +3339363034336365363263643165656264333030323036640a313937396562326539633430643931 +61626330343235646637653065666237626564376130376662366238336135373836613362643963 +6335393437333362390a393930373132366161333762643535373232613136306664346662366231 +33643832306638306130653937323863643237346231363432623462313534386162373866663362 +34313665323632393766626535656239333231396438383833663835623963323530323663323539 +38373639623235396133373632613337353538666666303538333637333537363162323238376366 +65393233366162643835316439613262326531373961646336626232626334643331643438663834 +66386332316561333435313535666161323661663038343464383130663131363130303238626632 +62383934643539623434333566376463653930353833333433633364383764393732633734633636 +37646139656634666666613161396631353164363831353366393365643336376330613565353966 +32363431666534316165366636376164353165333738326230326232386137666366353636633865 +66623264623730653866663030646431386238633662336162623665356536613832346131316230 +34353664373930636361383961646334633838366636343335313438663836623761666235383431 +33373534653930623666323433326636633133336538633166333362353663356264323936623763 +37396338623962626638346538363565316262646232336266393936323839613533666465663439 +63343635346333636539373335323831366630356536336262353534643035323765653366656363 +63393534396135393061653234646362303066386133333736643739373164623034396361363539 +37326532643064656666363735333535643765643433633131356334393434333939623239343761 +32383966646435363936346464616233313865303264333331613437396635373336383664326665 +30366436303264633762336234356431666238353535396234383133353362366465363834643666 +33653732646264343636646266653138313634346239623764656136303462316364656234623833 +35313561323464346435656565633036383264373436313164363262643164616436663564643032 +30646637653036663533663430386134663237333030336430373936323738653030353564313464 +39393562383735313237366433646431356364363039353539366133333237303532653965666364 +35393830336533373133366666653765366562336539646131636633326434393164343530633737 +34626263636163626333373438376137636139643263646336643735316462313361663834613031 +37396233633831393536313838313964343762383363356238393761646230393334303836633735 +65666366393932346636396237333166323936613732333036323333343637393931393534323166 +61626334613035353137303365373365373837616336343838643365616538623538653238363664 +30356539663763633337393162623764376265646435663064303930643364396439626661666532 +38306364356463643866336530303430633766336236616135326462353163343637336438373534 +37616536386131366634633663633566313238366133376131353666663464306463313232626436 +61306236663332373764303566643332343530366362376134653437356630613937346663323131 +65376563666434653132383032643830386465363965653530323036623034313764306136366239 +34666538623232646266666537353033386661333861386564316662386233636265366536386135 +35653735383231616664366338623264326663353730623461613766613432396233383061383464 +35376461323233633938646532373663396233396463323565633539353630653934616231636166 +39633037333963653061386362316662613235646236326666633164336661373963386339633932 +64616436333637373930373062333463336562303439623937643136323735616231303762373161 +62666438613038363833626664316435316331373030343738356438323563313565613039326639 +65386533306132663964386330396566623063633433653439383235373761363037633138376461 +65323465663137363135336662643432616437323466656666313437333366626234623765643033 +30633132636235383561373566366465393664663464643965363634323466303433623361613061 +66623861336537333339636161636564356239636562363166326635646166653933376634626234 +61643738346263646664356134313138373331343731366532383264613931353030313061636135 +30343430383630633966393933396238366463373934653130656433633437323137326666633964 +32626639396165323334393263393961663666623137643834373065383966353835613335636362 +31646635356233323730393039366162613331393465633139616432353462363165333530373364 +32343935643933326136383835633232356263343264373437383630313537343138383135613832 +32383738353435323437336137626231343535633364666663633133353662383139383364373837 +37386133383135326662383661346639393134313931383637613631343836646663663834336632 +64383734373362316666343031393764393161613035373863323839383237643863326664656465 +34306637316466366332666237313064366534323961373166663339333439303365633137386236 +64313338383037613439626462323737393034303732303537636565353033386365653239326131 +32326162663766626264653965323134366664386238393564386163613165383661303832633565 +31306335393439323635653731363931663364613438373130623437376638386364333266643838 +35303436383839653434316632616163623264326531616439643437663538376333366432666165 +38363635653864383662346235353561316233656332383031643938613735396635363436313735 +66396535383030353437626165626432646634613434643830303434643530636566333063323366 +30663738326562343732376662366566636330346435373838363165643666393764343832656638 +65666134616265633138376133386438666465666661323631373539666330616638306439636533 +34346365333462623438333930376133383233373064366336343937616638376163303435313163 +34303537306532396230383236393731663230393135386133316638343735373666306337376235 +34616639326432386266373361306537343637356335613136346261316433613464323263646134 +31326232323738313830353535313363663363393037653631353932613834346133616535666361 +62353539646331656665323763396662313137366261336139356231646663646564646536373433 +62646234393737666635626536656636316535343661626364376536633461383530633135396137 +32343163373265623138333162653231636336373661316530633331346463393365353462336136 +66336236313765616436646532336164363261656262646135653734376331646665353139613037 +65323338316139353837623661353134656164613362313632656163643737353435366432666564 +36303631643331373965616239353762663862636232326234643663383664613666303538316465 +36323232663263653238393066663839653539343536316461333964316132353531333936663461 +37326337653930306637333163343431626663633139303263646639313862313365326665376264 +373039623038653731373939343537376634 diff --git a/roles/services/defaults/main.yml b/roles/services/defaults/main.yml index 5eb250c..6699a85 100644 --- a/roles/services/defaults/main.yml +++ b/roles/services/defaults/main.yml @@ -27,3 +27,8 @@ grafana_image: "grafana/grafana:11.6.1" # https://hub alertmanager_image: "prom/alertmanager:v0.28.1" # https://hub.docker.com/r/prom/alertmanager/tags loki_image: "grafana/loki:3.4.3" # https://hub.docker.com/r/grafana/loki/tags promtail_image: "grafana/promtail:3.4.3" # https://hub.docker.com/r/grafana/promtail/tags +crowdsec_image: "crowdsecurity/crowdsec:v1.6.8" # https://hub.docker.com/r/crowdsecurity/crowdsec/tags +crowdsec_bouncer_image: "crowdsecurity/cs-firewall-bouncer:v0.0.31" # https://hub.docker.com/r/crowdsecurity/cs-firewall-bouncer/tags +authelia_image: "authelia/authelia:4.38" # https://hub.docker.com/r/authelia/authelia/tags +redis_image: "redis:7-alpine" # shared with plane-redis +authelia_admin_user: "admin" diff --git a/roles/services/tasks/configs.yml b/roles/services/tasks/configs.yml index 2607ec5..628bdb8 100644 --- a/roles/services/tasks/configs.yml +++ b/roles/services/tasks/configs.yml @@ -134,6 +134,33 @@ mode: "0644" notify: Restart stack +- name: Deploy CrowdSec acquisition config + ansible.builtin.template: + src: crowdsec/acquis.yaml.j2 + dest: "{{ services_root }}/crowdsec/acquis.yaml" + owner: "{{ deploy_user }}" + group: "{{ deploy_group }}" + mode: "0644" + notify: Restart stack + +- name: Deploy Authelia configuration + ansible.builtin.template: + src: authelia/configuration.yml.j2 + dest: "{{ services_root }}/authelia/configuration.yml" + owner: "{{ deploy_user }}" + group: "{{ deploy_group }}" + mode: "0600" + notify: Restart stack + +- name: Deploy Authelia users database + ansible.builtin.template: + src: authelia/users.yml.j2 + dest: "{{ services_root }}/authelia/users.yml" + owner: "{{ deploy_user }}" + group: "{{ deploy_group }}" + mode: "0600" + notify: Restart stack + - name: Create acme.json for Let's Encrypt certificates ansible.builtin.file: path: "{{ services_root }}/traefik/acme.json" diff --git a/roles/services/tasks/directories.yml b/roles/services/tasks/directories.yml index 72b7069..119b98b 100644 --- a/roles/services/tasks/directories.yml +++ b/roles/services/tasks/directories.yml @@ -31,3 +31,6 @@ - grafana/provisioning/dashboards/json - prometheus/rules - loki + - traefik/logs + - crowdsec + - authelia diff --git a/roles/services/tasks/main.yml b/roles/services/tasks/main.yml index dae97e9..bbfae39 100644 --- a/roles/services/tasks/main.yml +++ b/roles/services/tasks/main.yml @@ -25,6 +25,9 @@ - "{{ alertmanager_image }}" - "{{ loki_image }}" - "{{ promtail_image }}" + - "{{ crowdsec_image }}" + - "{{ crowdsec_bouncer_image }}" + - "{{ authelia_image }}" register: pull_result changed_when: "'Status: Downloaded newer image' in pull_result.stdout" retries: 5 diff --git a/roles/services/templates/authelia/configuration.yml.j2 b/roles/services/templates/authelia/configuration.yml.j2 new file mode 100644 index 0000000..787aad8 --- /dev/null +++ b/roles/services/templates/authelia/configuration.yml.j2 @@ -0,0 +1,77 @@ +# Generated by Ansible — do not edit manually +# Authelia v4 configuration + +theme: dark + +server: + host: 0.0.0.0 + port: 9091 + +log: + level: warn + +jwt_secret: "{{ authelia_jwt_secret }}" + +default_redirection_url: "https://{{ domain_auth }}" + +session: + name: authelia_session + secret: "{{ authelia_session_secret }}" + expiration: 12h + inactivity: 30m + domain: "{{ domain_base }}" + redis: + host: authelia-redis + port: 6379 + +regulation: + max_retries: 3 + find_time: 2m + ban_time: 10m + +storage: + encryption_key: "{{ authelia_storage_key }}" + local: + path: /config/db.sqlite3 + +notifier: + disable_startup_check: true + filesystem: + filename: /config/notifications.txt + +authentication_backend: + password_reset: + disable: false + file: + path: /config/users.yml + password: + algorithm: argon2id + iterations: 3 + memory: 65536 + parallelism: 4 + key_length: 32 + salt_length: 16 + +access_control: + default_policy: deny + rules: + # Authelia portal itself — всегда доступен + - domain: "{{ domain_auth }}" + policy: bypass + + # Traefik dashboard — только admin, требует 2FA + - domain: "{{ domain_traefik }}" + policy: two_factor + subject: "group:admins" + + # Syncthing — только admin, требует 2FA + - domain: "{{ domain_sync }}" + policy: two_factor + subject: "group:admins" + + # Plane god-mode — только admin, требует 2FA + - domain: "{{ domain_plane }}" + resources: + - "^/god-mode/.*$" + policy: two_factor + subject: "group:admins" diff --git a/roles/services/templates/authelia/users.yml.j2 b/roles/services/templates/authelia/users.yml.j2 new file mode 100644 index 0000000..f5f657d --- /dev/null +++ b/roles/services/templates/authelia/users.yml.j2 @@ -0,0 +1,12 @@ +# Generated by Ansible — do not edit manually +# Authelia users database +# To update password hash: docker exec authelia authelia crypto hash generate argon2 --password 'yourpassword' +# To set up TOTP: visit https://{{ domain_auth }} and login — QR code will appear on first use + +users: + {{ authelia_admin_user }}: + displayname: "Admin" + password: "{{ authelia_admin_password_hash }}" + email: "{{ acme_email }}" + groups: + - admins diff --git a/roles/services/templates/crowdsec/acquis.yaml.j2 b/roles/services/templates/crowdsec/acquis.yaml.j2 new file mode 100644 index 0000000..ed0a061 --- /dev/null +++ b/roles/services/templates/crowdsec/acquis.yaml.j2 @@ -0,0 +1,18 @@ +# Generated by Ansible — do not edit manually +# CrowdSec acquisition config: what log sources to watch + +--- +filenames: + - /var/log/traefik/access.log +labels: + type: traefik +--- +filenames: + - /var/log/auth.log +labels: + type: syslog +--- +filenames: + - /var/log/syslog +labels: + type: syslog diff --git a/roles/services/templates/docker-compose.yml.j2 b/roles/services/templates/docker-compose.yml.j2 index 610ad57..db7a371 100644 --- a/roles/services/templates/docker-compose.yml.j2 +++ b/roles/services/templates/docker-compose.yml.j2 @@ -26,6 +26,9 @@ networks: monitoring: driver: bridge internal: true + authelia-internal: + driver: bridge + internal: true volumes: vaultwarden_data: @@ -41,6 +44,8 @@ volumes: prometheus_data: grafana_data: loki_data: + crowdsec_data: + authelia_data: services: @@ -50,6 +55,8 @@ services: image: {{ traefik_image }} container_name: traefik restart: unless-stopped + security_opt: + - no-new-privileges:true ports: - "80:80" - "443:443" @@ -60,6 +67,7 @@ services: - {{ services_root }}/traefik/traefik.yml:/etc/traefik/traefik.yml:ro - {{ services_root }}/traefik/dynamic:/etc/traefik/dynamic:ro - {{ services_root }}/traefik/acme.json:/acme/acme.json + - {{ services_root }}/traefik/logs:/var/log/traefik healthcheck: test: ["CMD", "traefik", "healthcheck", "--ping"] interval: 30s @@ -71,6 +79,8 @@ services: image: {{ vaultwarden_image }} container_name: vaultwarden restart: unless-stopped + security_opt: + - no-new-privileges:true networks: - backend volumes: @@ -94,6 +104,8 @@ services: image: {{ forgejo_image }} container_name: forgejo restart: unless-stopped + security_opt: + - no-new-privileges:true depends_on: forgejo-db: condition: service_healthy @@ -484,6 +496,8 @@ services: image: {{ grafana_image }} container_name: grafana restart: unless-stopped + security_opt: + - no-new-privileges:true depends_on: - prometheus networks: @@ -532,5 +546,75 @@ services: - /var/log:/var/log:ro - /var/lib/docker/containers:/var/lib/docker/containers:ro - /var/run/docker.sock:/var/run/docker.sock:ro + - {{ services_root }}/traefik/logs:/var/log/traefik:ro - {{ services_root }}/loki/promtail.yml:/etc/promtail/config.yml:ro command: -config.file=/etc/promtail/config.yml + + # ── Security Stack ───────────────────────────────────────────────────────── + # CrowdSec: анализирует логи Traefik, банит злоумышленников по IP + # Использует community-репутацию + локальный анализ поведения + crowdsec: + image: {{ crowdsec_image }} + container_name: crowdsec + restart: unless-stopped + networks: + - monitoring + environment: + - COLLECTIONS=crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/linux + - GID=1000 + volumes: + - crowdsec_data:/var/lib/crowdsec/data + - {{ services_root }}/crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml:ro + - {{ services_root }}/traefik/logs:/var/log/traefik:ro + - /var/log/auth.log:/var/log/auth.log:ro + - /var/log/syslog:/var/log/syslog:ro + + # Bouncer: получает решения от CrowdSec и блокирует IP через firewall + crowdsec-bouncer: + image: {{ crowdsec_bouncer_image }} + container_name: crowdsec-bouncer + restart: unless-stopped + networks: + - monitoring + environment: + - CROWDSEC_BOUNCER_API_KEY=${CROWDSEC_BOUNCER_KEY} + - CROWDSEC_AGENT_HOST=crowdsec:8080 + - GID=0 + cap_add: + - NET_ADMIN + - NET_RAW + + # ── Authelia: 2FA SSO portal ─────────────────────────────────────────────── + # Защищает: Traefik dashboard, Syncthing, Plane /god-mode/ + # Вход: логин + пароль + TOTP (Google Authenticator) + authelia: + image: {{ authelia_image }} + container_name: authelia + restart: unless-stopped + depends_on: + - authelia-redis + networks: + - backend + - authelia-internal + volumes: + - authelia_data:/config + - {{ services_root }}/authelia/configuration.yml:/config/configuration.yml:ro + - {{ services_root }}/authelia/users.yml:/config/users.yml:ro + environment: + - AUTHELIA_JWT_SECRET=${AUTHELIA_JWT_SECRET} + - AUTHELIA_SESSION_SECRET=${AUTHELIA_SESSION_SECRET} + - AUTHELIA_STORAGE_ENCRYPTION_KEY=${AUTHELIA_STORAGE_KEY} + - TZ=UTC + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:9091/api/health"] + interval: 30s + timeout: 5s + retries: 3 + + authelia-redis: + image: {{ redis_image }} + container_name: authelia-redis + restart: unless-stopped + networks: + - authelia-internal + command: redis-server --appendonly yes --maxmemory 64mb --maxmemory-policy allkeys-lru diff --git a/roles/services/templates/env.j2 b/roles/services/templates/env.j2 index 67be240..328a79d 100644 --- a/roles/services/templates/env.j2 +++ b/roles/services/templates/env.j2 @@ -12,3 +12,7 @@ DOMAIN_SYNC={{ domain_sync }} DOMAIN_TRAEFIK={{ domain_traefik }} FORGEJO_RUNNER_TOKEN={{ forgejo_runner_token }} GRAFANA_ADMIN_PASSWORD={{ grafana_admin_password }} +AUTHELIA_JWT_SECRET={{ authelia_jwt_secret }} +AUTHELIA_SESSION_SECRET={{ authelia_session_secret }} +AUTHELIA_STORAGE_KEY={{ authelia_storage_key }} +CROWDSEC_BOUNCER_KEY={{ crowdsec_bouncer_key }} diff --git a/roles/services/templates/traefik/dynamic/routes.yml.j2 b/roles/services/templates/traefik/dynamic/routes.yml.j2 index 5da4fc2..ee0014c 100644 --- a/roles/services/templates/traefik/dynamic/routes.yml.j2 +++ b/roles/services/templates/traefik/dynamic/routes.yml.j2 @@ -9,7 +9,7 @@ http: tls: certresolver: letsencrypt service: api@internal - middlewares: [traefik-auth] + middlewares: [authelia@docker, rate-limit-strict] vaultwarden: rule: "Host(`{{ domain_vault }}`)" @@ -17,6 +17,7 @@ http: tls: certresolver: letsencrypt service: vaultwarden + middlewares: [rate-limit-default] forgejo: rule: "Host(`{{ domain_git }}`)" @@ -24,6 +25,7 @@ http: tls: certresolver: letsencrypt service: forgejo + middlewares: [rate-limit-default] plane-api: rule: "Host(`{{ domain_plane }}`) && (PathPrefix(`/api/`) || PathPrefix(`/auth/`))" @@ -31,6 +33,7 @@ http: tls: certresolver: letsencrypt service: plane-api + middlewares: [rate-limit-api] plane: rule: "Host(`{{ domain_plane }}`)" @@ -38,6 +41,25 @@ http: tls: certresolver: letsencrypt service: plane-web + middlewares: [rate-limit-default] + + plane-godmode: + rule: "Host(`{{ domain_plane }}`) && PathPrefix(`/god-mode/`)" + entrypoints: [websecure] + tls: + certresolver: letsencrypt + service: plane-admin + middlewares: [authelia@docker, rate-limit-strict] + priority: 10 + + plane-spaces: + rule: "Host(`{{ domain_plane }}`) && PathPrefix(`/spaces/`)" + entrypoints: [websecure] + tls: + certresolver: letsencrypt + service: plane-space + middlewares: [rate-limit-default] + priority: 10 syncthing: rule: "Host(`{{ domain_sync }}`)" @@ -45,7 +67,7 @@ http: tls: certresolver: letsencrypt service: syncthing - middlewares: [syncthing-auth] + middlewares: [authelia@docker, rate-limit-strict] grafana: rule: "Host(`{{ domain_dashboard }}`)" @@ -53,6 +75,15 @@ http: tls: certresolver: letsencrypt service: grafana + middlewares: [rate-limit-default] + + authelia: + rule: "Host(`{{ domain_auth }}`)" + entrypoints: [websecure] + tls: + certresolver: letsencrypt + service: authelia + middlewares: [rate-limit-strict] services: vaultwarden: @@ -75,6 +106,16 @@ http: servers: - url: "http://plane-web:3000" + plane-admin: + loadBalancer: + servers: + - url: "http://plane-admin:80" + + plane-space: + loadBalancer: + servers: + - url: "http://plane-space:3000" + syncthing: loadBalancer: servers: @@ -85,7 +126,51 @@ http: servers: - url: "http://grafana:3000" + authelia: + loadBalancer: + servers: + - url: "http://authelia:9091" + middlewares: + # ── Security Headers (applied globally via entrypoint) ───────────────── + security-headers: + headers: + stsSeconds: 31536000 + stsIncludeSubdomains: true + stsPreload: true + forceSTSHeader: true + frameDeny: true + contentTypeNosniff: true + browserXssFilter: true + referrerPolicy: "strict-origin-when-cross-origin" + permissionsPolicy: "camera=(), microphone=(), geolocation=(), payment=()" + customResponseHeaders: + X-Robots-Tag: "noindex, nofollow" + Server: "" + + # ── Rate Limiting ────────────────────────────────────────────────────── + # Default: 100 req/s burst 50 — general web traffic + rate-limit-default: + rateLimit: + average: 100 + burst: 50 + period: 1s + + # API: 30 req/s burst 20 — API endpoints + rate-limit-api: + rateLimit: + average: 30 + burst: 20 + period: 1s + + # Strict: 10 req/s burst 5 — admin/login interfaces + rate-limit-strict: + rateLimit: + average: 10 + burst: 5 + period: 1s + + # ── Auth (legacy basic auth kept for direct fallback) ───────────────── traefik-auth: basicAuth: users: @@ -95,3 +180,14 @@ http: basicAuth: users: - "{{ syncthing_basic_auth_htpasswd }}" + + # ── Authelia ForwardAuth ─────────────────────────────────────────────── + authelia: + forwardAuth: + address: "http://authelia:9091/api/verify?rd=https://{{ domain_auth }}" + trustForwardHeader: true + authResponseHeaders: + - Remote-User + - Remote-Groups + - Remote-Email + - Remote-Name diff --git a/roles/services/templates/traefik/traefik.yml.j2 b/roles/services/templates/traefik/traefik.yml.j2 index c24052f..1875238 100644 --- a/roles/services/templates/traefik/traefik.yml.j2 +++ b/roles/services/templates/traefik/traefik.yml.j2 @@ -8,7 +8,16 @@ global: log: level: INFO -accessLog: {} +accessLog: + filePath: /var/log/traefik/access.log + bufferingSize: 100 + fields: + defaultMode: keep + headers: + defaultMode: drop + names: + User-Agent: keep + Referer: drop api: dashboard: true @@ -26,6 +35,9 @@ entryPoints: scheme: https websecure: address: ":443" + http: + middlewares: + - security-headers@file certificatesResolvers: letsencrypt: