153 lines
5.8 KiB
Nix
153 lines
5.8 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
|
|
let
|
|
cfg = config.services.gebos.secrets;
|
|
secretsFile = ../secrets/secrets.yaml;
|
|
hasSecrets = builtins.pathExists secretsFile;
|
|
|
|
# Map from logical group → list of secret keys in secrets.yaml.
|
|
# Keys are flat (e.g. "supabase_jwt_secret") so .sops.yaml regex rules can
|
|
# filter per-host without nested-path footguns.
|
|
supabaseKeys = [
|
|
"supabase_postgres_password"
|
|
"supabase_jwt_secret"
|
|
"supabase_anon_key"
|
|
"supabase_service_role_key"
|
|
"supabase_dashboard_password"
|
|
];
|
|
ingesterKeys = [ "ingester_postgres_password" ];
|
|
postgresAdminKeys = [ "postgres_admin_password" ];
|
|
emqxKeys = [
|
|
"emqx_ingester_password"
|
|
"emqx_admin_password"
|
|
"emqx_device_password"
|
|
];
|
|
|
|
# EMQX authn bootstrap CSV (bootstrap_type = plain). Seeds the broker's
|
|
# built-in auth database on first start. Broker users:
|
|
# ingester — the Go ingester; SUBSCRIBE the whole device tree (see the ACL
|
|
# in gebos-emqx.nix).
|
|
# admin — operational break-glass; is_superuser bypasses the ACL.
|
|
# bender — shared device user for the whole fleet (Option A). Tenant
|
|
# isolation is enforced downstream by the ingester's IMSI→tenant
|
|
# registry, NOT at the broker. TODO(Option B): per-device users
|
|
# (username == IMSI) — see the ACL comment in gebos-emqx.nix.
|
|
#
|
|
# Plaintext passwords are acceptable here because the file is sops-encrypted
|
|
# at rest and rendered 0400 into a tmpfs at runtime; passwords must not
|
|
# contain commas (CSV).
|
|
emqxUsersCsv = ingesterPassword: adminPassword: devicePassword: ''
|
|
user_id,password,is_superuser
|
|
ingester,${ingesterPassword},false
|
|
admin,${adminPassword},true
|
|
bender,${devicePassword},false
|
|
'';
|
|
|
|
# Render an env file body from a list of (key, envVarName) pairs.
|
|
envBody = pairs: lib.concatStringsSep "\n" (map
|
|
({ key, var }: "${var}=${config.sops.placeholder.${key}}")
|
|
pairs);
|
|
|
|
enabledPairs =
|
|
lib.optionals cfg.supabase [
|
|
{ key = "supabase_postgres_password"; var = "POSTGRES_PASSWORD"; }
|
|
{ key = "supabase_jwt_secret"; var = "JWT_SECRET"; }
|
|
{ key = "supabase_anon_key"; var = "ANON_KEY"; }
|
|
{ key = "supabase_service_role_key"; var = "SERVICE_ROLE_KEY"; }
|
|
{ key = "supabase_dashboard_password"; var = "DASHBOARD_PASSWORD"; }
|
|
]
|
|
++ lib.optionals cfg.ingester [
|
|
{ key = "ingester_postgres_password"; var = "GEBOS_POSTGRES_PASSWORD"; }
|
|
]
|
|
++ lib.optionals cfg.postgresAdmin [
|
|
{ key = "postgres_admin_password"; var = "POSTGRES_ADMIN_PASSWORD"; }
|
|
];
|
|
|
|
enabledKeys =
|
|
lib.optionals cfg.supabase supabaseKeys
|
|
++ lib.optionals cfg.ingester ingesterKeys
|
|
++ lib.optionals cfg.postgresAdmin postgresAdminKeys
|
|
++ lib.optionals cfg.emqx emqxKeys;
|
|
in
|
|
{
|
|
options.services.gebos.secrets = {
|
|
supabase = lib.mkEnableOption ''
|
|
Supabase compose stack secrets: POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY,
|
|
SERVICE_ROLE_KEY, DASHBOARD_PASSWORD.'';
|
|
|
|
ingester = lib.mkEnableOption ''
|
|
Ingester DB password: GEBOS_POSTGRES_PASSWORD (for the gebos_ingest role).'';
|
|
|
|
postgresAdmin = lib.mkEnableOption ''
|
|
Postgres bootstrap admin password applied via a one-shot ALTER ROLE unit.'';
|
|
|
|
emqx = lib.mkEnableOption ''
|
|
EMQX broker secrets: the authn bootstrap users.csv (ingester + admin +
|
|
device MQTT users).'';
|
|
|
|
emqxBootstrapUsersFile = lib.mkOption {
|
|
type = lib.types.path;
|
|
readOnly = true;
|
|
description = ''
|
|
Path of the rendered EMQX authn bootstrap users.csv consumed by the
|
|
gebos-emqx broker. Resolves to the sops-templated secret when sops
|
|
material is present and the emqx toggle is on, else to a baked-in
|
|
dev-defaults file (so first-boot eval and local VMs work).
|
|
'';
|
|
default =
|
|
if hasSecrets && cfg.emqx
|
|
then config.sops.templates."emqx-users.csv".path
|
|
else "${pkgs.writeText "emqx-users-dev.csv"
|
|
(emqxUsersCsv "dev-ingester" "dev-admin" "dev-device")}";
|
|
};
|
|
|
|
envFile = lib.mkOption {
|
|
type = lib.types.path;
|
|
readOnly = true;
|
|
description = ''
|
|
Path of the rendered env file consumed by service units' EnvironmentFile=.
|
|
Resolves to the sops-templated file under /run/secrets when sops material
|
|
is present, or to a baked-in dev-defaults file otherwise (so first-boot
|
|
eval and local NixOS VMs work without any secrets bootstrap).
|
|
'';
|
|
default =
|
|
if hasSecrets
|
|
then config.sops.templates."gebos-env".path
|
|
else "${pkgs.writeText "gebos-env-dev-defaults" (
|
|
# Dev fallback. Never used in prod — overridden once secrets.yaml exists.
|
|
lib.concatStringsSep "\n" (map ({ var, ... }: "${var}=dev-${var}") enabledPairs)
|
|
)}";
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf hasSecrets {
|
|
sops = {
|
|
defaultSopsFile = secretsFile;
|
|
# Derive each host's age identity from its existing ssh host key.
|
|
# No extra key material to provision — ssh-to-age handles conversion.
|
|
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
|
|
|
|
secrets = lib.listToAttrs (map
|
|
(key: { name = key; value = { }; })
|
|
enabledKeys);
|
|
|
|
templates."gebos-env" = lib.mkIf (enabledPairs != []) {
|
|
content = envBody enabledPairs;
|
|
mode = "0400";
|
|
owner = "root";
|
|
};
|
|
|
|
# Rendered as a standalone file — the gebos-emqx preStart copies it into
|
|
# the broker state dir, chowned to the container's emqx uid.
|
|
templates."emqx-users.csv" = lib.mkIf cfg.emqx {
|
|
content = emqxUsersCsv
|
|
config.sops.placeholder.emqx_ingester_password
|
|
config.sops.placeholder.emqx_admin_password
|
|
config.sops.placeholder.emqx_device_password;
|
|
mode = "0400";
|
|
owner = "root";
|
|
};
|
|
};
|
|
};
|
|
}
|