add headscale and syncthing

This commit is contained in:
waveringana 2025-07-01 16:58:02 -04:00
parent d90ad7fbc5
commit cfa997a852
9 changed files with 533 additions and 29 deletions

View file

@ -0,0 +1,229 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.modules.headscale;
in
{
options = {
modules = {
headscale = {
enable = mkEnableOption "Deploy headscale";
oidcClientSecretPath = mkOption {
type = types.str;
default = "/etc/headscale/oidc_client_secret.key";
description = "Path to OIDC client secret file";
example = "config.age.secrets.headscale-oidc-key.path";
};
litestream = {
enable = mkEnableOption "Enable litestream for headscale database backups";
replicas = mkOption {
type = types.listOf (types.attrsOf types.anything);
default = [];
description = "List of litestream replica configurations";
example = [
{
url = "s3://your-backup-bucket/headscale/db";
access-key-id = "$LITESTREAM_ACCESS_KEY_ID";
secret-access-key = "$LITESTREAM_SECRET_ACCESS_KEY";
region = "us-east-1";
}
];
};
backupPath = mkOption {
type = types.nullOr types.str;
default = null;
description = "Local backup path (alternative to S3)";
example = "/backup/headscale";
};
syncInterval = mkOption {
type = types.str;
default = "1s";
description = "How often to sync to replicas";
};
retention = mkOption {
type = types.str;
default = "72h";
description = "How long to retain snapshots";
};
environmentFile = mkOption {
type = types.nullOr types.path;
default = null;
description = "Environment file containing S3 credentials (can be agenix secret)";
example = "config.age.secrets.litestream-env.path";
};
};
};
};
};
config = mkIf cfg.enable {
services.headscale = {
enable = true;
address = "0.0.0.0";
port = 8080;
settings = {
server_url = "https://headscale.nekomimi.pet";
# Metrics and gRPC
metrics_listen_addr = "127.0.0.1:9090";
grpc_listen_addr = "127.0.0.1:50443";
grpc_allow_insecure = false;
# Prefixes
prefixes = {
v4 = "100.64.0.0/10";
v6 = "fd7a:115c:a1e0::/48";
allocation = "sequential";
};
# Database
database = {
type = "sqlite";
sqlite = {
path = "/var/lib/headscale/db.sqlite";
write_ahead_log = true;
};
};
# Noise
noise = {
private_key_path = "/var/lib/headscale/noise_private.key";
};
# DERP
derp = {
urls = [
"https://controlplane.tailscale.com/derpmap/default"
];
paths = [];
auto_update_enabled = true;
update_frequency = "24h";
server = {
enabled = false;
region_id = 999;
region_code = "headscale";
region_name = "Headscale Embedded DERP";
stun_listen_addr = "0.0.0.0:3478";
private_key_path = "/var/lib/headscale/derp_server_private.key";
automatically_add_embedded_derp_region = true;
ipv4 = "1.2.3.4";
ipv6 = "2001:db8::1";
};
};
# DNS
dns = {
magic_dns = true;
base_domain = "dns.sharkgirl.pet";
nameservers = {
global = [
"100.64.0.7"
"1.1.1.1"
"1.0.0.1"
"2606:4700:4700::1111"
"2606:4700:4700::1001"
];
};
search_domains = [];
};
# OIDC with configurable secret path
oidc = {
only_start_if_oidc_is_available = true;
issuer = "https://pocketid.nekomimi.pet";
client_id = "f345acad-3eac-45b7-9d91-57f388987a57";
client_secret_path = cfg.oidcClientSecretPath;
pkce = {
enabled = true;
method = "S256";
};
};
# Policy
policy = {
mode = "database";
};
# TLS/ACME
acme_url = "https://acme-v02.api.letsencrypt.org/directory";
acme_email = "";
tls_letsencrypt_hostname = "";
tls_letsencrypt_cache_dir = "/var/lib/headscale/cache";
tls_letsencrypt_challenge_type = "HTTP-01";
tls_letsencrypt_listen = ":http";
tls_cert_path = "";
tls_key_path = "";
# Logging
log = {
format = "text";
level = "info";
};
# Misc settings
disable_check_updates = false;
ephemeral_node_inactivity_timeout = "30m";
unix_socket = "/var/run/headscale/headscale.sock";
unix_socket_permission = "0770";
logtail = {
enabled = false;
};
randomize_client_port = false;
};
};
# Configurable Litestream for SQLite database backups
services.litestream = mkIf cfg.litestream.enable {
enable = true;
settings = {
dbs = [
{
path = "/var/lib/headscale/db.sqlite";
sync-interval = cfg.litestream.syncInterval;
retention = cfg.litestream.retention;
replicas =
# Use custom replicas if provided
if cfg.litestream.replicas != [] then
cfg.litestream.replicas
# Otherwise use local backup if path is provided
else if cfg.litestream.backupPath != null then
[{ path = cfg.litestream.backupPath; }]
# Default empty (user must configure)
else
[];
}
];
};
};
# Configure systemd service to use agenix secrets
systemd.services.headscale.serviceConfig = mkMerge [
{
SupplementaryGroups = [ "headscale-secrets" ];
}
# Add environment file for litestream if specified
(mkIf (cfg.litestream.enable && cfg.litestream.environmentFile != null) {
EnvironmentFile = cfg.litestream.environmentFile;
})
];
# Configure litestream service with environment file if specified
systemd.services.litestream = mkIf (cfg.litestream.enable && cfg.litestream.environmentFile != null) {
serviceConfig = {
EnvironmentFile = cfg.litestream.environmentFile;
};
};
# Create a group for accessing secrets
users.groups.headscale-secrets = {};
};
}

