setup octodns for automatic dns records

This commit is contained in:
Joakim Repomaa
2025-02-15 03:22:20 +02:00
parent 459cae639d
commit c15d518e4a
14 changed files with 351 additions and 92 deletions

View File

@@ -3,7 +3,10 @@
# and in the NixOS manual (accessible by running `nixos-help`). # and in the NixOS manual (accessible by running `nixos-help`).
{ config, pkgs, ssh, ... }: { config, pkgs, ssh, ... }:
let
ipv4Address = "65.21.145.150";
ipv6Address = "2a01:4f9:c011:9ac1::1";
in
{ {
nix = { nix = {
settings = { settings = {
@@ -25,13 +28,24 @@
networking.useDHCP = false; networking.useDHCP = false;
networking.nftables.enable = true; networking.nftables.enable = true;
services.octodns.records."" = {
A = {
ttl = 86400;
values = [ ipv4Address ];
};
AAAA = {
ttl = 86400;
values = [ ipv6Address ];
};
};
systemd.network = { systemd.network = {
enable = true; enable = true;
networks.static = { networks.static = {
name = "enp1s0"; name = "enp1s0";
address = [ address = [
"65.21.145.150/32" "${ipv4Address}/32"
"2a01:4f9:c011:9ac1::1/64" "${ipv6Address}/64"
]; ];
routes = [ routes = [
{ Gateway = "fe80::1"; } { Gateway = "fe80::1"; }

View File

@@ -11,6 +11,7 @@
"vaultwarden" "vaultwarden"
"donetick" "donetick"
"dnote" "dnote"
"octodns"
] ]
) // { ) // {
smtp-password = { smtp-password = {

View File

@@ -12,7 +12,6 @@ let
secrets = config.age.secrets; secrets = config.age.secrets;
in in
{ {
services.postgresql.package = pkgs.postgresql_16;
virtualisation.podman.enable = true; virtualisation.podman.enable = true;
virtualisation.oci-containers.backend = "podman"; virtualisation.oci-containers.backend = "podman";
@@ -33,6 +32,16 @@ in
}; };
services = { services = {
postgresql.package = pkgs.postgresql_16;
octodns = {
enable = true;
records."".MX = {
ttl = 86400;
values = [{ exchange = "${smtp.host}."; }];
};
defaults.CNAME.ttl = 60;
};
hastebin = { hastebin = {
enable = true; enable = true;
subdomain = "bin"; subdomain = "bin";

View File

@@ -17,5 +17,6 @@
./readeck.nix ./readeck.nix
./donetick.nix ./donetick.nix
./dnote.nix ./dnote.nix
./octodns.nix
]; ];
} }

View File

@@ -31,28 +31,30 @@ in
}; };
}; };
services.prometheus = { services = {
enable = true; prometheus = {
exporters.node.enable = true; enable = true;
scrapeConfigs = [ exporters.node.enable = true;
{ scrapeConfigs = [
job_name = "node"; {
static_configs = [ job_name = "node";
{ targets = [ "localhost:9100" ]; } static_configs = [
]; { targets = [ "localhost:9100" ]; }
} ];
]; }
}; ];
};
services.webserver.vHosts.${fqdn}.locations."/".proxyPort = cfg.settings.server.http_port; webserver.vHosts.${fqdn}.locations."/".proxyPort = cfg.settings.server.http_port;
services.postgresql = { postgresql = {
enable = lib.mkDefault true; enable = lib.mkDefault true;
ensureDatabases = [ "grafana" ]; ensureDatabases = [ "grafana" ];
ensureUsers = [{ ensureUsers = [{
name = "grafana"; name = "grafana";
ensureDBOwnership = true; ensureDBOwnership = true;
}]; }];
};
}; };
}; };
} }

View File

@@ -11,24 +11,26 @@ in
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
services.invidious = { services = {
domain = fqdn; invidious = {
address = "127.0.0.1"; domain = fqdn;
address = "127.0.0.1";
settings = { settings = {
external_port = 443; external_port = 443;
db = { db = {
dbname = "invidious"; dbname = "invidious";
user = "invidious"; user = "invidious";
};
}; };
}; };
};
services.postgresql.enable = lib.mkDefault true; postgresql.enable = lib.mkDefault true;
services.webserver = { webserver = {
enable = lib.mkDefault true; enable = lib.mkDefault true;
vHosts.${fqdn}.locations."/".proxyPort = cfg.port; vHosts.${fqdn}.locations."/".proxyPort = cfg.port;
};
}; };
}; };
} }

View File

