Merge pull request 'clanModule: Extend dyndns support for most dns hosters. Improve security.' (#1871) from Qubasa/clan-core:Qubasa-main into main

This commit is contained in:
clan-bot
2024-08-12 21:03:22 +00:00
4 changed files with 112 additions and 12 deletions

View File

@@ -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;

View File

@@ -169,6 +169,11 @@ in
];
};
networking.firewall.allowedTCPPorts = [
80
443
];
services.nginx = {
enable = true;
virtualHosts = {

View File

@@ -109,6 +109,7 @@ def deploy_machine(machines: MachineGroup) -> None:
cmd = [
"nixos-rebuild",
"switch",
"--show-trace",
"--fast",
"--option",
"keep-going",

View File

@@ -35,6 +35,7 @@ def nix_build(flags: list[str], gcroot: Path | None = None) -> list[str]:
str(gcroot),
"--print-out-paths",
"--no-write-lock-file",
"--show-trace",
]
)
+ flags
@@ -47,6 +48,7 @@ def nix_build(flags: list[str], gcroot: Path | None = None) -> list[str]:
"--no-link",
"--print-out-paths",
"--no-write-lock-file",
"--show-trace",
]
)
+ flags