mirror of
https://github.com/rubenhensen/k8scd.git
synced 2026-09-18 02:22:56 +02:00
Add nix-infra-machine
This commit is contained in:
@@ -0,0 +1,718 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
let
|
||||
appName = "haproxy";
|
||||
|
||||
cfg = config.infrastructure.${appName};
|
||||
|
||||
# Generate combined PEM file path for a domain
|
||||
combinedPemPath = domain: "/var/lib/acme/${domain}/combined.pem";
|
||||
|
||||
# Self-signed certificate directory
|
||||
selfSignedCertDir = "/var/lib/haproxy/certs";
|
||||
|
||||
# Script to concatenate fullchain.pem and privkey.pem for HAProxy
|
||||
# HAProxy requires a single file with cert chain + private key
|
||||
mkCombinePemScript = domain: pkgs.writeShellScript "combine-pem-${domain}" ''
|
||||
ACME_DIR="/var/lib/acme/${domain}"
|
||||
COMBINED="$ACME_DIR/combined.pem"
|
||||
|
||||
if [ -f "$ACME_DIR/fullchain.pem" ] && [ -f "$ACME_DIR/privkey.pem" ]; then
|
||||
cat "$ACME_DIR/fullchain.pem" "$ACME_DIR/privkey.pem" > "$COMBINED"
|
||||
chmod 640 "$COMBINED"
|
||||
chown acme:haproxy "$COMBINED"
|
||||
fi
|
||||
'';
|
||||
|
||||
# Script to generate self-signed certificates for testing
|
||||
mkSelfSignedCertScript = domain: pkgs.writeShellScript "generate-self-signed-${domain}" ''
|
||||
CERT_DIR="${selfSignedCertDir}"
|
||||
COMBINED="$CERT_DIR/${domain}.pem"
|
||||
|
||||
mkdir -p "$CERT_DIR"
|
||||
|
||||
# Only generate if not exists or expired
|
||||
if [ ! -f "$COMBINED" ] || ! ${pkgs.openssl}/bin/openssl x509 -checkend 86400 -noout -in "$COMBINED" 2>/dev/null; then
|
||||
echo "Generating self-signed certificate for ${domain}..."
|
||||
${pkgs.openssl}/bin/openssl req -x509 -newkey rsa:4096 \
|
||||
-keyout "$CERT_DIR/${domain}.key" \
|
||||
-out "$CERT_DIR/${domain}.crt" \
|
||||
-sha256 -days 365 -nodes \
|
||||
-subj "/CN=${domain}" \
|
||||
-addext "subjectAltName=DNS:${domain},DNS:*.${domain}"
|
||||
|
||||
# Combine for HAProxy
|
||||
cat "$CERT_DIR/${domain}.crt" "$CERT_DIR/${domain}.key" > "$COMBINED"
|
||||
chmod 640 "$COMBINED"
|
||||
chown haproxy:haproxy "$COMBINED"
|
||||
rm -f "$CERT_DIR/${domain}.key" "$CERT_DIR/${domain}.crt"
|
||||
fi
|
||||
'';
|
||||
|
||||
# Default HAProxy global configuration
|
||||
defaultGlobalConfig = ''
|
||||
global
|
||||
log /dev/log local0
|
||||
log /dev/log local1 notice
|
||||
maxconn 4096
|
||||
# Modern SSL settings
|
||||
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
|
||||
ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11
|
||||
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
|
||||
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11
|
||||
tune.ssl.default-dh-param 2048
|
||||
'';
|
||||
|
||||
# Default HAProxy defaults configuration
|
||||
defaultDefaultsConfig = ''
|
||||
defaults
|
||||
log global
|
||||
mode http
|
||||
option httplog
|
||||
option dontlognull
|
||||
option forwardfor
|
||||
option http-server-close
|
||||
timeout connect 5s
|
||||
timeout client 50s
|
||||
timeout server 50s
|
||||
timeout http-request 10s
|
||||
timeout http-keep-alive 10s
|
||||
errorfile 400 /dev/null
|
||||
errorfile 403 /dev/null
|
||||
errorfile 408 /dev/null
|
||||
errorfile 500 /dev/null
|
||||
errorfile 502 /dev/null
|
||||
errorfile 503 /dev/null
|
||||
errorfile 504 /dev/null
|
||||
'';
|
||||
|
||||
in
|
||||
{
|
||||
options.infrastructure.${appName} = {
|
||||
enable = lib.mkEnableOption "infrastructure.haproxy";
|
||||
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
description = "HAProxy package to use.";
|
||||
default = pkgs.haproxy;
|
||||
example = "pkgs.haproxy-lts";
|
||||
};
|
||||
|
||||
openFirewall = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
description = "Whether to open firewall ports for HTTP (80) and HTTPS (443).";
|
||||
default = true;
|
||||
};
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "User account under which HAProxy runs.";
|
||||
default = "haproxy";
|
||||
};
|
||||
|
||||
group = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Group account under which HAProxy runs.";
|
||||
default = "haproxy";
|
||||
};
|
||||
|
||||
# ==========================================================================
|
||||
# Let's Encrypt / ACME Configuration
|
||||
# ==========================================================================
|
||||
|
||||
acme = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
description = "Enable ACME (Let's Encrypt) certificate management.";
|
||||
default = false;
|
||||
};
|
||||
|
||||
acceptTerms = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
description = ''
|
||||
Accept the ACME provider's terms of service.
|
||||
For Let's Encrypt: https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf
|
||||
'';
|
||||
default = false;
|
||||
};
|
||||
|
||||
email = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
description = "Default email address for ACME certificate registration and renewal notifications.";
|
||||
default = null;
|
||||
example = "admin@example.com";
|
||||
};
|
||||
|
||||
staging = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
description = ''
|
||||
Use Let's Encrypt staging server for testing.
|
||||
Certificates won't be trusted but you won't hit rate limits.
|
||||
'';
|
||||
default = false;
|
||||
};
|
||||
|
||||
domains = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.submodule {
|
||||
options = {
|
||||
extraDomainNames = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "Additional domain names (SANs) for this certificate.";
|
||||
default = [];
|
||||
example = [ "www.example.com" "api.example.com" ];
|
||||
};
|
||||
webroot = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
description = "Webroot path for HTTP-01 challenge. If null, standalone mode is used.";
|
||||
default = "/var/lib/acme/acme-challenge";
|
||||
};
|
||||
extraConfig = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.anything;
|
||||
description = "Extra configuration options for this certificate.";
|
||||
default = {};
|
||||
};
|
||||
};
|
||||
});
|
||||
description = ''
|
||||
Domains to obtain certificates for. The key is the primary domain name.
|
||||
Use extraDomainNames for additional SANs (Subject Alternative Names).
|
||||
'';
|
||||
default = {};
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
"example.com" = {
|
||||
extraDomainNames = [ "www.example.com" ];
|
||||
};
|
||||
"api.example.com" = {};
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.anything;
|
||||
description = ''
|
||||
Extra configuration options passed to security.acme.defaults.
|
||||
See https://nixos.org/manual/nixos/stable/#module-security-acme for options.
|
||||
'';
|
||||
default = {};
|
||||
example = {
|
||||
renewInterval = "daily";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# ==========================================================================
|
||||
# Self-Signed Certificate Configuration (for testing)
|
||||
# ==========================================================================
|
||||
|
||||
selfSigned = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
description = ''
|
||||
Enable self-signed certificate generation for testing.
|
||||
These certificates are NOT trusted by browsers but useful for development/testing.
|
||||
'';
|
||||
default = false;
|
||||
};
|
||||
|
||||
domains = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "List of domains to generate self-signed certificates for.";
|
||||
default = [];
|
||||
example = [ "localhost" "test.local" ];
|
||||
};
|
||||
|
||||
regenerate = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
description = "Force regeneration of self-signed certificates on each activation.";
|
||||
default = false;
|
||||
};
|
||||
};
|
||||
|
||||
# ==========================================================================
|
||||
# SSL/TLS Configuration
|
||||
# ==========================================================================
|
||||
|
||||
ssl = {
|
||||
minVersion = lib.mkOption {
|
||||
type = lib.types.enum [ "TLSv1.2" "TLSv1.3" ];
|
||||
description = "Minimum TLS version to accept.";
|
||||
default = "TLSv1.2";
|
||||
};
|
||||
|
||||
ciphers = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
description = "Custom cipher suite for TLS 1.2 and below.";
|
||||
default = null;
|
||||
example = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256";
|
||||
};
|
||||
|
||||
ciphersuites = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
description = "Custom cipher suite for TLS 1.3.";
|
||||
default = null;
|
||||
example = "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384";
|
||||
};
|
||||
|
||||
hsts = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
description = "Enable HTTP Strict Transport Security (HSTS) header.";
|
||||
default = false;
|
||||
};
|
||||
|
||||
maxAge = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
description = "HSTS max-age in seconds.";
|
||||
default = 31536000; # 1 year
|
||||
};
|
||||
|
||||
includeSubDomains = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
description = "Include subdomains in HSTS policy.";
|
||||
default = true;
|
||||
};
|
||||
|
||||
preload = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
description = "Add preload directive to HSTS header.";
|
||||
default = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# ==========================================================================
|
||||
# HTTP to HTTPS Redirect
|
||||
# ==========================================================================
|
||||
|
||||
httpToHttpsRedirect = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
description = ''
|
||||
Automatically redirect HTTP requests to HTTPS.
|
||||
Creates a frontend on port 80 that redirects all traffic to HTTPS.
|
||||
'';
|
||||
default = false;
|
||||
};
|
||||
|
||||
code = lib.mkOption {
|
||||
type = lib.types.enum [ 301 302 307 308 ];
|
||||
description = "HTTP redirect status code to use.";
|
||||
default = 301;
|
||||
};
|
||||
|
||||
excludePaths = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "Paths to exclude from redirect (e.g., ACME challenge).";
|
||||
default = [ "/.well-known/acme-challenge/" ];
|
||||
};
|
||||
};
|
||||
|
||||
# ==========================================================================
|
||||
# HAProxy Configuration
|
||||
# ==========================================================================
|
||||
|
||||
globalConfig = lib.mkOption {
|
||||
type = lib.types.lines;
|
||||
description = "HAProxy global section configuration.";
|
||||
default = defaultGlobalConfig;
|
||||
example = ''
|
||||
global
|
||||
log /dev/log local0
|
||||
maxconn 2048
|
||||
'';
|
||||
};
|
||||
|
||||
defaultsConfig = lib.mkOption {
|
||||
type = lib.types.lines;
|
||||
description = "HAProxy defaults section configuration.";
|
||||
default = defaultDefaultsConfig;
|
||||
example = ''
|
||||
defaults
|
||||
log global
|
||||
mode http
|
||||
timeout connect 5s
|
||||
timeout client 50s
|
||||
timeout server 50s
|
||||
'';
|
||||
};
|
||||
|
||||
frontends = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.submodule {
|
||||
options = {
|
||||
bind = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "Bind addresses and ports.";
|
||||
default = [];
|
||||
example = [ "*:80" "*:443 ssl crt /path/to/cert.pem" ];
|
||||
};
|
||||
mode = lib.mkOption {
|
||||
type = lib.types.enum [ "http" "tcp" ];
|
||||
description = "Frontend mode.";
|
||||
default = "http";
|
||||
};
|
||||
options = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "HAProxy options for this frontend.";
|
||||
default = [];
|
||||
example = [ "httplog" "forwardfor" ];
|
||||
};
|
||||
acls = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "ACL definitions.";
|
||||
default = [];
|
||||
example = [ "is_api path_beg /api" "is_static path_beg /static" ];
|
||||
};
|
||||
httpRequest = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "http-request rules.";
|
||||
default = [];
|
||||
example = [ "set-header X-Forwarded-Proto https if { ssl_fc }" ];
|
||||
};
|
||||
httpResponse = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "http-response rules.";
|
||||
default = [];
|
||||
example = [ "set-header Strict-Transport-Security max-age=31536000" ];
|
||||
};
|
||||
tcpRequest = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "tcp-request rules (for TCP mode).";
|
||||
default = [];
|
||||
example = [ "inspect-delay 5s" "content accept if { req_ssl_hello_type 1 }" ];
|
||||
};
|
||||
useBackend = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "use_backend rules.";
|
||||
default = [];
|
||||
example = [ "api_backend if is_api" "static_backend if is_static" ];
|
||||
};
|
||||
defaultBackend = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
description = "Default backend for this frontend.";
|
||||
default = null;
|
||||
example = "web_backend";
|
||||
};
|
||||
extraConfig = lib.mkOption {
|
||||
type = lib.types.lines;
|
||||
description = "Extra configuration for this frontend.";
|
||||
default = "";
|
||||
};
|
||||
};
|
||||
});
|
||||
description = "HAProxy frontend configurations.";
|
||||
default = {};
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
http = {
|
||||
bind = [ "*:80" ];
|
||||
defaultBackend = "web_backend";
|
||||
};
|
||||
https = {
|
||||
bind = [ "*:443 ssl crt /var/lib/acme/example.com/combined.pem" ];
|
||||
httpRequest = [ "set-header X-Forwarded-Proto https" ];
|
||||
defaultBackend = "web_backend";
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
backends = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.submodule {
|
||||
options = {
|
||||
mode = lib.mkOption {
|
||||
type = lib.types.enum [ "http" "tcp" ];
|
||||
description = "Backend mode.";
|
||||
default = "http";
|
||||
};
|
||||
balance = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Load balancing algorithm.";
|
||||
default = "roundrobin";
|
||||
example = "leastconn";
|
||||
};
|
||||
options = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "HAProxy options for this backend.";
|
||||
default = [];
|
||||
example = [ "httpchk GET /health" ];
|
||||
};
|
||||
httpRequest = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "http-request rules for this backend.";
|
||||
default = [];
|
||||
};
|
||||
httpResponse = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "http-response rules for this backend.";
|
||||
default = [];
|
||||
};
|
||||
tcpCheck = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "tcp-check rules for TCP mode health checking.";
|
||||
default = [];
|
||||
example = [ "connect" "send PING\\r\\n" "expect string +PONG" ];
|
||||
};
|
||||
servers = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "Backend server definitions.";
|
||||
default = [];
|
||||
example = [ "server1 127.0.0.1:8080 check" "server2 127.0.0.1:8081 check" ];
|
||||
};
|
||||
extraConfig = lib.mkOption {
|
||||
type = lib.types.lines;
|
||||
description = "Extra configuration for this backend.";
|
||||
default = "";
|
||||
};
|
||||
};
|
||||
});
|
||||
description = "HAProxy backend configurations.";
|
||||
default = {};
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
web_backend = {
|
||||
balance = "roundrobin";
|
||||
servers = [ "web1 127.0.0.1:8080 check" "web2 127.0.0.1:8081 check" ];
|
||||
options = [ "httpchk GET /health" ];
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
listen = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.submodule {
|
||||
options = {
|
||||
bind = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "Bind addresses and ports.";
|
||||
default = [];
|
||||
};
|
||||
mode = lib.mkOption {
|
||||
type = lib.types.enum [ "http" "tcp" ];
|
||||
description = "Listen mode.";
|
||||
default = "http";
|
||||
};
|
||||
balance = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
description = "Load balancing algorithm.";
|
||||
default = null;
|
||||
};
|
||||
options = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "HAProxy options for this listen section.";
|
||||
default = [];
|
||||
};
|
||||
servers = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "Server definitions.";
|
||||
default = [];
|
||||
};
|
||||
extraConfig = lib.mkOption {
|
||||
type = lib.types.lines;
|
||||
description = "Extra configuration for this listen section.";
|
||||
default = "";
|
||||
};
|
||||
};
|
||||
});
|
||||
description = "HAProxy listen sections (combined frontend/backend).";
|
||||
default = {};
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
stats = {
|
||||
bind = [ "*:8404" ];
|
||||
options = [ "http-use-htx" "httplog" ];
|
||||
extraConfig = '''
|
||||
stats enable
|
||||
stats uri /stats
|
||||
stats refresh 10s
|
||||
''';
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = lib.mkOption {
|
||||
type = lib.types.lines;
|
||||
description = "Extra HAProxy configuration appended to the config file.";
|
||||
default = "";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
# Assertions
|
||||
assertions = [
|
||||
{
|
||||
assertion = !(cfg.acme.enable && cfg.selfSigned.enable);
|
||||
message = "Cannot enable both ACME and self-signed certificates. Choose one.";
|
||||
}
|
||||
{
|
||||
assertion = cfg.acme.enable -> cfg.acme.acceptTerms;
|
||||
message = "You must accept the ACME terms of service to use Let's Encrypt.";
|
||||
}
|
||||
{
|
||||
assertion = cfg.acme.enable -> cfg.acme.email != null;
|
||||
message = "You must provide an email address for ACME certificate registration.";
|
||||
}
|
||||
];
|
||||
|
||||
# ACME configuration for Let's Encrypt
|
||||
security.acme = lib.mkIf cfg.acme.enable {
|
||||
acceptTerms = cfg.acme.acceptTerms;
|
||||
defaults = {
|
||||
email = cfg.acme.email;
|
||||
server = lib.mkIf cfg.acme.staging "https://acme-staging-v02.api.letsencrypt.org/directory";
|
||||
webroot = "/var/lib/acme/acme-challenge";
|
||||
group = "haproxy";
|
||||
} // cfg.acme.extraConfig;
|
||||
|
||||
# Create certificate configurations for each domain
|
||||
certs = lib.mapAttrs (domain: domainCfg: {
|
||||
inherit (domainCfg) extraDomainNames;
|
||||
webroot = domainCfg.webroot;
|
||||
# Reload HAProxy after certificate renewal
|
||||
postRun = ''
|
||||
# Combine fullchain and privkey for HAProxy
|
||||
${mkCombinePemScript domain}
|
||||
# Reload HAProxy to pick up new certificates
|
||||
${pkgs.systemd}/bin/systemctl reload haproxy.service || true
|
||||
'';
|
||||
} // domainCfg.extraConfig) cfg.acme.domains;
|
||||
};
|
||||
|
||||
# Ensure haproxy user is in acme group to read certificates
|
||||
users.users.haproxy = lib.mkIf cfg.acme.enable {
|
||||
extraGroups = [ "acme" ];
|
||||
};
|
||||
|
||||
# Create directories for ACME and self-signed certificates
|
||||
systemd.tmpfiles.rules =
|
||||
lib.optionals cfg.acme.enable [
|
||||
"d /var/lib/acme/acme-challenge 0755 acme acme -"
|
||||
"d /var/lib/acme/acme-challenge/.well-known 0755 acme acme -"
|
||||
"d /var/lib/acme/acme-challenge/.well-known/acme-challenge 0755 acme acme -"
|
||||
] ++
|
||||
lib.optionals cfg.selfSigned.enable [
|
||||
"d ${selfSignedCertDir} 0750 haproxy haproxy -"
|
||||
];
|
||||
|
||||
# Self-signed certificate generation service
|
||||
systemd.services.haproxy-generate-self-signed = lib.mkIf cfg.selfSigned.enable {
|
||||
description = "Generate self-signed certificates for HAProxy";
|
||||
wantedBy = [ "haproxy.service" ];
|
||||
before = [ "haproxy.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = lib.concatMapStringsSep "\n" (domain:
|
||||
"${mkSelfSignedCertScript domain}"
|
||||
) cfg.selfSigned.domains;
|
||||
};
|
||||
|
||||
# HAProxy configuration
|
||||
services.haproxy = {
|
||||
enable = true;
|
||||
package = cfg.package;
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
|
||||
config = let
|
||||
# HSTS header value
|
||||
hstsHeader = lib.optionalString cfg.ssl.hsts.enable (
|
||||
"max-age=${toString cfg.ssl.hsts.maxAge}" +
|
||||
lib.optionalString cfg.ssl.hsts.includeSubDomains "; includeSubDomains" +
|
||||
lib.optionalString cfg.ssl.hsts.preload "; preload"
|
||||
);
|
||||
|
||||
# HTTP to HTTPS redirect frontend
|
||||
httpRedirectFrontend = lib.optionalString cfg.httpToHttpsRedirect.enable ''
|
||||
frontend http-redirect
|
||||
bind *:80
|
||||
mode http
|
||||
${lib.concatMapStringsSep "\n " (path: "acl is_acme path_beg ${path}") cfg.httpToHttpsRedirect.excludePaths}
|
||||
${lib.optionalString (cfg.httpToHttpsRedirect.excludePaths != []) "use_backend acme_backend if is_acme"}
|
||||
http-request redirect scheme https code ${toString cfg.httpToHttpsRedirect.code} unless { ssl_fc }${lib.optionalString (cfg.httpToHttpsRedirect.excludePaths != []) " or is_acme"}
|
||||
'';
|
||||
|
||||
# Generate frontend configuration
|
||||
frontendConfigs = lib.concatStringsSep "\n\n" (lib.mapAttrsToList (name: frontend: ''
|
||||
frontend ${name}
|
||||
${lib.concatMapStringsSep "\n " (b: "bind ${b}") frontend.bind}
|
||||
mode ${frontend.mode}
|
||||
${lib.concatMapStringsSep "\n " (o: "option ${o}") frontend.options}
|
||||
${lib.concatMapStringsSep "\n " (a: "acl ${a}") frontend.acls}
|
||||
${lib.concatMapStringsSep "\n " (r: "http-request ${r}") frontend.httpRequest}
|
||||
${lib.concatMapStringsSep "\n " (r: "http-response ${r}") frontend.httpResponse}
|
||||
${lib.concatMapStringsSep "\n " (r: "tcp-request ${r}") frontend.tcpRequest}
|
||||
${lib.optionalString (cfg.ssl.hsts.enable && frontend.mode == "http") "http-response set-header Strict-Transport-Security \"${hstsHeader}\""}
|
||||
${lib.concatMapStringsSep "\n " (u: "use_backend ${u}") frontend.useBackend}
|
||||
${lib.optionalString (frontend.defaultBackend != null) "default_backend ${frontend.defaultBackend}"}
|
||||
${frontend.extraConfig}
|
||||
'') cfg.frontends);
|
||||
|
||||
# Generate backend configuration
|
||||
backendConfigs = lib.concatStringsSep "\n\n" (lib.mapAttrsToList (name: backend: ''
|
||||
backend ${name}
|
||||
mode ${backend.mode}
|
||||
balance ${backend.balance}
|
||||
${lib.concatMapStringsSep "\n " (o: "option ${o}") backend.options}
|
||||
${lib.concatMapStringsSep "\n " (r: "http-request ${r}") backend.httpRequest}
|
||||
${lib.concatMapStringsSep "\n " (r: "http-response ${r}") backend.httpResponse}
|
||||
${lib.concatMapStringsSep "\n " (c: "tcp-check ${c}") backend.tcpCheck}
|
||||
${lib.concatMapStringsSep "\n " (s: "server ${s}") backend.servers}
|
||||
${backend.extraConfig}
|
||||
'') cfg.backends);
|
||||
|
||||
# Generate listen configuration
|
||||
listenConfigs = lib.concatStringsSep "\n\n" (lib.mapAttrsToList (name: listenCfg: ''
|
||||
listen ${name}
|
||||
${lib.concatMapStringsSep "\n " (b: "bind ${b}") listenCfg.bind}
|
||||
mode ${listenCfg.mode}
|
||||
${lib.optionalString (listenCfg.balance != null) "balance ${listenCfg.balance}"}
|
||||
${lib.concatMapStringsSep "\n " (o: "option ${o}") listenCfg.options}
|
||||
${lib.concatMapStringsSep "\n " (s: "server ${s}") listenCfg.servers}
|
||||
${listenCfg.extraConfig}
|
||||
'') cfg.listen);
|
||||
|
||||
in ''
|
||||
${cfg.globalConfig}
|
||||
|
||||
${cfg.defaultsConfig}
|
||||
|
||||
${httpRedirectFrontend}
|
||||
|
||||
${frontendConfigs}
|
||||
|
||||
${backendConfigs}
|
||||
|
||||
${listenConfigs}
|
||||
|
||||
${cfg.extraConfig}
|
||||
'';
|
||||
};
|
||||
|
||||
# Ensure HAProxy starts after certificates are ready
|
||||
systemd.services.haproxy = lib.mkMerge [
|
||||
(lib.mkIf cfg.acme.enable {
|
||||
wants = lib.mapAttrsToList (domain: _: "acme-${domain}.service") cfg.acme.domains;
|
||||
after = lib.mapAttrsToList (domain: _: "acme-${domain}.service") cfg.acme.domains;
|
||||
})
|
||||
(lib.mkIf cfg.selfSigned.enable {
|
||||
wants = [ "haproxy-generate-self-signed.service" ];
|
||||
after = [ "haproxy-generate-self-signed.service" ];
|
||||
})
|
||||
{
|
||||
serviceConfig = {
|
||||
# Allow HAProxy to reload without restart
|
||||
ExecReload = "${pkgs.coreutils}/bin/kill -USR2 $MAINPID";
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
# Open firewall for HTTP/HTTPS
|
||||
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ 80 443 ];
|
||||
|
||||
# Install useful utilities
|
||||
environment.systemPackages = [ cfg.package pkgs.curl pkgs.openssl ];
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user