diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..9b4eae4 --- /dev/null +++ b/flake.nix @@ -0,0 +1,42 @@ +{ + description = "Noisebell - door monitor system"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + agenix = { + url = "github:ryantm/agenix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + pi-service.url = "path:./pi/pi-service"; + remote.url = "path:./remote"; + remote.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = { self, nixpkgs, agenix, pi-service, remote }: { + nixosModules = remote.nixosModules; + + nixosConfigurations.pi = nixpkgs.lib.nixosSystem { + system = "aarch64-linux"; + modules = [ + agenix.nixosModules.default + (import ./pi/module.nix { + pkg = pi-service.packages.aarch64-linux.default; + rev = self.shortRev or "dirty"; + }) + ./pi/configuration.nix + ./pi/hardware-configuration.nix + ]; + }; + + nixosConfigurations.bootstrap = nixpkgs.lib.nixosSystem { + system = "aarch64-linux"; + modules = [ ./pi/bootstrap.nix ]; + }; + + devShells.x86_64-linux.default = let + pkgs = nixpkgs.legacyPackages.x86_64-linux; + in pkgs.mkShell { + packages = [ agenix.packages.x86_64-linux.default ]; + }; + }; +} diff --git a/pi/configuration.nix b/pi/configuration.nix index 9108d69..234d2fd 100644 --- a/pi/configuration.nix +++ b/pi/configuration.nix @@ -20,17 +20,16 @@ }; # Decrypted at runtime by agenix - age.secrets.tailscale-auth-key.file = ./secrets/tailscale-auth-key.age; - - age.secrets.api-key.file = ./secrets/api-key.age; - age.secrets.inbound-api-key.file = ./secrets/inbound-api-key.age; + age.secrets.tailscale-auth-key.file = ../secrets/tailscale-auth-key.age; + age.secrets.pi-to-cache-key.file = ../secrets/pi-to-cache-key.age; + age.secrets.cache-to-pi-key.file = ../secrets/cache-to-pi-key.age; services.noisebell = { enable = true; port = 80; endpointUrl = "https://noisebell.extremist.software/webhook"; - apiKeyFile = config.age.secrets.api-key.path; - inboundApiKeyFile = config.age.secrets.inbound-api-key.path; + apiKeyFile = config.age.secrets.pi-to-cache-key.path; + inboundApiKeyFile = config.age.secrets.cache-to-pi-key.path; }; nix.settings.experimental-features = [ "nix-command" "flakes" ]; diff --git a/pi/flake.nix b/pi/flake.nix index 1f5531a..dedd986 100644 --- a/pi/flake.nix +++ b/pi/flake.nix @@ -1,163 +1,13 @@ { - description = "NixOS configuration for noisebell Pi"; + description = "Noisebell Pi service"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; noisebell.url = "path:./pi-service"; - agenix = { - url = "github:ryantm/agenix"; - inputs.nixpkgs.follows = "nixpkgs"; - }; }; - outputs = { self, nixpkgs, noisebell, agenix }: - let - nixosModule = { config, lib, pkgs, ... }: - let - cfg = config.services.noisebell; - in - { - options.services.noisebell = { - enable = lib.mkEnableOption "noisebell GPIO door monitor"; - - gpioPin = lib.mkOption { - type = lib.types.ints.unsigned; - default = 17; - description = "GPIO pin number to monitor."; - }; - - debounceSecs = lib.mkOption { - type = lib.types.ints.positive; - default = 5; - description = "Debounce delay in seconds."; - }; - - port = lib.mkOption { - type = lib.types.port; - default = 8080; - description = "HTTP port for the status endpoint."; - }; - - endpointUrl = lib.mkOption { - type = lib.types.str; - description = "Webhook endpoint URL to POST state changes to."; - }; - - apiKeyFile = lib.mkOption { - type = lib.types.path; - description = "Path to a file containing the outbound API key for the cache endpoint."; - }; - - inboundApiKeyFile = lib.mkOption { - type = lib.types.path; - description = "Path to a file containing the inbound API key for authenticating GET requests."; - }; - - retryAttempts = lib.mkOption { - type = lib.types.ints.unsigned; - default = 3; - description = "Number of retries after a failed webhook POST."; - }; - - retryBaseDelaySecs = lib.mkOption { - type = lib.types.ints.positive; - default = 1; - description = "Base delay in seconds for exponential backoff between retries."; - }; - - httpTimeoutSecs = lib.mkOption { - type = lib.types.ints.positive; - default = 10; - description = "Timeout in seconds for outbound HTTP requests to the webhook endpoint."; - }; - - bindAddress = lib.mkOption { - type = lib.types.str; - default = "0.0.0.0"; - description = "Address to bind the HTTP server to."; - }; - - activeLow = lib.mkOption { - type = lib.types.bool; - default = true; - description = "Whether a low GPIO level means open. Set to false if your sensor wiring is inverted."; - }; - - restartDelaySecs = lib.mkOption { - type = lib.types.ints.positive; - default = 5; - description = "Seconds to wait before systemd restarts the service on failure."; - }; - - watchdogSecs = lib.mkOption { - type = lib.types.ints.positive; - default = 30; - description = "Watchdog timeout in seconds. The service is restarted if it fails to notify systemd within this interval."; - }; - }; - - config = lib.mkIf cfg.enable { - systemd.services.noisebell = let - bin = "${noisebell.packages.aarch64-linux.default}/bin/noisebell"; - in { - description = "Noisebell GPIO door monitor"; - wantedBy = [ "multi-user.target" ]; - after = [ "network-online.target" "tailscaled.service" ]; - wants = [ "network-online.target" ]; - - environment = { - NOISEBELL_GPIO_PIN = toString cfg.gpioPin; - NOISEBELL_DEBOUNCE_SECS = toString cfg.debounceSecs; - NOISEBELL_PORT = toString cfg.port; - NOISEBELL_RETRY_ATTEMPTS = toString cfg.retryAttempts; - NOISEBELL_RETRY_BASE_DELAY_SECS = toString cfg.retryBaseDelaySecs; - NOISEBELL_HTTP_TIMEOUT_SECS = toString cfg.httpTimeoutSecs; - NOISEBELL_ENDPOINT_URL = cfg.endpointUrl; - NOISEBELL_BIND_ADDRESS = cfg.bindAddress; - NOISEBELL_ACTIVE_LOW = if cfg.activeLow then "true" else "false"; - NOISEBELL_COMMIT = self.shortRev or "dirty"; - RUST_LOG = "info"; - }; - - script = '' - export NOISEBELL_API_KEY="$(cat ${cfg.apiKeyFile})" - export NOISEBELL_INBOUND_API_KEY="$(cat ${cfg.inboundApiKeyFile})" - exec ${bin} - ''; - - serviceConfig = { - Type = "notify"; - NotifyAccess = "all"; - WatchdogSec = cfg.watchdogSecs; - Restart = "on-failure"; - RestartSec = cfg.restartDelaySecs; - }; - }; - }; - }; - in - { - nixosModules.default = nixosModule; - - nixosConfigurations.pi = nixpkgs.lib.nixosSystem { - system = "aarch64-linux"; - modules = [ - agenix.nixosModules.default - nixosModule - ./configuration.nix - ./hardware-configuration.nix - ]; - }; - - nixosConfigurations.bootstrap = nixpkgs.lib.nixosSystem { - system = "aarch64-linux"; - modules = [ ./bootstrap.nix ]; - }; - - devShells.x86_64-linux.default = let - pkgs = nixpkgs.legacyPackages.x86_64-linux; - in pkgs.mkShell { - packages = [ agenix.packages.x86_64-linux.default ]; - }; - }; + outputs = { self, nixpkgs, noisebell }: { + packages = noisebell.packages; + nixosModules.default = import ./module.nix; + }; } diff --git a/pi/module.nix b/pi/module.nix new file mode 100644 index 0000000..b539712 --- /dev/null +++ b/pi/module.nix @@ -0,0 +1,123 @@ +{ pkg, rev }: +{ config, lib, ... }: +let + cfg = config.services.noisebell; + bin = "${pkg}/bin/noisebell"; +in +{ + options.services.noisebell = { + enable = lib.mkEnableOption "noisebell GPIO door monitor"; + + gpioPin = lib.mkOption { + type = lib.types.ints.unsigned; + default = 17; + description = "GPIO pin number to monitor."; + }; + + debounceSecs = lib.mkOption { + type = lib.types.ints.positive; + default = 5; + description = "Debounce delay in seconds."; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 8080; + description = "HTTP port for the status endpoint."; + }; + + endpointUrl = lib.mkOption { + type = lib.types.str; + description = "Webhook endpoint URL to POST state changes to."; + }; + + apiKeyFile = lib.mkOption { + type = lib.types.path; + description = "Path to a file containing the outbound API key for the cache endpoint."; + }; + + inboundApiKeyFile = lib.mkOption { + type = lib.types.path; + description = "Path to a file containing the inbound API key for authenticating GET requests."; + }; + + retryAttempts = lib.mkOption { + type = lib.types.ints.unsigned; + default = 3; + description = "Number of retries after a failed webhook POST."; + }; + + retryBaseDelaySecs = lib.mkOption { + type = lib.types.ints.positive; + default = 1; + description = "Base delay in seconds for exponential backoff between retries."; + }; + + httpTimeoutSecs = lib.mkOption { + type = lib.types.ints.positive; + default = 10; + description = "Timeout in seconds for outbound HTTP requests to the webhook endpoint."; + }; + + bindAddress = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0"; + description = "Address to bind the HTTP server to."; + }; + + activeLow = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether a low GPIO level means open. Set to false if your sensor wiring is inverted."; + }; + + restartDelaySecs = lib.mkOption { + type = lib.types.ints.positive; + default = 5; + description = "Seconds to wait before systemd restarts the service on failure."; + }; + + watchdogSecs = lib.mkOption { + type = lib.types.ints.positive; + default = 30; + description = "Watchdog timeout in seconds. The service is restarted if it fails to notify systemd within this interval."; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.noisebell = { + description = "Noisebell GPIO door monitor"; + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" "tailscaled.service" ]; + wants = [ "network-online.target" ]; + + environment = { + NOISEBELL_GPIO_PIN = toString cfg.gpioPin; + NOISEBELL_DEBOUNCE_SECS = toString cfg.debounceSecs; + NOISEBELL_PORT = toString cfg.port; + NOISEBELL_RETRY_ATTEMPTS = toString cfg.retryAttempts; + NOISEBELL_RETRY_BASE_DELAY_SECS = toString cfg.retryBaseDelaySecs; + NOISEBELL_HTTP_TIMEOUT_SECS = toString cfg.httpTimeoutSecs; + NOISEBELL_ENDPOINT_URL = cfg.endpointUrl; + NOISEBELL_BIND_ADDRESS = cfg.bindAddress; + NOISEBELL_ACTIVE_LOW = if cfg.activeLow then "true" else "false"; + NOISEBELL_COMMIT = rev; + RUST_LOG = "info"; + }; + + script = '' + export NOISEBELL_API_KEY="$(cat ${cfg.apiKeyFile})" + export NOISEBELL_INBOUND_API_KEY="$(cat ${cfg.inboundApiKeyFile})" + exec ${bin} + ''; + + serviceConfig = { + Type = "notify"; + NotifyAccess = "all"; + WatchdogSec = cfg.watchdogSecs; + Restart = "on-failure"; + RestartSec = cfg.restartDelaySecs; + }; + }; + }; +} diff --git a/pi/secrets/api-key.age b/pi/secrets/api-key.age deleted file mode 100644 index 3fa347f..0000000 --- a/pi/secrets/api-key.age +++ /dev/null @@ -1,7 +0,0 @@ -age-encryption.org/v1 --> ssh-ed25519 Ziw7aw KeT7ZRx4LnoXw4TzbqAZ6U4tc74XdTGOoFvF3p1cwwc -FMlEdlMjMGfMJByOBYaY2m0Zr6eGOVm/wYCBkUMUUmg --> ssh-ed25519 jcT/MQ A0b/6ZhKbqAk8Wx/jCcZsPSygXDyll+f6v8GmM3v1T8 -BE4MTRbbXBFGVYFpKdbD8EzYSLCZxxo3UX9hxrlX9N8 ---- rsyZbWLKxYWHT71Yg0nVryroF8QCjmVvGsYidVM2LP0 -lh^M5ʏ'QO'sxU[[f11N"KE ߬bb*FhPus \ No newline at end of file diff --git a/pi/secrets/inbound-api-key.age b/pi/secrets/inbound-api-key.age deleted file mode 100644 index c062f50..0000000 --- a/pi/secrets/inbound-api-key.age +++ /dev/null @@ -1,7 +0,0 @@ -age-encryption.org/v1 --> ssh-ed25519 Ziw7aw Gykk5VLi/S/Lbf0YDFG7rax5WCIwjmfJMpe9r8XK4T0 -nbggjY+m082BdKzUYucVFrwu08ziQtnY3BMXQ2dwMJo --> ssh-ed25519 jcT/MQ OmtLLN7acSjwZrbYpc8eCOfgw1ZelHk2OWusj3kukyM -DoW2NvsOBLHhfa7NYEZP92nm1fAY7gYjM+/WHIPLD88 ---- HuvJEBrp5h5PXzlruLVL9pIKmlu8VBK17spsb/gco2E -`%(OtNA^ؽz %i'cGK3v0bZ0߇П\ٔ2gIV+91AT.VMZ \ No newline at end of file diff --git a/pi/secrets/secrets.nix b/pi/secrets/secrets.nix deleted file mode 100644 index 68edf6f..0000000 --- a/pi/secrets/secrets.nix +++ /dev/null @@ -1,9 +0,0 @@ -let - jet = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu"; - pi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKmJFZAVJUjDziQ7XytVhQIv+Pm9hnbVU0flxZK17K5E"; -in -{ - "api-key.age".publicKeys = [ jet pi ]; - "inbound-api-key.age".publicKeys = [ jet pi ]; - "tailscale-auth-key.age".publicKeys = [ jet pi ]; -} diff --git a/pi/secrets/tailscale-auth-key.age b/pi/secrets/tailscale-auth-key.age deleted file mode 100644 index f479417..0000000 --- a/pi/secrets/tailscale-auth-key.age +++ /dev/null @@ -1,7 +0,0 @@ -age-encryption.org/v1 --> ssh-ed25519 Ziw7aw VR4dTYsFGIL0wGdikj1l034Uxg+C06qn9xwNrDfrYGg -FSp36eUOslycLme0hQalv/6oS6tQmJbXbIZ9ezaCznc --> ssh-ed25519 jcT/MQ 65epjpYA8RNKGxgyEbASm7aZaAdaj88FH2D3/yWHXH0 -rUhjanumw1mr/YkpMYfVamegTKNMXkQS/CifOTvQHAA ---- nyitQGsf/UyVmRaqMXOBXYRhbeViymeHhCWZkKqtfNo -x!f#ٌCZGBy%A/~}JA(ruLgDy2Rv%eP (q \ No newline at end of file diff --git a/secrets/secrets.nix b/secrets/secrets.nix new file mode 100644 index 0000000..f535e36 --- /dev/null +++ b/secrets/secrets.nix @@ -0,0 +1,12 @@ +let + jet = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu"; + pi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKmJFZAVJUjDziQ7XytVhQIv+Pm9hnbVU0flxZK17K5E"; + server = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAING219cDKTDLaZefmqvOHfXvYloA/ErsCGE0pM022vlB"; +in +{ + "pi-to-cache-key.age".publicKeys = [ jet pi server ]; + "cache-to-pi-key.age".publicKeys = [ jet pi server ]; + "tailscale-auth-key.age".publicKeys = [ jet pi ]; + "discord-token.age".publicKeys = [ jet server ]; + "discord-webhook-secret.age".publicKeys = [ jet server ]; +}