Support multiple dns zones per host
Some checks failed
/ rust-packages (push) Successful in 4m2s
/ dev-shell (push) Successful in 1m3s
/ terraform-providers (push) Successful in 8m7s
/ check (push) Failing after 7m43s
/ systems (push) Successful in 30m24s

This commit is contained in:
Kaare Hoff Skovgaard 2025-07-23 23:28:15 +02:00
parent 46375018e0
commit d842025c81
Signed by: khs
GPG key ID: C7D890804F01E9F0
12 changed files with 301 additions and 88 deletions

View file

@ -160,7 +160,6 @@ in
enable = true; enable = true;
dns = { dns = {
enable = true; enable = true;
zone_name = tldFromFqdn fqdn;
aRecords = lib.lists.map (d: { aRecords = lib.lists.map (d: {
fqdn = d; fqdn = d;
content = config.khscodes.hcloud.output.server.compute.ipv4_address; content = config.khscodes.hcloud.output.server.compute.ipv4_address;

View file

@ -103,6 +103,18 @@ in
default = false; default = false;
}; };
}; };
network = {
router = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = "monitoring.kaareskovgaard.net";
description = "Set to null to create new router. Will make routing to monitoring instance unroutable over ipv6 (which is the only one that has ipv6 record)";
};
ipv4Cidr = lib.mkOption {
type = lib.types.str;
default = "172.24.0.0/24";
description = "Set to unique cidr for non monitoring instances";
};
};
extraFirewallRules = lib.mkOption { extraFirewallRules = lib.mkOption {
type = lib.types.listOf lib.types.attrs; type = lib.types.listOf lib.types.attrs;
description = "Extra firewall rules added to the instance"; description = "Extra firewall rules added to the instance";
@ -136,6 +148,8 @@ in
ssh_public_key = cfg.ssh_key; ssh_public_key = cfg.ssh_key;
firewall_rules = firewallRules; firewall_rules = firewallRules;
user_data = builtins.toJSON provisioningUserData; user_data = builtins.toJSON provisioningUserData;
ip4_cidr = cfg.network.ipv4Cidr;
router = cfg.network.router;
}; };
khscodes.unifi.enable = true; khscodes.unifi.enable = true;
khscodes.unifi.static_route.compute = { khscodes.unifi.static_route.compute = {
@ -148,7 +162,6 @@ in
enable = true; enable = true;
dns = { dns = {
enable = true; enable = true;
zone_name = tldFromFqdn fqdn;
aRecords = lib.mkIf cfg.dns.mapIpv4Address ( aRecords = lib.mkIf cfg.dns.mapIpv4Address (
lib.lists.map (d: { lib.lists.map (d: {
fqdn = d; fqdn = d;

View file

@ -105,10 +105,21 @@ let
description = "Extra configuration to inject into the generated nginx config"; description = "Extra configuration to inject into the generated nginx config";
default = ''''; default = '''';
}; };
rateLimit.enable = lib.mkOption { rateLimit = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable rate limiting";
};
burst = lib.mkOption {
type = lib.types.int;
default = 20;
};
};
fail2ban.enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = true; default = true;
description = "Enable rate limiting"; description = "Enable fail2ban rate limiting";
}; };
locations = lib.mkOption { locations = lib.mkOption {
type = lib.types.attrsOf ( type = lib.types.attrsOf (
@ -211,6 +222,10 @@ in
}; };
groups.${config.services.prometheus.exporters.nginxlog.user} = { }; groups.${config.services.prometheus.exporters.nginxlog.user} = { };
}; };
systemd.services.fail2ban = {
# fail2ban fails starting if the nginx log files don't exist
after = [ "nginx.service" ];
};
services.fail2ban.jails = { services.fail2ban.jails = {
nginx-botsearch = { nginx-botsearch = {
settings = { settings = {
@ -271,14 +286,9 @@ in
commonHttpConfig = '' commonHttpConfig = ''
${logfmt} ${logfmt}
access_log /var/log/nginx/access.logfmt.log logfmt; access_log /var/log/nginx/access.logfmt.log logfmt;
log_format fail2ban '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
log_format nginx_exporter '$remote_addr - $remote_user [$time_local] "$request" ' log_format nginx_exporter '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" ' '$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" $upstream_response_time'; '"$http_user_agent" "$http_x_forwarded_for" $upstream_response_time';
access_log /var/log/nginx/access.fail2ban.log fail2ban;
''; '';
appendHttpConfig = '' appendHttpConfig = ''
limit_req_zone $binary_remote_addr zone=nobots:10m rate=20r/s; limit_req_zone $binary_remote_addr zone=nobots:10m rate=20r/s;
@ -314,12 +324,11 @@ in
else else
''''; '''';
reqLimit = lib.strings.optionalString value.rateLimit.enable '' reqLimit = lib.strings.optionalString value.rateLimit.enable ''
limit_req zone=nobots burst=20 nodelay; limit_req zone=nobots burst=${toString value.rateLimit.burst} nodelay;
''; '';
extraConfig = '' extraConfig = ''
${mtls} ${mtls}
${reqLimit} ${reqLimit}
access_log /var/log/nginx/access.fail2ban.log fail2ban;
access_log /var/log/nginx/access.logfmt.log logfmt; access_log /var/log/nginx/access.logfmt.log logfmt;
access_log /var/log/nginx/access.${name}.log nginx_exporter; access_log /var/log/nginx/access.${name}.log nginx_exporter;
${value.extraConfig} ${value.extraConfig}

View file

@ -12,6 +12,15 @@ let
"@" "@"
else else
fqdn; fqdn;
tldFromFqdn =
fqdn:
let
split = lib.strings.splitString "." fqdn;
in
if lib.lists.length split < 3 then
fqdn
else
lib.strings.removePrefix "${builtins.head split}." fqdn;
dnsARecordModule = lib.khscodes.mkSubmodule { dnsARecordModule = lib.khscodes.mkSubmodule {
description = "Module for defining dns A/AAAA record"; description = "Module for defining dns A/AAAA record";
options = { options = {
@ -77,14 +86,11 @@ let
}; };
in in
{ {
imports = [ ./dns_zone.nix ];
options.khscodes.cloudflare = { options.khscodes.cloudflare = {
enable = lib.mkEnableOption "Enables khscodes cloudflare terranix integration"; enable = lib.mkEnableOption "Enables khscodes cloudflare terranix integration";
dns = { dns = {
enable = lib.mkEnableOption "Enables setting up DNS records"; enable = lib.mkEnableOption "Enables setting up DNS records";
zone_name = lib.mkOption {
type = lib.types.str;
description = "The dns zone name (TLD)";
};
aRecords = lib.mkOption { aRecords = lib.mkOption {
type = lib.types.listOf dnsARecordModule; type = lib.types.listOf dnsARecordModule;
default = [ ]; default = [ ];
@ -119,51 +125,77 @@ in
version = "~> 4.0"; version = "~> 4.0";
}; };
data.cloudflare_zone.dns_zone = lib.attrsets.optionalAttrs cfg.dns.enable { khscodes.cloudflare.data.dns_zones = lib.lists.unique (
name = cfg.dns.zone_name; lib.lists.map (a: tldFromFqdn (a.fqdn)) (
}; cfg.dns.aRecords ++ cfg.dns.aaaaRecords ++ cfg.dns.txtRecords ++ cfg.dns.mxRecords
)
);
resource.cloudflare_record = lib.attrsets.optionalAttrs cfg.dns.enable ( resource.cloudflare_record = lib.attrsets.optionalAttrs cfg.dns.enable (
lib.listToAttrs ( lib.listToAttrs (
(lib.lists.map (record: { (lib.lists.map (
name = "${lib.khscodes.sanitize-terraform-name record.fqdn}_a"; record:
value = { let
inherit (record) content ttl proxied; zoneName = tldFromFqdn record.fqdn;
name = nameFromFQDNAndZone record.fqdn cfg.dns.zone_name; in
type = "A"; {
zone_id = "\${ data.cloudflare_zone.dns_zone.id }"; name = "${lib.khscodes.sanitize-terraform-name record.fqdn}_a";
comment = "app=${cfg.dns.zone_name}"; value = {
}; inherit (record) content ttl proxied;
}) cfg.dns.aRecords) name = nameFromFQDNAndZone record.fqdn zoneName;
++ (lib.lists.map (record: { type = "A";
name = "${lib.khscodes.sanitize-terraform-name record.fqdn}_aaaa"; zone_id = "\${ data.cloudflare_zone.${lib.khscodes.sanitize-terraform-name zoneName}.id }";
value = { comment = "app=${zoneName}";
inherit (record) content ttl proxied; };
name = nameFromFQDNAndZone record.fqdn cfg.dns.zone_name; }
type = "AAAA"; ) cfg.dns.aRecords)
zone_id = "\${ data.cloudflare_zone.dns_zone.id }"; ++ (lib.lists.map (
comment = "app=${cfg.dns.zone_name}"; record:
}; let
}) cfg.dns.aaaaRecords) zoneName = tldFromFqdn record.fqdn;
++ (lib.lists.map (record: { in
name = "${lib.khscodes.sanitize-terraform-name record.fqdn}_txt"; {
value = { name = "${lib.khscodes.sanitize-terraform-name record.fqdn}_aaaa";
inherit (record) content ttl; value = {
name = nameFromFQDNAndZone record.fqdn cfg.dns.zone_name; inherit (record) content ttl proxied;
type = "TXT"; name = nameFromFQDNAndZone record.fqdn zoneName;
zone_id = "\${ data.cloudflare_zone.dns_zone.id }"; type = "AAAA";
comment = "app=${cfg.dns.zone_name}"; zone_id = "\${ data.cloudflare_zone.${lib.khscodes.sanitize-terraform-name zoneName}.id }";
}; comment = "app=${zoneName}";
}) cfg.dns.txtRecords) };
++ (lib.lists.map (record: { }
name = "${lib.khscodes.sanitize-terraform-name record.fqdn}_mx"; ) cfg.dns.aaaaRecords)
value = { ++ (lib.lists.map (
inherit (record) content priority; record:
name = nameFromFQDNAndZone record.fqdn cfg.dns.zone_name; let
type = "MX"; zoneName = tldFromFqdn record.fqdn;
zone_id = "\${ data.cloudflare_zone.dns_zone.id }"; in
comment = "app=${cfg.dns.zone_name}"; {
}; name = "${lib.khscodes.sanitize-terraform-name record.fqdn}_txt";
}) cfg.dns.mxRecords) value = {
inherit (record) content ttl;
name = nameFromFQDNAndZone record.fqdn zoneName;
type = "TXT";
zone_id = "\${ data.cloudflare_zone.${lib.khscodes.sanitize-terraform-name zoneName}.id }";
comment = "app=${zoneName}";
};
}
) cfg.dns.txtRecords)
++ (lib.lists.map (
record:
let
zoneName = tldFromFqdn record.fqdn;
in
{
name = "${lib.khscodes.sanitize-terraform-name record.fqdn}_mx";
value = {
inherit (record) content priority;
name = nameFromFQDNAndZone record.fqdn zoneName;
type = "MX";
zone_id = "\${ data.cloudflare_zone.${lib.khscodes.sanitize-terraform-name zoneName}.id }";
comment = "app=${zoneName}";
};
}
) cfg.dns.mxRecords)
) )
); );
}; };

View file

@ -0,0 +1,22 @@
{ config, lib, ... }:
let
cfg = config.khscodes.cloudflare;
in
{
options.khscodes.cloudflare = {
data.dns_zones = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
};
};
config = lib.mkIf cfg.enable {
data.cloudflare_zone = lib.listToAttrs (
lib.lists.map (zone: {
name = lib.khscodes.sanitize-terraform-name zone;
value = {
name = zone;
};
}) (lib.lists.unique cfg.data.dns_zones)
);
};
}

View file

@ -74,7 +74,7 @@ let
}; };
ip4_cidr = lib.mkOption { ip4_cidr = lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "IPv4 cidr of the private virtual network"; description = "IPv4 cidr of the private virtual network. Must override when using a named router";
default = "172.24.0.0/24"; default = "172.24.0.0/24";
}; };
ip4_dns_nameservers = lib.mkOption { ip4_dns_nameservers = lib.mkOption {
@ -111,6 +111,11 @@ let
"2606:4700:4700::1001" "2606:4700:4700::1001"
]; ];
}; };
router = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = "monitoring.kaareskovgaard.net";
description = "Name of the router to attach to. If null will create a new router";
};
tags = lib.mkOption { tags = lib.mkOption {
type = lib.types.listOf lib.types.str; type = lib.types.listOf lib.types.str;
}; };
@ -226,20 +231,45 @@ in
) cfg.compute_instance; ) cfg.compute_instance;
# router # router
resource.openstack_networking_router_v2 = lib.mapAttrs' ( resource.openstack_networking_router_v2 = lib.filterAttrs (name: value: value != null) (
name: value: lib.mapAttrs' (
let name: value:
sanitizedName = lib.khscodes.sanitize-terraform-name name; let
in sanitizedName = lib.khscodes.sanitize-terraform-name name;
{ in
name = sanitizedName; {
value = { name = sanitizedName;
name = value.name; value =
external_network_id = "\${ data.openstack_networking_network_v2.provider.id }"; if value.router == null then
tags = value.tags; {
}; name = value.name;
} external_network_id = "\${ data.openstack_networking_network_v2.provider.id }";
) cfg.compute_instance; tags = value.tags;
}
else
null;
}
) cfg.compute_instance
);
data.openstack_networking_router_v2 = lib.filterAttrs (name: value: value != null) (
lib.mapAttrs' (
name: value:
let
sanitizedName = lib.khscodes.sanitize-terraform-name name;
in
{
name = sanitizedName;
value =
if value.router != null then
{
name = value.router;
}
else
null;
}
) cfg.compute_instance
);
# network # network
resource.openstack_networking_network_v2 = lib.mapAttrs' ( resource.openstack_networking_network_v2 = lib.mapAttrs' (
@ -305,7 +335,9 @@ in
{ {
name = "${sanitizedName}_ip4"; name = "${sanitizedName}_ip4";
value = { value = {
router_id = "\${ openstack_networking_router_v2.${sanitizedName}.id }"; router_id = "\${ ${
lib.strings.optionalString (value.router != null) "data."
} openstack_networking_router_v2.${sanitizedName}.id }";
subnet_id = "\${ openstack_networking_subnet_v2.${sanitizedName}_ip4.id }"; subnet_id = "\${ openstack_networking_subnet_v2.${sanitizedName}_ip4.id }";
}; };
} }
@ -318,7 +350,9 @@ in
{ {
name = "${sanitizedName}_ip6"; name = "${sanitizedName}_ip6";
value = { value = {
router_id = "\${ openstack_networking_router_v2.${sanitizedName}.id }"; router_id = "\${ ${
lib.strings.optionalString (value.router != null) "data."
}openstack_networking_router_v2.${sanitizedName}.id }";
subnet_id = "\${ openstack_networking_subnet_v2.${sanitizedName}_ip6.id }"; subnet_id = "\${ openstack_networking_subnet_v2.${sanitizedName}_ip6.id }";
}; };
} }

View file

@ -45,8 +45,10 @@ in
{ {
id = "\${ openstack_compute_instance_v2.${sanitizedName}.id }"; id = "\${ openstack_compute_instance_v2.${sanitizedName}.id }";
ipv4_address = "\${ openstack_networking_floatingip_v2.${sanitizedName}.address }"; ipv4_address = "\${ openstack_networking_floatingip_v2.${sanitizedName}.address }";
ipv6_address = "\${ data.openstack_networking_port_v2.${sanitizedName}.all_fixed_ips[1] }"; ipv6_address = "\${ [for ip in data.openstack_networking_port_v2.${sanitizedName}.all_fixed_ips : ip if replace(ip, \":\", \"\") != ip][0] }";
ipv6_external_gateway = "\${ [for ip in openstack_networking_router_v2.${sanitizedName}.external_fixed_ip : ip.ip_address if replace(ip.ip_address, \":\", \"\") != ip.ip_address][0] }"; ipv6_external_gateway = "\${ [for ip in ${
lib.strings.optionalString (value.router != null) "data."
}openstack_networking_router_v2.${sanitizedName}.external_fixed_ip : ip.ip_address if replace(ip.ip_address, \":\", \"\") != ip.ip_address][0] }";
ipv6_cidr = "\${ openstack_networking_subnet_v2.${sanitizedName}_ip6.cidr }"; ipv6_cidr = "\${ openstack_networking_subnet_v2.${sanitizedName}_ip6.cidr }";
} }
) )

