From 50210708c5d37f30b9712ef2161286c29d1e2356 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Mon, 12 Aug 2024 22:27:08 +0200 Subject: [PATCH] clanModule: Extend dyndns support for most dns hosters. Improve security. --- clanModules/dyndns/default.nix | 116 ++++++++++++++++++++++--- clanModules/matrix-synapse/default.nix | 5 ++ 2 files changed, 109 insertions(+), 12 deletions(-) diff --git a/clanModules/dyndns/default.nix b/clanModules/dyndns/default.nix index 3ef3759fa..3363b251c 100644 --- a/clanModules/dyndns/default.nix +++ b/clanModules/dyndns/default.nix @@ -14,19 +14,32 @@ let secret_path = opt: config.clan.core.facts.services."${secret_id opt}".secret."${secret_id opt}".path; + # We check that a secret has not been set in extraSettings. + extraSettingsSafe = + opt: + if (builtins.hasAttr opt.secret_field_name opt.extraSettings) then + throw "Please do not set ${opt.secret_field_name} in extraSettings, it is automatically set by the dyndns module." + else + opt.extraSettings; /* We go from: - {home.gchq.icu:{value:{domain:gchq.icu,host:home, provider:namecheap}}} + {home.example.com:{value:{domain:example.com,host:home, provider:namecheap}}} To: - {settings: [{domain: gchq.icu, host: home, provider: namecheap, password: dyndns-namecheap-gchq.icu}]} + {settings: [{domain: example.com, host: home, provider: namecheap, password: dyndns-namecheap-example.com}]} */ service_config = { settings = builtins.catAttrs "value" ( builtins.attrValues ( lib.mapAttrs (_: opt: { - value = opt // { - password = secret_id opt; - }; + value = + (extraSettingsSafe opt) + // { + domain = opt.domain; + provider = opt.provider; + } + // { + "${opt.secret_field_name}" = secret_id opt; + }; }) cfg.settings ) ); @@ -57,6 +70,25 @@ in description = "Group to run the service as"; }; + server = { + enable = lib.mkEnableOption "dyndns webserver"; + domain = lib.mkOption { + type = lib.types.str; + description = "Domain to serve the webservice on"; + }; + port = lib.mkOption { + type = lib.types.int; + default = 54805; + description = "Port to listen on"; + }; + }; + + period = lib.mkOption { + type = lib.types.int; + default = 5; + description = "Domain update period in minutes"; + }; + settings = lib.mkOption { type = lib.types.attrsOf ( lib.types.submodule ( @@ -64,24 +96,44 @@ in { options = { provider = lib.mkOption { + example = "namecheap"; type = lib.types.str; description = "The dyndns provider to use"; }; domain = lib.mkOption { type = lib.types.str; - description = "The top level domain to update. For example 'example.com'. - If you want to update a subdomain, add the 'subdomain' option"; + example = "example.com"; + description = "The top level domain to update."; }; - host = lib.mkOption { - type = lib.types.nullOr lib.types.str; - description = "The subdomain to update of the tld"; + secret_field_name = lib.mkOption { + example = [ + "password" + "api_key" + ]; + type = lib.types.enum [ + "password" + "token" + "api_key" + ]; + default = "password"; + description = "The field name for the secret"; + }; + # TODO: Ideally we would create a gigantic list of all possible settings / types + # optimally we would have a way to generate the options from the source code + extraSettings = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = '' + Extra settings for the provider. + Provider specific settings: https://github.com/qdm12/ddns-updater#configuration + ''; }; }; } ) ); default = [ ]; - description = "Wifi networks to predefine"; + description = "Configuration for which domains to update"; }; }; @@ -106,6 +158,24 @@ in createHome = true; }; + networking.firewall.allowedTCPPorts = lib.mkIf cfg.server.enable [ + 80 + 443 + ]; + + services.nginx = lib.mkIf cfg.server.enable { + enable = true; + virtualHosts = { + "${cfg.server.domain}" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://localhost:${toString cfg.server.port}"; + }; + }; + }; + }; + systemd.services.${name} = { path = [ ]; description = "Dynamic DNS updater"; @@ -113,6 +183,9 @@ in wantedBy = [ "multi-user.target" ]; environment = { MYCONFIG = "${builtins.toJSON service_config}"; + SERVER_ENABLED = if cfg.server.enable then "yes" else "no"; + PERIOD = "${toString cfg.period}m"; + LISTENING_ADDRESS = ":${toString cfg.server.port}"; }; serviceConfig = @@ -135,7 +208,14 @@ in config = json.loads(config_str) print(f"Config: {config}") for attrset in config["settings"]: - attrset['password'] = get_credential(attrset['password']) + if "password" in attrset: + attrset['password'] = get_credential(attrset['password']) + elif "token" in attrset: + attrset['token'] = get_credential(attrset['token']) + elif "api_key" in attrset: + attrset['api_key'] = get_credential(attrset['api_key']) + else: + raise ValueError(f"Missing secret field in {attrset}") # create directory data if it does not exist data_dir = Path('data') @@ -162,7 +242,19 @@ in LoadCredential = lib.mapAttrsToList (_: opt: "${secret_id opt}:${secret_path opt}") cfg.settings; User = cfg.user; Group = cfg.group; + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ReadOnlyPaths = "/"; + PrivateDevices = "yes"; + ProtectKernelModules = "yes"; + ProtectKernelTunables = "yes"; + WorkingDirectory = "/var/lib/${name}"; + ReadWritePaths = [ + "/proc/self" + "/var/lib/${name}" + ]; Restart = "always"; RestartSec = 60; diff --git a/clanModules/matrix-synapse/default.nix b/clanModules/matrix-synapse/default.nix index 5540f7bc6..cceb465c1 100644 --- a/clanModules/matrix-synapse/default.nix +++ b/clanModules/matrix-synapse/default.nix @@ -169,6 +169,11 @@ in ]; }; + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + services.nginx = { enable = true; virtualHosts = {