Add auth2api

This commit is contained in:
muon 2026-06-01 10:28:16 +00:00
parent b6318d9c4d
commit 28bb03187d
9 changed files with 335 additions and 11 deletions

View file

@ -0,0 +1,206 @@
{
pkgs,
lib,
config,
sources,
...
}:
let
cfg = config.mods.server.auth2api;
auth2api-pkg = pkgs.callPackage ../../../pkgs/auth2api/package.nix { inherit sources; };
# Wrap the binary so --config is always implicit, whether run by the
# service, by the user for --login, or any other invocation.
auth2api-wrapped = pkgs.symlinkJoin {
name = "auth2api-wrapped";
paths = [ auth2api-pkg ];
buildInputs = [ pkgs.makeWrapper ];
postBuild = ''
wrapProgram $out/bin/auth2api \
--add-flags "--config=${cfg.stateDir}/config.yaml" \
--run "umask 027"
'';
};
# Nix-managed config template — read-only in /nix/store.
# systemd-tmpfiles copies it into stateDir on first boot (C rule),
# so auth2api can write back to it (e.g. API key auto-generation)
# and the user can hand-edit it freely without rebuilds overwriting it.
configTemplate = pkgs.writeText "auth2api-config-template.yaml" ''
host: "${cfg.host}"
port: ${toString cfg.port}
# Directory where OAuth token files are stored (written by --login).
auth-dir: "${cfg.stateDir}/tokens"
# Empty = unauthenticated mode (safe when binding to 127.0.0.1).
api-keys: []
body-limit: "${cfg.bodyLimit}"
timeouts:
messages-ms: ${toString cfg.timeouts.messagesMs}
stream-messages-ms: ${toString cfg.timeouts.streamMessagesMs}
count-tokens-ms: ${toString cfg.timeouts.countTokensMs}
stats:
enabled: true
# off | errors | verbose
debug: "${cfg.debug}"
cloaking:
cli-version: "2.1.88"
entrypoint: "cli"
'';
in
with lib; {
options.mods.server.auth2api = {
enable = mkEnableOption "auth2api OAuth-to-API proxy";
port = mkOption {
type = types.port;
default = 8317;
description = "Port auth2api listens on.";
};
host = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Bind address. Defaults to localhost safe with no API key.";
};
stateDir = mkOption {
type = types.str;
default = "/var/lib/auth2api";
description = ''
State directory. Contains config.yaml (live, writable) and
tokens/ (OAuth token files written by --login).
'';
};
bodyLimit = mkOption {
type = types.str;
default = "200mb";
description = "Maximum JSON request body size.";
};
timeouts = {
messagesMs = mkOption {
type = types.int;
default = 120000;
description = "Non-streaming /v1/messages timeout (ms).";
};
streamMessagesMs = mkOption {
type = types.int;
default = 600000;
description = "Streaming /v1/messages timeout (ms).";
};
countTokensMs = mkOption {
type = types.int;
default = 30000;
description = "/v1/messages/count_tokens timeout (ms).";
};
};
debug = mkOption {
type = types.enum [ "off" "errors" "verbose" ];
default = "off";
description = "Log level: off | errors | verbose.";
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Open the firewall for the auth2api port.";
};
};
config = mkIf cfg.enable {
# tmpfiles runs as root before any service starts.
# 'd' creates the directory if missing; 'C' copies the template into
# config.yaml only if that file doesn't already exist — so hand-edits
# and auth2api's own writes are never clobbered on rebuild.
systemd.tmpfiles.rules = [
# stateDir: world-traversable so any local user can reach files inside
"d '${cfg.stateDir}' 0755 auth2api auth2api - -"
"z '${cfg.stateDir}' 0755 auth2api auth2api - -"
# tokens: group-writable so members of the auth2api group can run --login
"d '${cfg.stateDir}/tokens' 0770 auth2api auth2api - -"
"z '${cfg.stateDir}/tokens' 0770 auth2api auth2api - -"
# config.yaml: contains no secrets (empty api-keys), safe to be world-readable
"C '${cfg.stateDir}/config.yaml' 0644 auth2api auth2api - ${configTemplate}"
"z '${cfg.stateDir}/config.yaml' 0644 auth2api auth2api - -"
];
# auth2api.path watches the tokens directory and activates auth2api.service
# the moment a token file appears (i.e. after --login has been run).
# This means the service is never started — and never fails — during
# activation on a fresh machine, so nixos-rebuild always succeeds cleanly.
systemd.paths.auth2api = {
description = "Watch for auth2api OAuth tokens";
wantedBy = [ "multi-user.target" ];
after = [ "systemd-tmpfiles-setup.service" ];
pathConfig = {
# Trigger when any file is created inside the tokens directory.
DirectoryNotEmpty = "${cfg.stateDir}/tokens";
# Re-trigger if the service stops (e.g. after a logout / token removal).
Unit = "auth2api.service";
};
};
systemd.services.auth2api = {
description = "auth2api OAuth-to-API proxy";
# Started by the path unit, not directly by multi-user.target.
# Must wait for tmpfiles so config.yaml is always present before start.
after = [ "network.target" "systemd-tmpfiles-setup.service" ];
requires = [ "systemd-tmpfiles-setup.service" ];
serviceConfig = {
Type = "simple";
ExecStart = "${auth2api-wrapped}/bin/auth2api";
Restart = "on-failure";
RestartSec = "10s";
User = "auth2api";
Group = "auth2api";
ReadWritePaths = [ cfg.stateDir ];
# Hardening
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
PrivateDevices = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
RestrictNamespaces = true;
LockPersonality = true;
MemoryDenyWriteExecute = false; # Node.js JIT requires W+X pages
RestrictRealtime = true;
RestrictSUIDSGID = true;
};
};
users.users.auth2api = {
isSystemUser = true;
group = "auth2api";
home = cfg.stateDir;
description = "auth2api service user";
};
users.groups.auth2api = { };
# Allow the primary user to run --login and read/write tokens.
users.users.${config.mods.user.name}.extraGroups = [ "auth2api" ];
# Make the wrapped binary available in PATH for `auth2api --login` etc.
environment.systemPackages = [ auth2api-wrapped ];
networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
};
}

