Files
nixos/services/traefik.nix

239 lines
6.6 KiB
Nix

# Module: services/traefik
# Enables the traefik reverse proxy
{
config,
lib,
pkgs,
pkgsUnstable,
inputs,
...
}:
with lib;
let
cfg = config.traefik;
traefikPrometheusPort = 8082;
acmeFile = "/var/lib/traefik/acme.json";
letsEncryptEmail = "eric.torres@its-et.me";
letsEncryptStaging = "https://acme-staging-v02.api.letsencrypt.org/directory";
letsEncryptProd = "https://acme-v02.api.letsencrypt.org/directory";
logDir = "/var/log/traefik";
workingDir = "~";
in
{
options.traefik = {
enable = mkEnableOption "Enables traefik module";
redirectHttps = mkOption {
type = types.bool;
default = false;
description = "Set up HTTPs redirect on the web entryPoint";
example = true;
};
};
config = mkIf cfg.enable {
# Needed to be able to access internal-only services with tailscale
services.tailscale.permitCertUid = "traefik";
# Create log dir for access logs, then allow traefik to access them
# We need to set the working directory to /var/lib, for the plugins-storage directory
systemd = {
tmpfiles.rules = [
"d ${logDir} 0750 traefik traefik -"
];
services.traefik.serviceConfig = {
ReadWritePaths = [ logDir ];
WorkingDirectory = workingDir;
};
};
# We want the alloy collector to be able to read traefik logs
systemd.services.alloy.serviceConfig.SupplementaryGroups = [ "traefik" ];
services.traefik = {
enable = true;
package = pkgsUnstable.traefik;
staticConfigOptions = {
accessLog = {
format = "json";
filePath = "${logDir}/access.log";
filters.statusCodes = [
"200"
"400-404"
"500-503"
];
fields = {
names = {
ClientUsername = "drop";
};
headers = {
defaultMode = "keep";
names = {
User-Agent = "keep";
Content-Type = "keep";
};
};
};
};
metrics = {
addInternals = true;
prometheus = {
addEntrypointsLabels = true;
addRoutersLabels = true;
entrypoint = "prometheus";
};
};
entryPoints = {
web = {
address = ":80";
asDefault = true;
forwardedHeaders.trustedIPs = [ "100.64.10.0/23" ];
proxyProtocol.trustedIPs = [ "100.64.10.0/23" ];
http = {
redirections = mkIf cfg.redirectHttps {
entryPoint = {
to = "websecure";
scheme = "https";
permanent = true;
};
};
middlewares = [
"compress-content@file"
"default-headers@file"
"ratelimit@file"
];
};
};
websecure = {
address = ":443";
proxyProtocol.trustedIPs = [ "100.64.10.0/23" ];
http = {
tls.certResolver = "tailscale";
middlewares = [
"compress-content@file"
"default-headers@file"
"ratelimit@file"
#"crowdsec-bouncer@file"
];
};
http3.advertisedPort = 443;
};
prometheus = {
address = "127.0.0.1:${toString traefikPrometheusPort}";
};
};
certificatesResolvers = {
staging.acme = {
email = letsEncryptEmail;
storage = acmeFile;
caServer = letsEncryptStaging;
tlsChallenge = { };
};
production.acme = {
email = letsEncryptEmail;
storage = acmeFile;
caServer = letsEncryptProd;
tlsChallenge = { };
};
tailscale.tailscale = { };
};
};
dynamicConfigOptions = {
http.middlewares = {
compress-content.compress = { };
default-headers = {
headers = {
# ----- Security headers -----
browserXssFilter = true;
contentTypeNosniff = true;
forceSTSHeader = true;
stsIncludeSubdomains = true;
stsPreload = true;
# HSTS max-age attribute set to 1 year
stsSeconds = 31536000;
# We want to use same-origin, otherwise csrf verification fails for django
referrerPolicy = "same-origin";
# Disable for now, it breaks my websites
#contentSecurityPolicy= "default-src 'self'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; base-uri 'self'; form-action 'self'"
permissionsPolicy = "geolocation=(self 'https://its-et.me'), camera=(), microphone=(), payment=(), usb=(), vr=()";
customFrameOptionsValue = "SAMEORIGIN";
frameDeny = true;
# ----- Custom Headers -----
customRequestHeaders = {
X-Forwarded-Proto = "https";
};
customResponseHeaders = {
X-Powered-By = "";
};
};
};
limiter.circuitBreaker.expression = "LatencyAtQuantileMS(50.0) > 750 || ResponseCodeRatio(500, 600, 0, 600) > 0.30";
ratelimit.rateLimit = {
average = 150;
burst = 75;
};
strip-www.redirectRegex = {
regex = "^https?://www\\.(.*)";
replacement = "https://$1";
permanent = true;
};
};
};
};
environment.etc."alloy/traefik.alloy".text = ''
prometheus.scrape "traefik_scrape" {
targets = [
{
"__address__" = "127.0.0.1:${toString traefikPrometheusPort}",
},
]
forward_to = [prometheus.remote_write.default.receiver]
job_name = "traefik"
}
local.file_match "traefik_access_logs" {
path_targets = [
{
__path__ ="${logDir}/access.log",
"job" = "traefik_access_logs",
"instance" = "${config.networking.hostName}",
},
]
sync_period = "10s"
}
loki.source.file "traefik_access_logs" {
targets = local.file_match.traefik_access_logs.targets
forward_to = [loki.write.default.receiver]
}
'';
# Open firewall for 80 and 443, including http3
networking.firewall.allowedTCPPorts = [
80
443
];
networking.firewall.allowedUDPPorts = [
443
];
};
}