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;
dns = {
enable = true;
zone_name = tldFromFqdn fqdn;
aRecords = lib.lists.map (d: {
fqdn = d;
content = config.khscodes.hcloud.output.server.compute.ipv4_address;

View file

@ -103,6 +103,18 @@ in
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 {
type = lib.types.listOf lib.types.attrs;
description = "Extra firewall rules added to the instance";
@ -136,6 +148,8 @@ in
ssh_public_key = cfg.ssh_key;
firewall_rules = firewallRules;
user_data = builtins.toJSON provisioningUserData;
ip4_cidr = cfg.network.ipv4Cidr;
router = cfg.network.router;
};
khscodes.unifi.enable = true;
khscodes.unifi.static_route.compute = {
@ -148,7 +162,6 @@ in
enable = true;
dns = {
enable = true;
zone_name = tldFromFqdn fqdn;
aRecords = lib.mkIf cfg.dns.mapIpv4Address (
lib.lists.map (d: {
fqdn = d;

View file

@ -105,10 +105,21 @@ let
description = "Extra configuration to inject into the generated nginx config";
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;
default = true;
description = "Enable rate limiting";
description = "Enable fail2ban rate limiting";
};
locations = lib.mkOption {
type = lib.types.attrsOf (
@ -211,6 +222,10 @@ in
};
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 = {
nginx-botsearch = {
settings = {
@ -271,14 +286,9 @@ in
commonHttpConfig = ''
${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" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" $upstream_response_time';
access_log /var/log/nginx/access.fail2ban.log fail2ban;
'';
appendHttpConfig = ''
limit_req_zone $binary_remote_addr zone=nobots:10m rate=20r/s;
@ -314,12 +324,11 @@ in
else
'''';
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 = ''
${mtls}
${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.${name}.log nginx_exporter;
${value.extraConfig}

View file

@ -12,6 +12,15 @@ let
"@"
else
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 {
description = "Module for defining dns A/AAAA record";
options = {
@ -77,14 +86,11 @@ let
};
in
{
imports = [ ./dns_zone.nix ];
options.khscodes.cloudflare = {
enable = lib.mkEnableOption "Enables khscodes cloudflare terranix integration";
dns = {
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 {
type = lib.types.listOf dnsARecordModule;
default = [ ];
@ -119,51 +125,77 @@ in
version = "~> 4.0";
};
data.cloudflare_zone.dns_zone = lib.attrsets.optionalAttrs cfg.dns.enable {
name = cfg.dns.zone_name;
};
khscodes.cloudflare.data.dns_zones = lib.lists.unique (
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 (
lib.listToAttrs (
(lib.lists.map (record: {
name = "${lib.khscodes.sanitize-terraform-name record.fqdn}_a";
value = {
inherit (record) content ttl proxied;
name = nameFromFQDNAndZone record.fqdn cfg.dns.zone_name;
type = "A";
zone_id = "\${ data.cloudflare_zone.dns_zone.id }";
comment = "app=${cfg.dns.zone_name}";
};
}) cfg.dns.aRecords)
++ (lib.lists.map (record: {
name = "${lib.khscodes.sanitize-terraform-name record.fqdn}_aaaa";
value = {
inherit (record) content ttl proxied;
name = nameFromFQDNAndZone record.fqdn cfg.dns.zone_name;
type = "AAAA";
zone_id = "\${ data.cloudflare_zone.dns_zone.id }";
comment = "app=${cfg.dns.zone_name}";
};
}) cfg.dns.aaaaRecords)
++ (lib.lists.map (record: {
name = "${lib.khscodes.sanitize-terraform-name record.fqdn}_txt";
value = {
inherit (record) content ttl;
name = nameFromFQDNAndZone record.fqdn cfg.dns.zone_name;
type = "TXT";
zone_id = "\${ data.cloudflare_zone.dns_zone.id }";
comment = "app=${cfg.dns.zone_name}";
};
}) cfg.dns.txtRecords)
++ (lib.lists.map (record: {
name = "${lib.khscodes.sanitize-terraform-name record.fqdn}_mx";
value = {
inherit (record) content priority;
name = nameFromFQDNAndZone record.fqdn cfg.dns.zone_name;
type = "MX";
zone_id = "\${ data.cloudflare_zone.dns_zone.id }";
comment = "app=${cfg.dns.zone_name}";
};
}) cfg.dns.mxRecords)
(lib.lists.map (
record:
let
zoneName = tldFromFqdn record.fqdn;
in
{
name = "${lib.khscodes.sanitize-terraform-name record.fqdn}_a";
value = {
inherit (record) content ttl proxied;
name = nameFromFQDNAndZone record.fqdn zoneName;
type = "A";
zone_id = "\${ data.cloudflare_zone.${lib.khscodes.sanitize-terraform-name zoneName}.id }";
comment = "app=${zoneName}";
};
}
) cfg.dns.aRecords)
++ (lib.lists.map (
record:
let
zoneName = tldFromFqdn record.fqdn;
in
{
name = "${lib.khscodes.sanitize-terraform-name record.fqdn}_aaaa";
value = {
inherit (record) content ttl proxied;
name = nameFromFQDNAndZone record.fqdn zoneName;
type = "AAAA";
zone_id = "\${ data.cloudflare_zone.${lib.khscodes.sanitize-terraform-name zoneName}.id }";
comment = "app=${zoneName}";
};
}
) cfg.dns.aaaaRecords)
++ (lib.lists.map (
record:
let
zoneName = tldFromFqdn record.fqdn;
in
{
name = "${lib.khscodes.sanitize-terraform-name record.fqdn}_txt";
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 {
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";
};
ip4_dns_nameservers = lib.mkOption {
@ -111,6 +111,11 @@ let
"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 {
type = lib.types.listOf lib.types.str;
};
@ -226,20 +231,45 @@ in
) cfg.compute_instance;
# router
resource.openstack_networking_router_v2 = lib.mapAttrs' (
name: value:
let
sanitizedName = lib.khscodes.sanitize-terraform-name name;
in
{
name = sanitizedName;
value = {
name = value.name;
external_network_id = "\${ data.openstack_networking_network_v2.provider.id }";
tags = value.tags;
};
}
) cfg.compute_instance;
resource.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.name;
external_network_id = "\${ data.openstack_networking_network_v2.provider.id }";
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
resource.openstack_networking_network_v2 = lib.mapAttrs' (
@ -305,7 +335,9 @@ in
{
name = "${sanitizedName}_ip4";
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 }";
};
}
@ -318,7 +350,9 @@ in
{
name = "${sanitizedName}_ip6";
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 }";
};
}

View file

@ -45,8 +45,10 @@ in
{
id = "\${ openstack_compute_instance_v2.${sanitizedName}.id }";
ipv4_address = "\${ openstack_networking_floatingip_v2.${sanitizedName}.address }";
ipv6_address = "\${ data.openstack_networking_port_v2.${sanitizedName}.all_fixed_ips[1] }";
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_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 ${
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 }";
}
)

View file

@ -77,7 +77,7 @@ in
resource.cloudflare_record.dkim_rsa = {
name = "snm_rsa._domainkey";
zone_id = "\${ data.cloudflare_zone.dns_zone.id }";
zone_id = "\${ data.cloudflare_zone.kas_codes.id }";
type = "TXT";
content = ''"v=DKIM1;k=rsa;p=${dkimPublicKey "tls_private_key.dkim_rsa"}"'';
comment = "app=kas.codes";
@ -86,7 +86,7 @@ in
resource.cloudflare_record.dkim_ed25519 = {
name = "snm_ed25519._domainkey";
zone_id = "\${ data.cloudflare_zone.dns_zone.id }";
zone_id = "\${ data.cloudflare_zone.kas_codes.id }";
type = "TXT";
content = ''"v=DKIM1;k=ed25519;p=${dkimPublicKey "tls_private_key.dkim_ed25519"}"'';
comment = "app=kas.codes";
@ -95,7 +95,7 @@ in
resource.cloudflare_record.spf = {
name = "@";
zone_id = "\${ data.cloudflare_zone.dns_zone.id }";
zone_id = "\${ data.cloudflare_zone.kas_codes.id }";
type = "TXT";
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";
@ -103,7 +103,7 @@ in
};
resource.cloudflare_record.dmarc = {
name = "_dmarc";
zone_id = "\${ data.cloudflare_zone.dns_zone.id }";
zone_id = "\${ data.cloudflare_zone.kas_codes.id }";
type = "TXT";
content = ''"v=DMARC1; p=reject; adkim=s; aspf=s;"'';
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
domain = "login.kaareskovgaard.net";
bootstrapping = config.khscodes."security.kaareskovgaard.net".bootstrap.enable;
openbaoAppBasicSecretFile = "/var/lib/vault-agent/kanidm/openbao_basic_secret";
openbaoCliAppBasicSecretFile = "/var/lib/vault-agent/kanidm/openbao_cli_basic_secret";
monitoringAppBasicSecretFile = "/var/lib/vault-agent/kanidm/monitoring_basic_secret";
forgejoAppBasicSecretFile = "/var/lib/vault-agent/kanidm/forgejo_basic_secret";
openbaoAppBasicSecretFile = "/run/kanidm/openbao_basic_secret";
openbaoCliAppBasicSecretFile = "/run/kanidm/openbao_cli_basic_secret";
monitoringAppBasicSecretFile = "/run/kanidm/monitoring_basic_secret";
forgejoAppBasicSecretFile = "/run/kanidm/forgejo_basic_secret";
secretFiles = [
openbaoAppBasicSecretFile
openbaoCliAppBasicSecretFile
monitoringAppBasicSecretFile
forgejoAppBasicSecretFile
];
openbaoDomain = config.khscodes.infrastructure.openbao.domain;
openbaoAllowedRedirectUrls = [
"https://${openbaoDomain}/ui/vault/auth/kanidm/oidc/callback"
@ -183,9 +189,7 @@ in
# Don't add dependencies from bootstrapping when not bootstrapping.
systemd.services.kanidm = lib.mkIf (!bootstrapping) {
unitConfig = {
ConditionPathExists = [
openbaoAppBasicSecretFile
];
ConditionPathExists = secretFiles;
};
};

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 = {
enable = true;
flavor = "m.large";
network = {
router = null;
};
};
services.nginx = {
enable = true;