View file

@ -30,5 +30,6 @@
./audio.nix
./atuin.nix
./murmur.nix
./auth2api.nix
];
}

View file

@ -5,8 +5,7 @@ lemmy-password: ENC[AES256_GCM,data:VVPbhW6l+VYSUfmlySPSwITwonKQHaIY,iv:XcwM7Sz2
sops-key: ENC[AES256_GCM,data:CT2FJnxRV0nVccCS+bofjIDqoVnJKMs63BVdmC4KEXEJAdsiyINTNJ+19aMqIkr2eosvXX1+nvV6oeBvNv1uN9xCrrzu4Qj0yRA=,iv:w9Fp68KK8hnUirlDGOYKSQwlfp3OBWU4XWqliZn/apc=,tag:XZdhC65WpcazSol1mbdp5A==,type:str]
sops:
age:
- recipient: age1m97a3eptxwpdd7h5kkqe9gkmhg6rquc64qjmlsfqfhfqv8q72crqrylhgc
enc: |
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4bUg1Z1JBcmRldDIzN2Zt
Ky9LOTVBK0IzdE1UUFBXci94R0x1bitjT2hjCjA1NC9wMzNHZkorZllIaFpNMVlm
@ -14,8 +13,8 @@ sops:
eWlTRmEzYVpQdENiNUMxaWJta0NjcVEKx3togykPGYRNGgJR6fl9cDbJKiLWHjA9
XujrttnDTwNCCZENn/E4BABC4XecW8IqSsUmJW6GwZzYJu+4rNTSwA==
-----END AGE ENCRYPTED FILE-----
- recipient: age1v4s4hg7u3vjjkarvrk7v6ev7w3wja2r5xm7f4t06culw3fuq7qns8sfju7
enc: |
recipient: age1m97a3eptxwpdd7h5kkqe9gkmhg6rquc64qjmlsfqfhfqv8q72crqrylhgc
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWWE5tU0ltaTJscUVQSDBy
WHRDb2FTRVFtZ2s2eGRjb21ncU1HNkx3RmhRClMwQ0E1cCt1SmtoYi9TWExXdVdX
@ -23,8 +22,8 @@ sops:
Qm5yVjBNc1l6VFQ4OGJsWXdsWUIyNFkKksIW0x8RxTdaw9YR4y+84VrYnfVZz2js
qz1RG4TXs9NRcm8fGGa/ZYZZN72h/l0WY+fayZ+ZUaHD43tHFisoYg==
-----END AGE ENCRYPTED FILE-----
- recipient: age1n7qz2w3hkf7fcdv92kxw9k6uef487na2tlc87486rcjwj8lyfuws5q46gn
enc: |
recipient: age1v4s4hg7u3vjjkarvrk7v6ev7w3wja2r5xm7f4t06culw3fuq7qns8sfju7
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1L29jY3lNeU8xeE03VUFu
MWJyczNxZFJHSG82c0p3OEtBOThqaE8xTFMwCm9KemZJMjBOQ0I1TU9Qd2IvMGVU
@ -32,8 +31,8 @@ sops:
UXp0a3AwM0hvbG1jeEZIMlViYU9ZWTgKKJ2YL6Q2LyR9x4Oqt5qWiyL7f4wAWrqw
FTY5r2unI7YdIFtzmbjIAqv/4qqy62Th8EEsqAZUcL/YBcuNIiyg6Q==
-----END AGE ENCRYPTED FILE-----
- recipient: age1mgjhkqy9x27gv2t2xvq46dxcajkr9c8zes7rr3dj0ac7md2j6vas43dftp
enc: |
recipient: age1n7qz2w3hkf7fcdv92kxw9k6uef487na2tlc87486rcjwj8lyfuws5q46gn
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJOUhRY0RtaUhNaHZZTHk3
QVF4cXV2Lzc3d1RRM2pzMXBBQU95endLRFFZCkdMVVlkV3VzSnRyRHpROHlReUdJ
@ -41,7 +40,8 @@ sops:
VFdIbUg1WjlldFFNbGx3dytQNXBsMDgKuU/86fojKVJ5X8+9OIf3k7ud6bujjyFI
HQoONJgXGoQJtkPsmJbMUuMjo/znK+tdCd/uAwxK1Nk670NVxGmJYA==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2025-08-04T09:14:07Z"
mac: ENC[AES256_GCM,data:Qu5kuhV2c31S9l01e7IWCrjLKU8eBepK42eR1nEvPpoHqXxbIT3vcDbxJdcn2Ay6Z4pARYqmHctVDOCiilxFyYfzF8mP91u6NhsZC5kHMdP7GI5Pl5FXSCMxQbbBWgXxJXruq/NkrlrLnFTWyzBRLa4wTBZdDMZ2CGo6jLi7G0o=,iv:q3WG536FkLpYEp8AAcW0agYq6rDIhzzt47l7grDvGyo=,tag:T5msy2cSZ/bZ9HvbxTw0Rg==,type:str]
recipient: age1mgjhkqy9x27gv2t2xvq46dxcajkr9c8zes7rr3dj0ac7md2j6vas43dftp
lastmodified: "2026-06-01T08:59:15Z"
mac: ENC[AES256_GCM,data:DtHxyzG5d+USGmfFg6vYzk5NIfHPSq/5CKTBAXuipW1pD4WCFs85cugzs8fctS0+yaBL1It72YxSTzMw43kGGzSjn9Uy5AGoZnhLAw6E7CO+4D6FCkV4Ui83Ku+UMQ/klMDN3KFl7aL0NpAcsjsvyQeSHwBQvXF7oSXDc/wbc6E=,iv:bpbKsNEu+jrKMz6gIBTEEyH8GuVRKvVSS/vJJ4l/npI=,tag:/bb34cXzy/k957K1X6PLHw==,type:str]
unencrypted_suffix: _unencrypted
version: 3.10.2
version: 3.13.0