{ config, lib, pkgs, ... }: let cfg = config.nodeiwest; tailscaleOpenbaoCfg = cfg.tailscale.openbao; in { options.nodeiwest.tailscale.openbao = { enable = lib.mkEnableOption "fetching the Tailscale auth key from OpenBao"; namespace = lib.mkOption { type = lib.types.str; default = "it"; description = "OpenBao namespace used when fetching the Tailscale auth key."; }; authPath = lib.mkOption { type = lib.types.str; default = "auth/approle"; description = "OpenBao auth mount path used by the AppRole login."; }; secretPath = lib.mkOption { type = lib.types.str; default = "tailscale"; description = "OpenBao secret path containing the Tailscale auth key."; }; field = lib.mkOption { type = lib.types.str; default = "CLIENT_SECRET"; description = "Field in the OpenBao secret that contains the Tailscale auth key."; }; renderedAuthKeyFile = lib.mkOption { type = lib.types.str; default = "/run/nodeiwest/tailscale-auth-key"; description = "Runtime file rendered by OpenBao Agent and consumed by Tailscale autoconnect."; }; approle = { roleIdFile = lib.mkOption { type = lib.types.str; default = "/var/lib/nodeiwest/openbao-approle-role-id"; description = "Root-only file containing the OpenBao AppRole role_id."; }; secretIdFile = lib.mkOption { type = lib.types.str; default = "/var/lib/nodeiwest/openbao-approle-secret-id"; description = "Root-only file containing the OpenBao AppRole secret_id."; }; }; }; config = { systemd.tmpfiles.rules = [ "d /var/lib/nodeiwest 0700 root root - -" "d /run/nodeiwest 0700 root root - -" ]; services.tailscale = { enable = true; openFirewall = true; extraUpFlags = lib.optionals tailscaleOpenbaoCfg.enable [ "--ssh" ]; authKeyFile = if tailscaleOpenbaoCfg.enable then tailscaleOpenbaoCfg.renderedAuthKeyFile else null; }; services.vault-agent.instances.tailscale = lib.mkIf tailscaleOpenbaoCfg.enable { package = pkgs.openbao; settings = { vault.address = cfg.openbao.address; auto_auth = { method = [ { type = "approle"; mount_path = tailscaleOpenbaoCfg.authPath; namespace = tailscaleOpenbaoCfg.namespace; config = { role_id_file_path = tailscaleOpenbaoCfg.approle.roleIdFile; secret_id_file_path = tailscaleOpenbaoCfg.approle.secretIdFile; remove_secret_id_file_after_reading = false; }; } ]; }; template = [ { contents = ''{{- with secret "${tailscaleOpenbaoCfg.secretPath}" -}}{{- if .Data.data -}}{{ index .Data.data "${tailscaleOpenbaoCfg.field}" }}{{- else -}}{{ index .Data "${tailscaleOpenbaoCfg.field}" }}{{- end -}}{{- end -}}''; destination = tailscaleOpenbaoCfg.renderedAuthKeyFile; perms = "0400"; } ]; }; }; systemd.services.vault-agent-tailscale = lib.mkIf tailscaleOpenbaoCfg.enable { wants = [ "network-online.target" ]; after = [ "network-online.target" ]; serviceConfig.Environment = [ "BAO_NAMESPACE=${tailscaleOpenbaoCfg.namespace}" ]; }; systemd.services.nodeiwest-tailscale-authkey-ready = lib.mkIf tailscaleOpenbaoCfg.enable { description = "Wait for the Tailscale auth key rendered by OpenBao Agent"; after = [ "vault-agent-tailscale.service" ]; requires = [ "vault-agent-tailscale.service" ]; before = [ "tailscaled-autoconnect.service" ]; requiredBy = [ "tailscaled-autoconnect.service" ]; path = [ pkgs.coreutils ]; serviceConfig = { Type = "oneshot"; }; script = '' set -euo pipefail for _ in $(seq 1 60); do if [ -s ${lib.escapeShellArg tailscaleOpenbaoCfg.renderedAuthKeyFile} ]; then exit 0 fi sleep 1 done echo "Timed out waiting for rendered Tailscale auth key at ${tailscaleOpenbaoCfg.renderedAuthKeyFile}" >&2 exit 1 ''; }; systemd.services.tailscaled-autoconnect = lib.mkIf tailscaleOpenbaoCfg.enable { after = [ "vault-agent-tailscale.service" "nodeiwest-tailscale-authkey-ready.service" ]; requires = [ "vault-agent-tailscale.service" "nodeiwest-tailscale-authkey-ready.service" ]; serviceConfig.ExecStartPre = [ "${lib.getExe' pkgs.coreutils "test"} -s ${tailscaleOpenbaoCfg.renderedAuthKeyFile}" ]; }; assertions = [ { assertion = (!tailscaleOpenbaoCfg.enable) || (tailscaleOpenbaoCfg.approle.roleIdFile != "" && tailscaleOpenbaoCfg.approle.secretIdFile != ""); message = "AppRole roleIdFile and secretIdFile must be set when OpenBao-backed Tailscale enrollment is enabled."; } ]; }; }