diff --git a/checks/service-dummy-test-from-flake/flake.nix b/checks/service-dummy-test-from-flake/flake.nix index c6d959822..ec0621d81 100644 --- a/checks/service-dummy-test-from-flake/flake.nix +++ b/checks/service-dummy-test-from-flake/flake.nix @@ -27,7 +27,9 @@ modules.new-service = { _class = "clan.service"; manifest.name = "new-service"; - roles.peer = { }; + roles.peer = { + description = "A peer that uses the new-service to generate some files."; + }; perMachine = { nixosModule = { # This should be generated by: diff --git a/checks/service-dummy-test/default.nix b/checks/service-dummy-test/default.nix index 263133f3a..7f16945bd 100644 --- a/checks/service-dummy-test/default.nix +++ b/checks/service-dummy-test/default.nix @@ -34,7 +34,9 @@ nixosLib.runTest ( modules.new-service = { _class = "clan.service"; manifest.name = "new-service"; - roles.peer = { }; + roles.peer = { + description = "A peer that uses the new-service to generate some files."; + }; perMachine = { nixosModule = { # This should be generated by: diff --git a/clanServices/admin/default.nix b/clanServices/admin/default.nix index 807d8a69c..89ab46cae 100644 --- a/clanServices/admin/default.nix +++ b/clanServices/admin/default.nix @@ -1,14 +1,14 @@ { _class = "clan.service"; manifest.name = "clan-core/admin"; - manifest.description = "Convenient Administration for the Clan App"; + manifest.description = "Adds a root user with ssh access"; manifest.categories = [ "Utility" ]; roles.default = { + description = "Placeholder role to apply the admin service"; interface = { lib, ... }: { - options = { allowedKeys = lib.mkOption { default = { }; diff --git a/clanServices/borgbackup/README.md b/clanServices/borgbackup/README.md index af1fc5f56..2e5eaa7e7 100644 --- a/clanServices/borgbackup/README.md +++ b/clanServices/borgbackup/README.md @@ -9,7 +9,7 @@ inventory.instances = { }; roles.client.machines."jon".settings = { destinations."storagebox" = { - repo = "username@$hostname:/./borgbackup"; + repo = "username@hostname:/./borgbackup"; rsh = ''ssh -oPort=23 -i /run/secrets/vars/borgbackup/borgbackup.ssh''; }; }; diff --git a/clanServices/borgbackup/default.nix b/clanServices/borgbackup/default.nix index fa7d575cb..fa9538c1d 100644 --- a/clanServices/borgbackup/default.nix +++ b/clanServices/borgbackup/default.nix @@ -62,7 +62,7 @@ }; roles.client = { - description = "A borgbackup client that backs up to one or more borgbackup servers."; + description = "A borgbackup client that backs up to all borgbackup server roles."; interface = { lib, diff --git a/clanServices/certificates/default.nix b/clanServices/certificates/default.nix index 0d9f5344b..d862923e0 100644 --- a/clanServices/certificates/default.nix +++ b/clanServices/certificates/default.nix @@ -2,12 +2,12 @@ { _class = "clan.service"; manifest.name = "certificates"; - manifest.description = "Sets up a certificates internal to your Clan"; + manifest.description = "Sets up a PKI certificate chain using step-ca"; manifest.categories = [ "Network" ]; manifest.readme = builtins.readFile ./README.md; roles.ca = { - + description = "A certificate authority that issues and signs certificates for other machines."; interface = { lib, ... }: { @@ -184,6 +184,7 @@ # Empty role, so we can add non-ca machins to the instance to trust the CA roles.default = { + description = "A machine that trusts the CA and can get certificates issued by it."; interface = { lib, ... }: { diff --git a/clanServices/coredns/README.md b/clanServices/coredns/README.md index cba868727..506ab6bd0 100644 --- a/clanServices/coredns/README.md +++ b/clanServices/coredns/README.md @@ -45,13 +45,15 @@ inventory = { # Add the default role to all machines, including `client` roles.default.tags.all = { }; - # DNS server + # DNS server queries to http://.foo are resolved here roles.server.machines."dnsserver".settings = { ip = "192.168.1.2"; tld = "foo"; }; # First service + # Registers http://one.foo will resolve to 192.168.1.3 + # underlying service runs on server01 roles.default.machines."server01".settings = { ip = "192.168.1.3"; services = [ "one" ]; diff --git a/clanServices/coredns/default.nix b/clanServices/coredns/default.nix index 36ec489ab..0b8728aa6 100644 --- a/clanServices/coredns/default.nix +++ b/clanServices/coredns/default.nix @@ -8,7 +8,7 @@ manifest.readme = builtins.readFile ./README.md; roles.server = { - + description = "A DNS server that resolves services in the clan network."; interface = { lib, ... }: { @@ -103,6 +103,7 @@ }; roles.default = { + description = "A machine that registers the 'server' role as resolver and registers services under the configured TLD in the resolver."; interface = { lib, ... }: { diff --git a/clanServices/data-mesher/default.nix b/clanServices/data-mesher/default.nix index 442311fd0..2c0562c04 100644 --- a/clanServices/data-mesher/default.nix +++ b/clanServices/data-mesher/default.nix @@ -101,6 +101,7 @@ in manifest.readme = builtins.readFile ./README.md; roles.admin = { + description = "A data-mesher admin node that bootstraps the network and can sign new nodes into the network."; interface = { lib, ... }: { @@ -177,6 +178,7 @@ in }; roles.signer = { + description = "A data-mesher signer node that can sign new nodes into the network."; interface = sharedInterface; perInstance = { @@ -208,6 +210,7 @@ in }; roles.peer = { + description = "A data-mesher peer node that connects to the network."; interface = sharedInterface; perInstance = { diff --git a/clanServices/dyndns/default.nix b/clanServices/dyndns/default.nix index c8326023a..21c8fc8a8 100644 --- a/clanServices/dyndns/default.nix +++ b/clanServices/dyndns/default.nix @@ -2,11 +2,12 @@ { _class = "clan.service"; manifest.name = "clan-core/dyndns"; - manifest.description = "A dynamic DNS service to update domain IPs"; + manifest.description = "A dynamic DNS service to auto update domain IPs"; manifest.categories = [ "Network" ]; manifest.readme = builtins.readFile ./README.md; roles.default = { + description = "Placeholder role to apply the dyndns service"; interface = { lib, ... }: { diff --git a/clanServices/emergency-access/default.nix b/clanServices/emergency-access/default.nix index 14d69978c..16880d952 100644 --- a/clanServices/emergency-access/default.nix +++ b/clanServices/emergency-access/default.nix @@ -2,31 +2,34 @@ { _class = "clan.service"; manifest.name = "clan-core/emergency-access"; - manifest.description = "Set recovery password for emergency access to machine"; + manifest.description = "Set recovery password for emergency access to machine to debug boot issues"; manifest.categories = [ "System" ]; manifest.readme = builtins.readFile ./README.md; - roles.default.perInstance = { - nixosModule = - { config, pkgs, ... }: - { - boot.initrd.systemd.emergencyAccess = - config.clan.core.vars.generators.emergency-access.files.password-hash.value; + roles.default = { + description = "Placeholder role to apply the emergency-access service"; + perInstance = { + nixosModule = + { config, pkgs, ... }: + { + boot.initrd.systemd.emergencyAccess = + config.clan.core.vars.generators.emergency-access.files.password-hash.value; - clan.core.vars.generators.emergency-access = { - runtimeInputs = [ - pkgs.coreutils - pkgs.mkpasswd - pkgs.xkcdpass - ]; - files.password.deploy = false; - files.password-hash.secret = false; + clan.core.vars.generators.emergency-access = { + runtimeInputs = [ + pkgs.coreutils + pkgs.mkpasswd + pkgs.xkcdpass + ]; + files.password.deploy = false; + files.password-hash.secret = false; - script = '' - xkcdpass --numwords 4 --delimiter - --count 1 | tr -d "\n" > $out/password - mkpasswd -s -m sha-512 < $out/password | tr -d "\n" > $out/password-hash - ''; + script = '' + xkcdpass --numwords 4 --delimiter - --count 1 | tr -d "\n" > $out/password + mkpasswd -s -m sha-512 < $out/password | tr -d "\n" > $out/password-hash + ''; + }; }; - }; + }; }; } diff --git a/clanServices/garage/default.nix b/clanServices/garage/default.nix index de48867ad..b796e63b5 100644 --- a/clanServices/garage/default.nix +++ b/clanServices/garage/default.nix @@ -6,7 +6,7 @@ manifest.categories = [ "System" ]; roles.default = { - + description = "Placeholder role to apply the garage service"; perInstance.nixosModule = { config, diff --git a/clanServices/hello-world/default.nix b/clanServices/hello-world/default.nix index 5d668ff94..f64857d4a 100644 --- a/clanServices/hello-world/default.nix +++ b/clanServices/hello-world/default.nix @@ -14,6 +14,7 @@ # defined in this file directly (e.g. the "morning" role) or split up into a # separate file (e.g. the "evening" role) roles.morning = { + description = "A morning greeting machine"; interface = { lib, ... }: { @@ -67,6 +68,7 @@ # the interface here, so we can see all settings of the service in one place, # but you can also move it to the respective file roles.evening = { + description = "An evening greeting machine"; interface = { lib, ... }: { diff --git a/clanServices/importer/default.nix b/clanServices/importer/default.nix index 3f2086e9e..1b65890c2 100644 --- a/clanServices/importer/default.nix +++ b/clanServices/importer/default.nix @@ -6,5 +6,7 @@ manifest.categories = [ "Utility" ]; manifest.readme = builtins.readFile ./README.md; - roles.default = { }; + roles.default = { + description = "Placeholder role to apply the importer service"; + }; } diff --git a/clanServices/internet/default.nix b/clanServices/internet/default.nix index c9222b743..575a41d7a 100644 --- a/clanServices/internet/default.nix +++ b/clanServices/internet/default.nix @@ -2,12 +2,13 @@ { _class = "clan.service"; manifest.name = "clan-core/internet"; - manifest.description = "direct access (or via ssh jumphost) to machines"; + manifest.description = "Part of the clan networking abstraction to define how to reach machines from outside the clan network over the internet, if defined has the highest priority"; manifest.categories = [ "System" "Network" ]; roles.default = { + description = "Placeholder role to apply the internet service"; interface = { lib, ... }: { diff --git a/clanServices/localbackup/default.nix b/clanServices/localbackup/default.nix index 88c4ca1d8..e12fda2bc 100644 --- a/clanServices/localbackup/default.nix +++ b/clanServices/localbackup/default.nix @@ -2,11 +2,12 @@ { _class = "clan.service"; manifest.name = "localbackup"; - manifest.description = "Automatically backups current machine to local directory."; + manifest.description = "Automatically backups current machine to local directory or a mounted drive."; manifest.categories = [ "System" ]; manifest.readme = builtins.readFile ./README.md; roles.default = { + description = "Placeholder role to apply the localbackup service"; interface = { lib, ... }: { diff --git a/clanServices/matrix-synapse/default.nix b/clanServices/matrix-synapse/default.nix index 9f738176a..d5782831c 100644 --- a/clanServices/matrix-synapse/default.nix +++ b/clanServices/matrix-synapse/default.nix @@ -6,6 +6,7 @@ manifest.categories = [ "Social" ]; roles.default = { + description = "Placeholder role to apply the matrix-synapse service"; interface = { lib, ... }: { diff --git a/clanServices/monitoring/default.nix b/clanServices/monitoring/default.nix index 84f5fa57a..dd61ead8f 100644 --- a/clanServices/monitoring/default.nix +++ b/clanServices/monitoring/default.nix @@ -6,6 +6,7 @@ manifest.readme = builtins.readFile ./README.md; roles.telegraf = { + description = "Placeholder role to apply the telegraf monitoring agent"; interface = { lib, ... }: { diff --git a/clanServices/mycelium/default.nix b/clanServices/mycelium/default.nix index 65737cff9..94fd51dc0 100644 --- a/clanServices/mycelium/default.nix +++ b/clanServices/mycelium/default.nix @@ -2,13 +2,14 @@ { _class = "clan.service"; manifest.name = "clan-core/mycelium"; - manifest.description = "End-2-end encrypted IPv6 overlay network"; + manifest.description = "End-2-end encrypted P2P IPv6 overlay network"; manifest.categories = [ "System" "Network" ]; roles.peer = { + description = "A peer in the mycelium network"; interface = { lib, ... }: { diff --git a/clanServices/packages/default.nix b/clanServices/packages/default.nix index 0ff5470c0..795e59b6e 100644 --- a/clanServices/packages/default.nix +++ b/clanServices/packages/default.nix @@ -8,6 +8,7 @@ ]; roles.default = { + description = "Placeholder role to apply the packages service"; interface = { lib, ... }: { diff --git a/clanServices/sshd/README.md b/clanServices/sshd/README.md index 0535da100..88df4ab25 100644 --- a/clanServices/sshd/README.md +++ b/clanServices/sshd/README.md @@ -1,36 +1,91 @@ -The `sshd` Clan service manages SSH to make it easy to securely access your machines over the internet. The service uses `vars` to store the SSH host keys for each machine to ensure they remain stable across deployments. +# Clan service: sshd +What it does +- Generates and persists SSH host keys via `vars`. +- Optionally issues CA‑signed host certificates for servers. +- Installs the `server` CA public key into `clients` `known_hosts` for TOFU‑less verification. -`sshd` also generates SSH certificates for both servers and clients allowing for certificate-based authentication for SSH. -The service also disables password-based authentication over SSH, to access your machines you'll need to use public key authentication or certificate-based authentication. +When to use it +- Zero‑TOFU SSH for dynamic fleets: admins/CI can connect to frequently rebuilt hosts (e.g., server-1.example.com) without prompts or per‑host `known_hosts` churn. -## Usage +Roles +- Server: runs sshd, presents a CA‑signed host certificate for `.`. +- Client: trusts the CA for the given domains to verify servers’ certificates. + Tip: assign both roles to a machine if it should both present a cert and verify others. +Quick start (with host certificates) +Useful if you never want to get a prompt about trusting the ssh fingerprint. +```nix +{ + inventory.instances = { + sshd-with-certs = { + module = { name = "sshd"; input = "clan-core"; }; + # Servers present certificates for .example.com + roles.server.tags.all = { }; + roles.server.settings = { + certificate.searchDomains = [ "example.com" ]; + # Optional: also add RSA host keys + # hostKeys.rsa.enable = true; + }; + # Clients trust the CA for *.example.com + roles.client.tags.all = { }; + roles.client.settings = { + certificate.searchDomains = [ "example.com" ]; + }; + }; + }; +} +``` + +Basic: only add persistent host keys (ed25519), no certificates +Useful if you want to get an ssh "trust this server" prompt once and then never again. ```nix { inventory.instances = { - # By default this service only generates ed25519 host keys sshd-basic = { module = { name = "sshd"; input = "clan-core"; }; roles.server.tags.all = { }; - roles.client.tags.all = { }; - }; - - # Also generate RSA host keys for all servers - sshd-with-rsa = { - module = { - name = "sshd"; - input = "clan-core"; - }; - roles.server.tags.all = { }; - roles.server.settings = { - hostKeys.rsa.enable = true; - }; - roles.client.tags.all = { }; }; }; } ``` + +Example: selective trust per environment +Admins should trust only production; CI should trust prod and staging. Servers are reachable under both domains. +```nix +{ + inventory.instances = { + sshd-env-scoped = { + module = { name = "sshd"; input = "clan-core"; }; + + # Servers present certs for both prod and staging FQDNs + roles.server.tags.all = { }; + roles.server.settings = { + certificate.searchDomains = [ "prod.example.com" "staging.example.com" ]; + }; + + # Admin laptop: trust prod only + roles.client.machines."admin-laptop".settings = { + certificate.searchDomains = [ "prod.example.com" ]; + }; + + # CI runner: trust prod and staging + roles.client.machines."ci-runner-1".settings = { + certificate.searchDomains = [ "prod.example.com" "staging.example.com" ]; + }; + }; + }; +} +``` +- Admin -> server1.prod.example.com: zero‑TOFU (verified via cert). +- Admin -> server1.staging.example.com: falls back to TOFU (or is blocked by policy). +- CI -> either prod or staging: zero‑TOFU for both. +Note: server and client searchDomains don’t have to be identical; they only need to overlap for the hostnames you actually use. + +Notes +- Connect using a name that matches a cert principal (e.g., `server1.example.com`); wildcards are not allowed inside the certificate. +- CA private key stays in `vars` (not deployed); only the CA public key is distributed. +- Logins still require your user SSH keys on the server (passwords are disabled). \ No newline at end of file diff --git a/clanServices/sshd/default.nix b/clanServices/sshd/default.nix index 34a5de7f2..038f78994 100644 --- a/clanServices/sshd/default.nix +++ b/clanServices/sshd/default.nix @@ -10,13 +10,13 @@ manifest.readme = builtins.readFile ./README.md; roles.client = { + description = "Installs the SSH CA public key into known_hosts for the configured domains, so this machine can verify servers’ host certificates without TOFU prompts."; interface = { lib, ... }: { options.certificate = { searchDomains = lib.mkOption { type = lib.types.listOf lib.types.str; - default = [ ]; example = [ "mydomain.com" ]; description = '' List of domains to include in the certificate. @@ -38,7 +38,6 @@ ... }: { - clan.core.vars.generators.openssh-ca = lib.mkIf (settings.certificate.searchDomains != [ ]) { share = true; files.id_ed25519.deploy = false; @@ -64,11 +63,12 @@ }; roles.server = { + description = "Runs sshd with persistent host keys and (if certificate.searchDomains is set) a CA‑signed host certificate for ., enabling TOFU‑less verification by clients that trust the CA."; interface = { lib, ... }: { options = { - hostKeys.rsa.enable = lib.mkEnableOption "Generate RSA host key"; + hostKeys.rsa.enable = lib.mkEnableOption "Also generates an RSA host key"; certificate = { searchDomains = lib.mkOption { diff --git a/clanServices/syncthing/README.md b/clanServices/syncthing/README.md index 7d91d9ff2..4ac4ebc91 100644 --- a/clanServices/syncthing/README.md +++ b/clanServices/syncthing/README.md @@ -13,7 +13,7 @@ } ``` -Now the folder `~/syncthing/documents` will be shared with all your machines. +Now the folder `~/syncthing/documents` will be shared and kept in sync with all your machines. ## Documentation diff --git a/clanServices/syncthing/default.nix b/clanServices/syncthing/default.nix index 11d106b34..1c4fc2f5f 100644 --- a/clanServices/syncthing/default.nix +++ b/clanServices/syncthing/default.nix @@ -11,6 +11,7 @@ manifest.readme = builtins.readFile ./README.md; roles.peer = { + description = "A peer in the syncthing cluster that syncs files with other peers."; interface = { lib, ... }: { diff --git a/clanServices/tor/default.nix b/clanServices/tor/default.nix index b72d6a068..2e29471fd 100644 --- a/clanServices/tor/default.nix +++ b/clanServices/tor/default.nix @@ -2,13 +2,17 @@ { _class = "clan.service"; manifest.name = "clan-core/tor"; - manifest.description = "Onion routing, use Hidden services to connect your machines"; + manifest.description = "Part of the clan networking abstraction to define how to reach machines through the Tor network, if used has the lowest priority"; manifest.categories = [ "System" "Network" ]; roles.client = { + description = '' + Enables a continuosly running Tor proxy on the machine, allowing access to other machines via the Tor network. + If not enabled, a Tor proxy will be started automatically when required. + ''; perInstance = { ... @@ -31,6 +35,7 @@ }; roles.server = { + description = "Sets up a Tor onion service for the machine, thus making it reachable over Tor."; # interface = # { lib, ... }: # { diff --git a/clanServices/trusted-nix-caches/default.nix b/clanServices/trusted-nix-caches/default.nix index ec8bab13f..f7c6027e7 100644 --- a/clanServices/trusted-nix-caches/default.nix +++ b/clanServices/trusted-nix-caches/default.nix @@ -7,7 +7,7 @@ manifest.readme = builtins.readFile ./README.md; roles.default = { - + description = "Placeholder role to apply the trusted-nix-caches service"; perInstance = { ... }: { diff --git a/clanServices/users/default.nix b/clanServices/users/default.nix index a5684683c..6d720ee94 100644 --- a/clanServices/users/default.nix +++ b/clanServices/users/default.nix @@ -10,6 +10,7 @@ manifest.readme = builtins.readFile ./README.md; roles.default = { + description = "Placeholder role to apply the user service"; interface = { lib, ... }: { diff --git a/clanServices/wifi/README.md b/clanServices/wifi/README.md new file mode 100644 index 000000000..40473014d --- /dev/null +++ b/clanServices/wifi/README.md @@ -0,0 +1,21 @@ +This module allows you to pre-configure WiFi networks for automatic connection. +Each attribute in `settings.network` serves as an internal identifier, not the actual SSID. +After defining your networks, you will be prompted for the SSID and password for each one. + +This module leverages NetworkManager for managing connections. + +```nix +instances = { + wifi = { + module.name = "wifi"; + module.input = "clan-core"; + + roles.default = { + machines."jon" = { + settings.networks.home = { }; + settings.networks.work = { keyMgmt = "wpa-eap"; }; + }; + }; + }; +}; +``` diff --git a/clanServices/wifi/default.nix b/clanServices/wifi/default.nix index 1ea222040..736f0fdc6 100644 --- a/clanServices/wifi/default.nix +++ b/clanServices/wifi/default.nix @@ -9,8 +9,11 @@ in { _class = "clan.service"; manifest.name = "wifi"; + manifest.description = "Pre configure wifi networks to connect to"; + manifest.readme = builtins.readFile ./README.md; roles.default = { + description = "Placeholder role to apply the wifi service"; interface = { options.networks = lib.mkOption { type = lib.types.attrsOf ( @@ -42,7 +45,18 @@ in ) ); default = { }; - description = "Wifi networks to predefine"; + example = { + home = { }; + guest = { + autoConnect = false; + keyMgmt = "wpa-eap"; + }; + }; + description = '' + List of wifi networks to configure for connection. + Each attribute name is an internal identifier (not the SSID). + For each network, you will be prompted to enter the SSID and password as secrets. + ''; }; }; diff --git a/clanServices/wireguard/default.nix b/clanServices/wireguard/default.nix index 74317a8c0..9188660d3 100644 --- a/clanServices/wireguard/default.nix +++ b/clanServices/wireguard/default.nix @@ -146,6 +146,7 @@ in # Peer options and configuration roles.peer = { + description = "A peer that connects to one or more controllers."; interface = { lib, ... }: { @@ -261,6 +262,7 @@ in # Controller options and configuration roles.controller = { + description = "A controller that routes peer traffic. Must be publicly reachable."; interface = { lib, ... }: { diff --git a/clanServices/yggdrasil/default.nix b/clanServices/yggdrasil/default.nix index 8dc1132ff..349b156f2 100644 --- a/clanServices/yggdrasil/default.nix +++ b/clanServices/yggdrasil/default.nix @@ -5,6 +5,7 @@ manifest.description = "Yggdrasil encrypted IPv6 routing overlay network"; roles.default = { + description = "Placeholder role to apply the yggdrasil service"; interface = { lib, ... }: { diff --git a/clanServices/zerotier/default.nix b/clanServices/zerotier/default.nix index 7c65f4a76..89e075818 100644 --- a/clanServices/zerotier/default.nix +++ b/clanServices/zerotier/default.nix @@ -2,11 +2,12 @@ { _class = "clan.service"; manifest.name = "clan-core/zerotier"; - manifest.description = "Configuration of the secure and efficient Zerotier VPN"; + manifest.description = "Zerotier Mesh VPN Service for secure P2P networking between machines"; manifest.categories = [ "Utility" ]; manifest.readme = builtins.readFile ./README.md; roles.peer = { + description = "A peer that connects to your private Zerotier network."; perInstance = { instanceName, @@ -51,6 +52,7 @@ }; roles.moon = { + description = "A moon acts as a relay node to connect other nodes in the zerotier network that are not publicly reachable. Each moon must be publicly reachable."; interface = { lib, ... }: { @@ -101,6 +103,7 @@ }; roles.controller = { + description = "Manages network membership and is responsible for admitting new peers to your Zerotier network."; interface = { lib, ... }: {