View file

@ -0,0 +1,257 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.modules.syncthing;
# Helper function to create a serviceConfig entry if the condition is met
mkServiceConfigOption = name: value: mkIf (value != null) { "${name}" = value; };
# Construct the settings object for Syncthing
syncthingSettings = mkMerge [
# GUI configuration
(mkIf cfg.gui.enable {
gui = mkMerge [
(mkIf (cfg.gui.user != null) {
user = cfg.gui.user;
})
];
})
# Devices configuration
(mkIf (cfg.devices != {}) {
devices = mapAttrs (name: device: {
id = device.id;
} // optionalAttrs (device.name != null) {
name = device.name;
} // optionalAttrs (device.addresses != []) {
addresses = device.addresses;
}) cfg.devices;
})
# Folders configuration
(mkIf (cfg.folders != {}) {
folders = mapAttrs (name: folder: {
path = folder.path;
devices = folder.devices;
} // optionalAttrs (folder.ignorePerms != null) {
ignorePerms = folder.ignorePerms;
} // optionalAttrs (folder.type != null) {
type = folder.type;
} // optionalAttrs (folder.rescanIntervalS != null) {
rescanIntervalS = folder.rescanIntervalS;
} // optionalAttrs (folder.versioning != null) {
versioning = folder.versioning;
}) cfg.folders;
})
# Extra options
cfg.extraOptions
];
in
{
options = {
modules.syncthing = {
enable = mkEnableOption "Deploy syncthing";
openDefaultPorts = mkOption {
type = types.bool;
default = true;
description = "Open ports in the firewall for Syncthing";
};
disableDefaultFolder = mkOption {
type = types.bool;
default = true;
description = "Don't create default ~/Sync folder";
};
gui = {
enable = mkEnableOption "Enable GUI configuration";
user = mkOption {
type = types.nullOr types.str;
default = null;
description = "GUI username";
example = "myuser";
};
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
description = "Path to file containing GUI password";
example = "config.age.secrets.syncthing-gui-password.path";
};
};
identity = {
keyPath = mkOption {
type = types.nullOr types.path;
default = null;
description = "Path to Syncthing private key for stable device ID";
example = "config.age.secrets.syncthing-key.path";
};
certPath = mkOption {
type = types.nullOr types.path;
default = null;
description = "Path to Syncthing certificate for stable device ID";
example = "config.age.secrets.syncthing-cert.path";
};
};
devices = mkOption {
type = types.attrsOf (types.submodule {
options = {
id = mkOption {
type = types.str;
description = "Device ID";
example = "DMWVMM6-MKEQVB4-I4UZTRH-5A6E24O-XHQTL3K-AAI5R5L-MXNMUGX-QTGRHQ2";
};
name = mkOption {
type = types.nullOr types.str;
default = null;
description = "Device name (optional)";
};
addresses = mkOption {
type = types.listOf types.str;
default = [];
description = "Device addresses";
example = [ "tcp://192.168.1.100:22000" ];
};
};
});
default = {};
description = "Syncthing devices configuration";
example = {
"laptop" = {
id = "DMWVMM6-MKEQVB4-I4UZTRH-5A6E24O-XHQTL3K-AAI5R5L-MXNMUGX-QTGRHQ2";
};
"phone" = {
id = "ANOTHER-DEVICE-ID-GOES-HERE";
addresses = [ "tcp://192.168.1.101:22000" ];
};
};
};
folders = mkOption {
type = types.attrsOf (types.submodule {
options = {
path = mkOption {
type = types.str;
description = "Local folder path";
example = "/home/myuser/Documents";
};
devices = mkOption {
type = types.listOf (types.either types.str (types.submodule {
options = {
name = mkOption {
type = types.str;
description = "Device name";
};
encryptionPasswordFile = mkOption {
type = types.path;
description = "Path to file containing encryption password";
};
};
}));
default = [];
description = "List of devices that can access this folder";
example = [ "laptop" "phone" ];
};
ignorePerms = mkOption {
type = types.nullOr types.bool;
default = null;
description = "Whether to ignore file permissions";
};
type = mkOption {
type = types.nullOr (types.enum [ "sendreceive" "sendonly" "receiveonly" ]);
default = null;
description = "Folder type";
};
rescanIntervalS = mkOption {
type = types.nullOr types.int;
default = null;
description = "Rescan interval in seconds";
};
versioning = mkOption {
type = types.nullOr (types.submodule {
options = {
type = mkOption {
type = types.enum [ "external" "simple" "staggered" "trashcan" ];
description = "Versioning type";
};
params = mkOption {
type = types.attrsOf types.str;
default = {};
description = "Versioning parameters";
};
};
});
default = null;
description = "Folder versioning configuration";
};
};
});
default = {};
description = "Syncthing folders configuration";
example = {
"Documents" = {
path = "/home/myuser/Documents";
devices = [ "laptop" "phone" ];
ignorePerms = false;
};
"Sensitive" = {
path = "/home/myuser/Sensitive";
devices = [
"laptop"
{
name = "phone";
encryptionPasswordFile = "/run/secrets/syncthing-sensitive-password";
}
];
};
};
};
extraOptions = mkOption {
type = types.attrsOf types.anything;
default = {};
description = "Additional Syncthing configuration options";
};
};
};
config = mkIf cfg.enable {
services.syncthing = {
enable = true;
openDefaultPorts = cfg.openDefaultPorts;
# Set stable identity if provided
key = mkIf (cfg.identity.keyPath != null) cfg.identity.keyPath;
cert = mkIf (cfg.identity.certPath != null) cfg.identity.certPath;
# Combine all settings
settings = syncthingSettings;
};
# Configure systemd service options collectively
systemd.services.syncthing = {
# Add environment variable to disable default folder creation
environment.STNODEFAULTFOLDER = mkIf cfg.disableDefaultFolder "true";
# Add supplementary groups for secret access
serviceConfig.SupplementaryGroups = [ "syncthing-secrets" ];
};
# Create a group for accessing secrets
users.groups.syncthing-secrets = {};
};
}