View file

@ -77,7 +77,7 @@ in
resource.cloudflare_record.dkim_rsa = { resource.cloudflare_record.dkim_rsa = {
name = "snm_rsa._domainkey"; name = "snm_rsa._domainkey";
zone_id = "\${ data.cloudflare_zone.dns_zone.id }"; zone_id = "\${ data.cloudflare_zone.kas_codes.id }";
type = "TXT"; type = "TXT";
content = ''"v=DKIM1;k=rsa;p=${dkimPublicKey "tls_private_key.dkim_rsa"}"''; content = ''"v=DKIM1;k=rsa;p=${dkimPublicKey "tls_private_key.dkim_rsa"}"'';
comment = "app=kas.codes"; comment = "app=kas.codes";
@ -86,7 +86,7 @@ in
resource.cloudflare_record.dkim_ed25519 = { resource.cloudflare_record.dkim_ed25519 = {
name = "snm_ed25519._domainkey"; name = "snm_ed25519._domainkey";
zone_id = "\${ data.cloudflare_zone.dns_zone.id }"; zone_id = "\${ data.cloudflare_zone.kas_codes.id }";
type = "TXT"; type = "TXT";
content = ''"v=DKIM1;k=ed25519;p=${dkimPublicKey "tls_private_key.dkim_ed25519"}"''; content = ''"v=DKIM1;k=ed25519;p=${dkimPublicKey "tls_private_key.dkim_ed25519"}"'';
comment = "app=kas.codes"; comment = "app=kas.codes";
@ -95,7 +95,7 @@ in
resource.cloudflare_record.spf = { resource.cloudflare_record.spf = {
name = "@"; name = "@";
zone_id = "\${ data.cloudflare_zone.dns_zone.id }"; zone_id = "\${ data.cloudflare_zone.kas_codes.id }";
type = "TXT"; type = "TXT";
content = ''"v=spf1 ip4:${config.khscodes.hcloud.output.server.compute.ipv4_address} ip6:${config.khscodes.hcloud.output.server.compute.ipv6_address} -all"''; content = ''"v=spf1 ip4:${config.khscodes.hcloud.output.server.compute.ipv4_address} ip6:${config.khscodes.hcloud.output.server.compute.ipv6_address} -all"'';
comment = "app=kas.codes"; comment = "app=kas.codes";
@ -103,7 +103,7 @@ in
}; };
resource.cloudflare_record.dmarc = { resource.cloudflare_record.dmarc = {
name = "_dmarc"; name = "_dmarc";
zone_id = "\${ data.cloudflare_zone.dns_zone.id }"; zone_id = "\${ data.cloudflare_zone.kas_codes.id }";
type = "TXT"; type = "TXT";
content = ''"v=DMARC1; p=reject; adkim=s; aspf=s;"''; content = ''"v=DMARC1; p=reject; adkim=s; aspf=s;"'';
comment = "app=kas.codes"; comment = "app=kas.codes";

View file

@ -0,0 +1,16 @@
{
inputs,
...
}:
{
imports = [
"${inputs.self}/nix/profiles/nixos/hetzner-server.nix"
];
khscodes.infrastructure.hetzner-instance = {
enable = true;
mapRdns = true;
server_type = "cax11";
};
khscodes.networking.fqdn = "mail.kaareskovgaard.net";
system.stateVersion = "25.05";
}

View file

@ -7,10 +7,16 @@
let let
domain = "login.kaareskovgaard.net"; domain = "login.kaareskovgaard.net";
bootstrapping = config.khscodes."security.kaareskovgaard.net".bootstrap.enable; bootstrapping = config.khscodes."security.kaareskovgaard.net".bootstrap.enable;
openbaoAppBasicSecretFile = "/var/lib/vault-agent/kanidm/openbao_basic_secret"; openbaoAppBasicSecretFile = "/run/kanidm/openbao_basic_secret";
openbaoCliAppBasicSecretFile = "/var/lib/vault-agent/kanidm/openbao_cli_basic_secret"; openbaoCliAppBasicSecretFile = "/run/kanidm/openbao_cli_basic_secret";
monitoringAppBasicSecretFile = "/var/lib/vault-agent/kanidm/monitoring_basic_secret"; monitoringAppBasicSecretFile = "/run/kanidm/monitoring_basic_secret";
forgejoAppBasicSecretFile = "/var/lib/vault-agent/kanidm/forgejo_basic_secret"; forgejoAppBasicSecretFile = "/run/kanidm/forgejo_basic_secret";
secretFiles = [
openbaoAppBasicSecretFile
openbaoCliAppBasicSecretFile
monitoringAppBasicSecretFile
forgejoAppBasicSecretFile
];
openbaoDomain = config.khscodes.infrastructure.openbao.domain; openbaoDomain = config.khscodes.infrastructure.openbao.domain;
openbaoAllowedRedirectUrls = [ openbaoAllowedRedirectUrls = [
"https://${openbaoDomain}/ui/vault/auth/kanidm/oidc/callback" "https://${openbaoDomain}/ui/vault/auth/kanidm/oidc/callback"
@ -183,9 +189,7 @@ in
# Don't add dependencies from bootstrapping when not bootstrapping. # Don't add dependencies from bootstrapping when not bootstrapping.
systemd.services.kanidm = lib.mkIf (!bootstrapping) { systemd.services.kanidm = lib.mkIf (!bootstrapping) {
unitConfig = { unitConfig = {
ConditionPathExists = [ ConditionPathExists = secretFiles;
openbaoAppBasicSecretFile
];
}; };
}; };

View file

@ -0,0 +1,79 @@
{ inputs, config, ... }:
let
mattermost = config.services.mattermost;
in
{
imports = [
"${inputs.self}/nix/profiles/nixos/khs-openstack-server.nix"
];
services.postgresql = {
enable = true;
ensureDatabases = [ "mattermost" ];
ensureUsers = [
{
name = "mattermost";
ensureDBOwnership = true;
}
];
};
services.mattermost = {
enable = true;
siteName = "chat.kaareskovgaard.net";
siteUrl = "https://chat.kaareskovgaard.net";
database = {
create = false;
};
telemetry = {
enableSecurityAlerts = false;
enableDiagnostics = false;
};
settings = {
EmailSettings = {
EnableSignUpWithEmail = false;
EnableSignInWithEmail = true;
EnableSignInWithUsername = false;
};
};
};
khscodes = {
infrastructure.khs-openstack-instance = {
enable = true;
flavor = "m.small";
network = {
ipv4Cidr = "172.24.1.0/24";
};
};
services.nginx = {
enable = true;
virtualHosts."chat.kaareskovgaard.net" = {
rateLimit.burst = 100;
locations."/" = {
proxyPass = "http://127.0.0.1:${toString mattermost.port}";
proxyWebsockets = true;
recommendedProxySettings = true;
};
};
};
services.vault-agent.templates = [
{
contents = ''
{{- with secret "kanidm/data/apps/mattermost" -}}
MM_OPENIDSETTINGS_SECRET={{ .Data.data.basic_secret }}
{{- end -}}
'';
destination = "/run/mattermost/env";
owner = "mattermost";
group = "mattermost";
perms = "0600";
reloadOrRestartUnits = [ "mattermost.service" ];
}
];
infrastructure.vault-server-approle.policy = {
"kanidm/data/apps/mattermost" = {
capabilities = [ "read" ];
};
};
};
khscodes.networking.fqdn = "chat.kaareskovgaard.net";
system.stateVersion = "25.05";
}

View file

@ -190,6 +190,9 @@ in
infrastructure.khs-openstack-instance = { infrastructure.khs-openstack-instance = {
enable = true; enable = true;
flavor = "m.large"; flavor = "m.large";
network = {
router = null;
};
}; };
services.nginx = { services.nginx = {
enable = true; enable = true;