diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 8463192..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "permissions": { - "allow": [ - "WebSearch", - "WebFetch(domain:forgejo.org)", - "Bash(ssh:*)" - ] - } -} diff --git a/.gitignore b/.gitignore index 619d00a..fad0876 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.qcow2 result .direnv +.claude diff --git a/.sops.yaml b/.sops.yaml index fdad7d3..6fa100a 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -12,13 +12,19 @@ creation_rules: key_groups: - age: - *desktop + # Shared secrets (desktop + server) + - path_regex: secrets/system/llama\.yaml$ # llama.cpp API key + key_groups: + - age: + - *desktop + - *server # Server secrets (cameras) - path_regex: secrets/system/cameras\.yaml$ # RTSP Feed key_groups: - age: - *server # Server secrets (searxng) - - path_regex: secrets/system/searxng\.yaml$ + - path_regex: secrets/system/searxng\.yaml$ # searxng token key_groups: - age: - *server diff --git a/secrets/system/llama.yaml b/secrets/system/llama.yaml new file mode 100644 index 0000000..6df19c1 --- /dev/null +++ b/secrets/system/llama.yaml @@ -0,0 +1,25 @@ +LLAMA_API_KEY: ENC[AES256_GCM,data:ZVDpwGAxnHbHxt+JW3mYGyyBU5JfFAbjc/byq6Ok9wTlpQZBx969Z0wV74F5pR4axmpdGs7XlZDh1rJaQTn7lg==,iv:oAG9G25x+1FRkRNBRzLW2UJmbSxgx5Cu64Qo/6VzAyw=,tag:nkO/SdzjjLxH4fkgIdwUYQ==,type:str] +sops: + age: + - recipient: age17ejyzyk52unr6eyaa9rpunxpmf7u9726v6sx7me3ww3mdu5xzgjqsgj9gl + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzUmV6Q2dCMWU3TUFkZ0I0 + dHA3dXd2U0RSRzNtL3YvdG8rYWdnOTZoTkMwCkNnYnVlVmMyRDNnS1FmWktlNU9N + UW1OMlJYODVzSHNIZWZMRkpPY05Ed3cKLS0tIDg0b0VkT0NrS3NIWE9EdWtWYXc1 + NjNESHpYbVptcnVRYWFKb3RlYkJ6OWMK3JsRXPDvJdKv2UyYIH8kr/WKbXgUDXbc + fYOD0Huo73BA0vr8PlrsF4STVgJr/arKCMdI1C0bDdcwjExKnR1tIw== + -----END AGE ENCRYPTED FILE----- + - recipient: age198jg29ryg3c0qj3yg6y9ha4ce2ue4hjdaa9kalf49fxju74dhchsquvjzp + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFTGNKOWczaityaXowWi9I + dmh0MjJoelV3bVlzeGpLZmVTVzJjckwwQUFzCk81ZHlTcm5oWHRQNklreUR4bWNS + OVdQelQ4YXkzeWZqOWZoNWlOVkZpWUkKLS0tIDZKQUU3LzV0UUhnRHVHQkFadkxm + djRyUEYyZ2srMlVxR0JtQlFqSWV1QWcKMIF9Sq4TUUmpVZAukjTjFbIrMxcE3+el + QSrHIm1HXLXwCKLDQ2N6b8Q9iUo/XMV0wsD3TLxdnUfegpQpfsDhag== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-04-14T05:45:37Z" + mac: ENC[AES256_GCM,data:G+o6OhNF5AFBDKQEU3f1MZ+GOkxQj/m7NNk4Ti8PxPPOHdByoCrauvgB78SdQf5ubcfupElcNB0yF5QsG3/m7eGaSA+8J0cDL6jB3NEE5EUbW1Fuzzg2Ez1JnFu4BstkLiDRD/TribXMNFAjykmNrHt4zee6fhU3H0MOn7+Acok=,iv:IqBLSBq1kOMRHQn1IvU8OgmWGn6EFJcef/rNr38txmY=,tag:/mSWgbPbhUNoIm3x+6zyRA==,type:str] + unencrypted_suffix: _unencrypted + version: 3.12.1 diff --git a/system/machines/desktop/system.nix b/system/machines/desktop/system.nix index 7a12793..e981ab8 100644 --- a/system/machines/desktop/system.nix +++ b/system/machines/desktop/system.nix @@ -5,6 +5,10 @@ let (user: user.modules.user.security.gpg.enable or false) (lib.attrValues config.home-manager.users); + devEnabled = lib.any + (user: user.modules.user.utils.dev.enable or false) + (lib.attrValues config.home-manager.users); + sysModules = config.modules.system; in @@ -19,6 +23,11 @@ in "WIFI_HOME_PSK" = wifi; "WIFI_CAMS_SSID" = wifi; "WIFI_CAMS_PSK" = wifi; + } // lib.optionalAttrs devEnabled { + "LLAMA_API_KEY" = { + sopsFile = ../../../secrets/system/llama.yaml; + owner = config.user.name; + }; }; sops.templates."wifi-env".content = '' diff --git a/system/machines/server/modules/nginx/default.nix b/system/machines/server/modules/nginx/default.nix index e423815..3f4b0f2 100644 --- a/system/machines/server/modules/nginx/default.nix +++ b/system/machines/server/modules/nginx/default.nix @@ -22,7 +22,6 @@ in ''; }; - searxng.enable = mkEnableOption "Publicly exposed SearXNG endpoint with secret path via sops"; }; config = mkIf cfg.enable { @@ -92,17 +91,6 @@ in }; }; - virtualHosts."test.${domain}" = { - useACMEHost = domain; - forceSSL = true; - locations."/" = { - return = "200 'nginx is working'"; - extraConfig = '' - add_header Content-Type text/plain; - ''; - }; - }; - virtualHosts."wg.${domain}" = { useACMEHost = domain; forceSSL = true; @@ -116,32 +104,49 @@ in }; }; - virtualHosts."searxng.${domain}" = mkIf cfg.searxng.enable { - useACMEHost = domain; - forceSSL = true; - locations."/".return = "404"; - extraConfig = '' - include ${config.sops.templates."nginx-searxng-location.conf".path}; + virtualHosts."ai.${domain}" = let + apiKeyAuth = '' + set $api_key ""; + if ($http_authorization ~* "^Bearer (.+)$") { + set $api_key $1; + } + if ($api_key = "") { + return 401 '{"error": "Missing Authorization header"}'; + } + include ${config.sops.templates."nginx-ai-auth.conf".path}; ''; - }; - - virtualHosts."chat.${domain}" = { + in { useACMEHost = domain; forceSSL = true; - locations."/" = { - proxyPass = "http://192.168.0.23:3080"; - proxyWebsockets = true; - extraConfig = privateAccessRules; - }; - }; - virtualHosts."ai.${domain}" = { - useACMEHost = domain; - forceSSL = true; + # Web UI + llama.cpp API (browser, /v1/* calls from the UI) + # Auth handled by llama.cpp itself (--api-key flag) locations."/" = { proxyPass = "http://192.168.0.23:8000"; proxyWebsockets = true; }; + + # Llama Stack API (opencode, programmatic clients) + # Clients use baseURL: https://ai.ramos.codes/stack/v1 + locations."/stack/v1/" = { + proxyPass = "http://192.168.0.23:8321/v1/"; + proxyWebsockets = true; + extraConfig = apiKeyAuth + '' + proxy_read_timeout 300s; + proxy_send_timeout 300s; + ''; + }; + + # MCP servers (namespaced, for llama.cpp web UI + direct access) + locations."/mcp/web_search/" = { + proxyPass = "http://192.168.0.23:8002/"; + proxyWebsockets = true; + extraConfig = '' + include ${config.sops.templates."nginx-mcp-auth.conf".path}; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + ''; + }; }; virtualHosts."comfy.${domain}" = { diff --git a/system/machines/server/system.nix b/system/machines/server/system.nix index b8674dd..43b75f6 100644 --- a/system/machines/server/system.nix +++ b/system/machines/server/system.nix @@ -9,20 +9,28 @@ # Camera RTSP credentials (used by frigate/go2rtc) sops.secrets = let cameras = { sopsFile = ../../../secrets/system/cameras.yaml; }; - searxng = { sopsFile = ../../../secrets/system/searxng.yaml; }; + llama = { sopsFile = ../../../secrets/system/llama.yaml; }; in { "RTSP_USER" = cameras; "RTSP_PASS" = cameras; - "SEARXNG_TOKEN" = searxng; + "LLAMA_API_KEY" = llama // { owner = config.user.name; }; }; - sops.templates."nginx-searxng-location.conf" = { + # API key auth for ai.ramos.codes — nginx validates Bearer token against sops secret + sops.templates."nginx-ai-auth.conf" = { content = '' - location /${config.sops.placeholder."SEARXNG_TOKEN"}/ { - proxy_pass http://192.168.0.23:8080/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; + if ($api_key != "${config.sops.placeholder."LLAMA_API_KEY"}") { + return 401 '{"error": "Invalid API key"}'; + } + ''; + owner = "nginx"; + }; + + # MCP endpoint auth — validates X-API-Key header + sops.templates."nginx-mcp-auth.conf" = { + content = '' + if ($http_x_api_key != "${config.sops.placeholder."LLAMA_API_KEY"}") { + return 401 '{"error": "Unauthorized"}'; } ''; owner = "nginx"; @@ -31,7 +39,6 @@ modules.system = { nginx = { enable = true; - searxng.enable = true; }; sandpack.enable = true; forgejo.enable = true; diff --git a/user/modules/utils/dev/default.nix b/user/modules/utils/dev/default.nix index 89c4809..991524b 100644 --- a/user/modules/utils/dev/default.nix +++ b/user/modules/utils/dev/default.nix @@ -30,9 +30,9 @@ in ]; programs = { - #bash = { - # initExtra = import ./config/penpot.nix; - #}; + bash = { + initExtra = "export LLAMA_API_KEY=$(cat /run/secrets/LLAMA_API_KEY)"; + }; direnv = { enable = true; enableBashIntegration = true;