diff --git a/nix/modules/nixos/infrastructure/hetzner-instance/default.nix b/nix/modules/nixos/infrastructure/hetzner-instance/default.nix index c9d9546..dc3d2b6 100644 --- a/nix/modules/nixos/infrastructure/hetzner-instance/default.nix +++ b/nix/modules/nixos/infrastructure/hetzner-instance/default.nix @@ -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; diff --git a/nix/modules/nixos/infrastructure/khs-openstack-instance/default.nix b/nix/modules/nixos/infrastructure/khs-openstack-instance/default.nix index 5f81a88..b0e7152 100644 --- a/nix/modules/nixos/infrastructure/khs-openstack-instance/default.nix +++ b/nix/modules/nixos/infrastructure/khs-openstack-instance/default.nix @@ -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; diff --git a/nix/modules/nixos/services/nginx/default.nix b/nix/modules/nixos/services/nginx/default.nix index 6d4607c..99eabdb 100644 --- a/nix/modules/nixos/services/nginx/default.nix +++ b/nix/modules/nixos/services/nginx/default.nix @@ -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} diff --git a/nix/modules/terranix/cloudflare/default.nix b/nix/modules/terranix/cloudflare/default.nix index 54fbf96..625b115 100644 --- a/nix/modules/terranix/cloudflare/default.nix +++ b/nix/modules/terranix/cloudflare/default.nix @@ -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) ) ); }; diff --git a/nix/modules/terranix/cloudflare/dns_zone.nix b/nix/modules/terranix/cloudflare/dns_zone.nix new file mode 100644 index 0000000..f9915a7 --- /dev/null +++ b/nix/modules/terranix/cloudflare/dns_zone.nix @@ -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) + ); + }; +} diff --git a/nix/modules/terranix/openstack/default.nix b/nix/modules/terranix/openstack/default.nix index 0941c2f..c38acd6 100644 --- a/nix/modules/terranix/openstack/default.nix +++ b/nix/modules/terranix/openstack/default.nix @@ -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 }"; }; } diff --git a/nix/modules/terranix/openstack/output.nix b/nix/modules/terranix/openstack/output.nix index b1aedeb..34f3c4c 100644 --- a/nix/modules/terranix/openstack/output.nix +++ b/nix/modules/terranix/openstack/output.nix @@ -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 }"; } ) diff --git a/nix/systems/aarch64-linux/kas.codes/mailserver/dkim.nix b/nix/systems/aarch64-linux/kas.codes/mailserver/dkim.nix index f15022d..13b0301 100644 --- a/nix/systems/aarch64-linux/kas.codes/mailserver/dkim.nix +++ b/nix/systems/aarch64-linux/kas.codes/mailserver/dkim.nix @@ -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"; diff --git a/nix/systems/aarch64-linux/mail.kaareskovgaard.net/default.nix b/nix/systems/aarch64-linux/mail.kaareskovgaard.net/default.nix new file mode 100644 index 0000000..ad5f185 --- /dev/null +++ b/nix/systems/aarch64-linux/mail.kaareskovgaard.net/default.nix @@ -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"; +} diff --git a/nix/systems/aarch64-linux/security.kaareskovgaard.net/kanidm.nix b/nix/systems/aarch64-linux/security.kaareskovgaard.net/kanidm.nix index b04983e..2453b77 100644 --- a/nix/systems/aarch64-linux/security.kaareskovgaard.net/kanidm.nix +++ b/nix/systems/aarch64-linux/security.kaareskovgaard.net/kanidm.nix @@ -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; }; }; diff --git a/nix/systems/x86_64-linux/chat.kaareskovgaard.net/default.nix b/nix/systems/x86_64-linux/chat.kaareskovgaard.net/default.nix new file mode 100644 index 0000000..efee7ca --- /dev/null +++ b/nix/systems/x86_64-linux/chat.kaareskovgaard.net/default.nix @@ -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"; +} diff --git a/nix/systems/x86_64-linux/monitoring.kaareskovgaard.net/default.nix b/nix/systems/x86_64-linux/monitoring.kaareskovgaard.net/default.nix index 8ef1e17..91cda8c 100644 --- a/nix/systems/x86_64-linux/monitoring.kaareskovgaard.net/default.nix +++ b/nix/systems/x86_64-linux/monitoring.kaareskovgaard.net/default.nix @@ -190,6 +190,9 @@ in infrastructure.khs-openstack-instance = { enable = true; flavor = "m.large"; + network = { + router = null; + }; }; services.nginx = { enable = true;