278 lines
10 KiB
Nix
278 lines
10 KiB
Nix
{ ... }:
|
|
{
|
|
_class = "clan.service";
|
|
manifest.name = "clan-core/dyndns";
|
|
manifest.description = "A dynamic DNS service to update domain IPs";
|
|
manifest.categories = [ "Network" ];
|
|
manifest.readme = builtins.readFile ./README.md;
|
|
|
|
roles.default = {
|
|
interface =
|
|
{ lib, ... }:
|
|
{
|
|
options = {
|
|
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";
|
|
};
|
|
acmeEmail = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = ''
|
|
Email address for account creation and correspondence from the CA.
|
|
It is recommended to use the same email for all certs to avoid account
|
|
creation limits.
|
|
'';
|
|
};
|
|
};
|
|
|
|
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 (
|
|
{ ... }:
|
|
{
|
|
options = {
|
|
provider = lib.mkOption {
|
|
example = "namecheap";
|
|
type = lib.types.str;
|
|
description = "The dyndns provider to use";
|
|
};
|
|
domain = lib.mkOption {
|
|
type = lib.types.str;
|
|
example = "example.com";
|
|
description = "The top level domain to update.";
|
|
};
|
|
secret_field_name = lib.mkOption {
|
|
example = "api_key";
|
|
|
|
type = lib.types.enum [
|
|
"password"
|
|
"token"
|
|
"api_key"
|
|
"secret_api_key"
|
|
];
|
|
default = "password";
|
|
description = "The field name for the secret";
|
|
};
|
|
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 = "Configuration for which domains to update";
|
|
};
|
|
};
|
|
};
|
|
|
|
perInstance =
|
|
{ settings, ... }:
|
|
{
|
|
nixosModule =
|
|
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
let
|
|
name = "dyndns";
|
|
cfg = settings;
|
|
|
|
# We dedup secrets if they have the same provider + base domain
|
|
secret_id = opt: "${name}-${opt.provider}-${opt.domain}";
|
|
secret_path =
|
|
opt: config.clan.core.vars.generators."${secret_id opt}".files."${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;
|
|
|
|
service_config = {
|
|
settings = builtins.catAttrs "value" (
|
|
builtins.attrValues (
|
|
lib.mapAttrs (_: opt: {
|
|
value =
|
|
(extraSettingsSafe opt)
|
|
// {
|
|
domain = opt.domain;
|
|
provider = opt.provider;
|
|
}
|
|
// {
|
|
"${opt.secret_field_name}" = secret_id opt;
|
|
};
|
|
}) cfg.settings
|
|
)
|
|
);
|
|
};
|
|
|
|
secret_generator = _: opt: {
|
|
name = secret_id opt;
|
|
value = {
|
|
share = true;
|
|
migrateFact = "${secret_id opt}";
|
|
prompts.${secret_id opt} = {
|
|
type = "hidden";
|
|
persist = true;
|
|
};
|
|
};
|
|
};
|
|
in
|
|
{
|
|
imports = lib.optional cfg.server.enable (
|
|
lib.modules.importApply ./nginx.nix {
|
|
inherit config;
|
|
inherit settings;
|
|
inherit lib;
|
|
}
|
|
);
|
|
|
|
clan.core.vars.generators = lib.mkIf (cfg.settings != { }) (
|
|
lib.mapAttrs' secret_generator cfg.settings
|
|
);
|
|
|
|
users.groups.${name} = lib.mkIf (cfg.settings != { }) { };
|
|
users.users.${name} = lib.mkIf (cfg.settings != { }) {
|
|
group = name;
|
|
isSystemUser = true;
|
|
description = "User for ${name} service";
|
|
home = "/var/lib/${name}";
|
|
createHome = true;
|
|
};
|
|
|
|
services.nginx = lib.mkIf cfg.server.enable {
|
|
virtualHosts = {
|
|
"${cfg.server.domain}" = {
|
|
forceSSL = true;
|
|
enableACME = true;
|
|
locations."/" = {
|
|
proxyPass = "http://localhost:${toString cfg.server.port}";
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
systemd.services.${name} = lib.mkIf (cfg.settings != { }) {
|
|
path = [ ];
|
|
description = "Dynamic DNS updater";
|
|
after = [ "network.target" ];
|
|
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}";
|
|
GODEBUG = "netdns=go"; # We need to set this untill this has been merged. https://github.com/NixOS/nixpkgs/pull/432758
|
|
};
|
|
|
|
serviceConfig =
|
|
let
|
|
pyscript =
|
|
pkgs.writers.writePython3Bin "generate_secret_config.py"
|
|
{
|
|
libraries = [ ];
|
|
doCheck = false;
|
|
}
|
|
''
|
|
import json
|
|
from pathlib import Path
|
|
import os
|
|
|
|
cred_dir = Path(os.getenv("CREDENTIALS_DIRECTORY"))
|
|
config_str = os.getenv("MYCONFIG")
|
|
|
|
|
|
def get_credential(name):
|
|
secret_p = cred_dir / name
|
|
with open(secret_p, 'r') as f:
|
|
return f.read().strip()
|
|
|
|
|
|
config = json.loads(config_str)
|
|
print(f"Config: {config}")
|
|
for attrset in config["settings"]:
|
|
if "password" in attrset:
|
|
attrset['password'] = get_credential(attrset['password'])
|
|
elif "token" in attrset:
|
|
attrset['token'] = get_credential(attrset['token'])
|
|
elif "secret_api_key" in attrset:
|
|
attrset['secret_api_key'] = get_credential(attrset['secret_api_key'])
|
|
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')
|
|
data_dir.mkdir(mode=0o770, exist_ok=True)
|
|
|
|
# Create a temporary config file
|
|
# with appropriate permissions
|
|
tmp_config_path = data_dir / '.config.json'
|
|
tmp_config_path.touch(mode=0o660, exist_ok=False)
|
|
|
|
# Write the config with secrets back
|
|
with open(tmp_config_path, 'w') as f:
|
|
f.write(json.dumps(config, indent=4))
|
|
|
|
# Move config into place
|
|
config_path = data_dir / 'config.json'
|
|
tmp_config_path.rename(config_path)
|
|
|
|
# Set file permissions to read
|
|
# and write only by the user and group
|
|
for file in data_dir.iterdir():
|
|
file.chmod(0o660)
|
|
'';
|
|
in
|
|
{
|
|
ExecStartPre = lib.getExe pyscript;
|
|
ExecStart = lib.getExe pkgs.ddns-updater;
|
|
LoadCredential = lib.mapAttrsToList (_: opt: "${secret_id opt}:${secret_path opt}") cfg.settings;
|
|
User = name;
|
|
Group = name;
|
|
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;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|