239 lines
6.6 KiB
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
|
|
];
|
|
};
|
|
}
|