@@ -0,0 +1,210 @@
{ pkgs, lib, config, ... }:
let
cfg = config.services.octodns;
secrets = config.age.secrets;
types = {
ttlOptions = default: {
ttl = lib.mkOption {
type = lib.types.int;
default = default;
};
};
defaults = lib.types.submodule {
options = {
A = lib.mkOption {
type = lib.types.submodule {
options = types.ttlOptions cfg.defaults.ttl;
};
default = { };
};
AAAA = lib.mkOption {
type = lib.types.submodule {
options = types.ttlOptions cfg.defaults.ttl;
};
default = { };
};
CNAME = lib.mkOption {
type = lib.types.submodule {
options = types.ttlOptions cfg.defaults.ttl;
};
default = { };
};
MX = lib.mkOption {
type = lib.types.submodule {
options = types.ttlOptions cfg.defaults.ttl;
};
default = { };
};
} // (types.ttlOptions 3600);
};
aRecord = lib.types.submodule {
options = {
values = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
};
ttl = lib.mkOption {
type = lib.types.int;
default = cfg.defaults.A.ttl;
};
};
};
aaaaRecord = lib.types.submodule {
options = {
values = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
};
ttl = lib.mkOption {
type = lib.types.int;
default = cfg.defaults.AAAA.ttl;
};
};
};
cnameRecord = lib.types.submodule {
options = {
target = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
};
toRoot = lib.mkOption {
type = lib.types.bool;
default = false;
};
ttl = lib.mkOption {
type = lib.types.int;
default = cfg.defaults.CNAME.ttl;
};
};
};
mxValue = lib.types.submodule {
options = {
exchange = lib.mkOption {
type = lib.types.str;
};
preference = lib.mkOption {
type = lib.types.int;
default = 10;
};
};
};
mxRecord = lib.types.submodule {
options = {
values = lib.mkOption {
type = lib.types.listOf types.mxValue;
default = [ ];
};
ttl = lib.mkOption {
type = lib.types.int;
default = cfg.defaults.MX.ttl;
};
};
};
records = lib.types.submodule {
options = {
A = lib.mkOption {
type = types.aRecord;
default = { };
};
AAAA = lib.mkOption {
type = types.aaaaRecord;
default = { };
};
CNAME = lib.mkOption {
type = types.cnameRecord;
default = { };
};
MX = lib.mkOption {
type = types.mxRecord;
default = { };
};
};
};
};
yamlFormat = pkgs.formats.yaml { };
zoneFile = yamlFormat.generate "octodns-zone"
(
lib.filterAttrs (_: records: (lib.length records) > 0)
(lib.mapAttrs
(_: types:
lib.filter
({ values ? [ ], value ? null, ... }: (lib.length values) > 0 || !(builtins.isNull value))
(lib.mapAttrsToList
(type: { ttl, ... }@options:
if (type == "CNAME")
then
let
inherit (options) target toRoot;
value = if toRoot then "${config.networking.domain}." else target;
in
{ inherit type ttl value; }
else
{ inherit type ttl; inherit (options) values; }
)
types
)
)
cfg.records
)
);
zonesDir = pkgs.linkFarm "octodns-zones" {
"${config.networking.domain}.yaml" = zoneFile;
};
configFile = yamlFormat.generate "octodns-config.yaml" {
providers = {
config = {
class = "octodns.provider.yaml.YamlProvider";
directory = zonesDir;
default_ttl = cfg.defaults.ttl;
};
hetzner = {
class = "octodns_hetzner.HetznerProvider";
token = "env/HETZNER_TOKEN";
};
};
zones."*" = {
sources = [ "config" ];
targets = [ "hetzner" ];
};
};
octodns = pkgs.octodns.withProviders (_: [
pkgs.octodns-providers.hetzner
]);
in
{
options.services.octodns = {
enable = lib.mkEnableOption "Enable octodns";
records = lib.mkOption {
type = lib.types.attrsOf types.records;
default = { };
};
defaults = lib.mkOption {
type = types.defaults;
default = { };
};
};
config = lib.mkIf cfg.enable {
systemd.services.octodns = {
enable = true;
description = "OctoDNS";
serviceConfig = {
Type = "oneshot";
ExecStart = "${octodns}/bin/octodns-sync --config-file ${configFile} --doit";
DynamicUser = true;
EnvironmentFile = secrets.octodns.path;
RemainAfterExit = true;
};
wantedBy = lib.optionals config.services.nginx.enable [ "nginx.service" ];
before = lib.optionals config.services.nginx.enable [ "nginx.service" ];
};
};
}

View File

@@ -11,10 +11,12 @@ in
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
services.owncast = { services = {
openFirewall = true; owncast = {
}; openFirewall = true;
};
services.webserver.vHosts.${fqdn}.locations."/".proxyPort = cfg.port; webserver.vHosts.${fqdn}.locations."/".proxyPort = cfg.port;
};
}; };
} }

View File

@@ -15,15 +15,17 @@ in
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
services.syncthing = { services = {
openDefaultPorts = true; syncthing = {
guiAddress = "[::1]:${toString cfg.port}"; openDefaultPorts = true;
settings.gui.insecureSkipHostCheck = true; guiAddress = "[::1]:${toString cfg.port}";
}; settings.gui.insecureSkipHostCheck = true;
};
services.webserver = { webserver = {
enable = lib.mkDefault true; enable = lib.mkDefault true;
vHosts.${fqdn}.locations."/".proxyPort = cfg.port; vHosts.${fqdn}.locations."/".proxyPort = cfg.port;
};
}; };
}; };
} }

View File

@@ -46,36 +46,47 @@ in
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
services.nginx = { services = {
enable = lib.mkDefault true; nginx = {
recommendedProxySettings = true; enable = lib.mkDefault true;
recommendedTlsSettings = true; recommendedProxySettings = true;
recommendedBrotliSettings = true; recommendedTlsSettings = true;
recommendedGzipSettings = true; recommendedBrotliSettings = true;
recommendedZstdSettings = true; recommendedGzipSettings = true;
recommendedOptimisation = true; recommendedZstdSettings = true;
recommendedOptimisation = true;
virtualHosts = lib.mapAttrs virtualHosts = lib.mapAttrs
(_: { proxyBuffering, locations }: { (_: { proxyBuffering, locations }: {
forceSSL = true; forceSSL = true;
enableACME = true; enableACME = true;
http2 = true; http2 = true;
acmeRoot = lib.mkIf cfg.acme.dnsChallenge null; acmeRoot = lib.mkIf cfg.acme.dnsChallenge null;
extraConfig = lib.concatLines [ extraConfig = lib.concatLines [
(lib.optionalString (!proxyBuffering) "proxy_buffering off;") (lib.optionalString (!proxyBuffering) "proxy_buffering off;")
"charset utf-8;" "charset utf-8;"
]; ];
locations = lib.mapAttrs locations = lib.mapAttrs
(_: { proxyPort, extraConfig }: lib.mergeAttrsList [ (_: { proxyPort, extraConfig }: lib.mergeAttrsList [
{ inherit extraConfig; } { inherit extraConfig; }
(if (lib.isInt proxyPort) then { (if (lib.isInt proxyPort) then {
proxyWebsockets = true; proxyWebsockets = true;
proxyPass = "http://localhost:${toString proxyPort}"; proxyPass = "http://localhost:${toString proxyPort}";
} else { }) } else { })
]) ])
locations; locations;
}) })
cfg.vHosts; cfg.vHosts;
};
octodns.records = lib.filterAttrs (name: _: name != config.networking.domain) (
lib.mapAttrs'
(fqdn: _: {
name = lib.removeSuffix ".${config.networking.domain}" fqdn;
value = { CNAME.toRoot = true; };
})
cfg.vHosts
);
}; };
security.acme = { security.acme = {

View File

@@ -46,16 +46,18 @@ in
confinement.enable = true; confinement.enable = true;
}; };
services.webserver = { services = {
enable = lib.mkDefault true; webserver = {
vHosts.${fqdn}.locations."/" = { enable = lib.mkDefault true;
proxyPort = port; vHosts.${fqdn}.locations."/" = {
extraConfig = '' proxyPort = port;
client_max_body_size 50m; extraConfig = ''
proxy_send_timeout 300; client_max_body_size 50m;
proxy_read_timeout 300; proxy_send_timeout 300;
send_timeout 300; proxy_read_timeout 300;
''; send_timeout 300;
'';
};
}; };
}; };
}; };

View File

@@ -13,11 +13,13 @@ in
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
services.workout-tracker = { inherit package; }; services = {
workout-tracker = { inherit package; };
services.webserver = { webserver = {
enable = lib.mkDefault true; enable = lib.mkDefault true;
vHosts.${fqdn}.locations."/".proxyPort = port; vHosts.${fqdn}.locations."/".proxyPort = port;
};
}; };
}; };
} }

BIN
secrets/octodns.age Normal file

Binary file not shown.

View File

@@ -19,4 +19,5 @@ in
"readeck.age".publicKeys = users ++ [ freun-dev ]; "readeck.age".publicKeys = users ++ [ freun-dev ];
"donetick.age".publicKeys = users ++ [ freun-dev ]; "donetick.age".publicKeys = users ++ [ freun-dev ];
"dnote.age".publicKeys = users ++ [ freun-dev ]; "dnote.age".publicKeys = users ++ [ freun-dev ];
"octodns.age".publicKeys = users ++ [ freun-dev ];
} }