diff --git a/CLAUDE.md b/CLAUDE.md index 6711ee8..f70d69a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,9 +58,6 @@ nix-shell -p sops --run 'sops hosts//secrets.yaml' ### Deployment The Git runner handles deployment automatically when changes merge to main. A successful `./scripts/test-configuration ` dry-build is the gate before pushing. -### Host Access -When connecting to a host via SSH or any network tool, always use the fully-qualified `.cloonar.com` form (e.g. `web-arm.cloonar.com`, `mail.cloonar.com`). Bare hostnames do not resolve. - ## Custom Packages When creating a new package in `utils/pkgs/`, always include an `update.sh` script to automate version updates. See `utils/pkgs/claude-code/update.sh` for the pattern: diff --git a/hosts/nb/users/configs/project_history b/hosts/nb/users/configs/project_history index a8eb34c..effdfe0 100644 --- a/hosts/nb/users/configs/project_history +++ b/hosts/nb/users/configs/project_history @@ -19,7 +19,6 @@ /home/dominik/projects/cloonar/ai-image-alt /home/dominik/projects/cloonar/bookmap /home/dominik/projects/cloonar/iso-bot -/home/dominik/projects/macher.solutions/mengenkauf-backend /home/dominik/projects/home-automation/lego-hetzner-bridge /home/dominik/projects/home-automation/ghetto-nixos diff --git a/hosts/nb/users/dominik.nix b/hosts/nb/users/dominik.nix index 3f5a46b..42bc68b 100644 --- a/hosts/nb/users/dominik.nix +++ b/hosts/nb/users/dominik.nix @@ -614,11 +614,6 @@ in home.activation.projects = lib.hm.dag.entryAfter ["writeBoundary"] '' PATH="${pkgs.git}/bin:${pkgs.openssh}/bin:$PATH" set +eu - - mkdir -p ${persistHome}/projects/infrastructure - mkdir -p ${persistHome}/projects/cloonar - mkdir -p ${persistHome}/projects/macher.solutions - ssh-keygen -R git.cloonar.com ssh-keyscan git.cloonar.com >> ~/.ssh/known_hosts git clone forgejo@git.cloonar.com/infrastructure/ci-templates.git ${persistHome}/projects/infrastructure/ci-templates 2>/dev/null @@ -643,7 +638,6 @@ in git clone forgejo@git.cloonar.com:Cloonar/ai-image-alt.git ${persistHome}/projects/cloonar/ai-image-alt 2>/dev/null git clone forgejo@git.cloonar.com:Cloonar/bookmap.git ${persistHome}/projects/cloonar/bookmap 2>/dev/null git clone forgejo@git.cloonar.com/openclawd/iso-bot.git ${persistHome}/projects/cloonar/iso-bot 2>/dev/null - git clone forgejo@git.cloonar.com/macher.solutions/mengenkauf-backend.git ${persistHome}/projects/macher.solutions/mengenkauf-backend git clone forgejo@git.cloonar.com:dominik.polakovics/typo3-basic.git ${persistHome}/cloonar/typo3-basic 2>/dev/null diff --git a/hosts/web-arm/configuration.nix b/hosts/web-arm/configuration.nix index 17927db..6d5849a 100644 --- a/hosts/web-arm/configuration.nix +++ b/hosts/web-arm/configuration.nix @@ -11,6 +11,7 @@ ./modules/bitwarden ./modules/authelia.nix ./modules/collabora.nix + ./modules/ocis.nix ./modules/nextcloud ./modules/rustdesk.nix ./modules/postgresql.nix @@ -54,6 +55,11 @@ "openssl-1.1.1w" ]; + # oCIS (ownCloud Infinite Scale) has an unfree license + nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ + "ocis_5-bin" + ]; + environment.systemPackages = with pkgs; [ vim davfs2 diff --git a/hosts/web-arm/modules/authelia.nix b/hosts/web-arm/modules/authelia.nix index f16d583..ca73718 100644 --- a/hosts/web-arm/modules/authelia.nix +++ b/hosts/web-arm/modules/authelia.nix @@ -169,6 +169,14 @@ in { oidc = { ## The other portions of the mandatory OpenID Connect 1.0 configuration go here. ## See: https://www.authelia.com/c/oidc + lifespans = { + custom = { + ocis = { + access_token = "2 days"; + refresh_token = "3 days"; + }; + }; + }; cors = { endpoints = [ "authorization" @@ -289,6 +297,79 @@ in { ]; userinfo_signing_algorithm = "none"; } + # oCIS (ownCloud Infinite Scale) - web client (public, PKCE) + { + id = "ocis"; + description = "ownCloud Infinite Scale"; + lifespan = "ocis"; + public = true; + authorization_policy = "internal"; + require_pkce = true; + pkce_challenge_method = "S256"; + redirect_uris = [ + "https://files.cloonar.com/" + "https://files.cloonar.com/oidc-callback.html" + "https://files.cloonar.com/oidc-silent-redirect.html" + "https://files.cloonar.com/apps/openidconnect/redirect" + ]; + scopes = [ "openid" "offline_access" "groups" "profile" "email" ]; + response_types = [ "code" ]; + grant_types = [ "authorization_code" "refresh_token" ]; + access_token_signed_response_alg = "none"; + userinfo_signing_algorithm = "none"; + token_endpoint_auth_method = "none"; + } + # oCIS Desktop - static credentials hardcoded in the oCIS desktop app + { + id = "xdXOt13JKxym1B1QcEncf2XDkLAexMBFwiT9j6EfhhHFJhs2KM9jbjTmf8JBXE69"; + description = "ownCloud Infinite Scale (Desktop)"; + secret = "$pbkdf2-sha512$310000$NR4tztBecptj1ZiITK/Ktw$GkFNBfq1B3T1lDTKMci1aO8iulQFNlEtfydLwTrNTKIfrQFjM7EiOBaHGOBC7ohPaNfYCRAYYzcP2fDQf5XRGQ"; + public = false; + authorization_policy = "internal"; + require_pkce = true; + pkce_challenge_method = "S256"; + redirect_uris = [ "http://127.0.0.1" "http://localhost" ]; + scopes = [ "openid" "offline_access" "groups" "profile" "email" ]; + response_types = [ "code" ]; + grant_types = [ "authorization_code" "refresh_token" ]; + access_token_signed_response_alg = "none"; + userinfo_signing_algorithm = "none"; + token_endpoint_auth_method = "client_secret_basic"; + } + # oCIS Android - static credentials hardcoded in the oCIS Android app + { + id = "e4rAsNUSIUs0lF4nbv9FmCeUkTlV9GdgTLDH1b5uie7syb90SzEVrbN7HIpmWJeD"; + description = "ownCloud Infinite Scale (Android)"; + secret = "$pbkdf2-sha512$310000$NjEumkph77Gql.CH0Oq3zg$I9ubOZ3VRCXPbHpW1U4bQmvLgP5DdiFeGgple2nIjtUJsFgkdiV/hcCt1h6adr1uvJSJAtHDRnMhYf3Zp2BpcQ"; + public = false; + authorization_policy = "internal"; + require_pkce = true; + pkce_challenge_method = "S256"; + redirect_uris = [ "oc://android.owncloud.com" ]; + scopes = [ "openid" "offline_access" "groups" "profile" "email" ]; + response_types = [ "code" ]; + grant_types = [ "authorization_code" "refresh_token" ]; + access_token_signed_response_alg = "none"; + userinfo_signing_algorithm = "none"; + token_endpoint_auth_method = "client_secret_basic"; + } + # oCIS iOS - static credentials hardcoded in the oCIS iOS app + { + id = "mxd5OQDk6es5LzOzRvidJNfXLUZS2oN3oUFeXPP8LpPrhx3UroJFduGEYIBOxkY1"; + description = "ownCloud Infinite Scale (iOS)"; + secret = "$pbkdf2-sha512$310000$.nIk0IUua7n8VAUoR85yyA$6UhT/gi7spH/0PRqTa6clz7QMRSmP/FZ0BDIumJupM4V2Ai6MgGKdzlEaNTc2IDqpGL3NxF626g4zAHFRgD7Zg"; + public = false; + authorization_policy = "internal"; + require_pkce = true; + pkce_challenge_method = "S256"; + redirect_uris = [ "oc://ios.owncloud.com" "oc.ios://ios.owncloud.com" ]; + scopes = [ "openid" "offline_access" "groups" "profile" "email" ]; + response_types = [ "code" ]; + grant_types = [ "authorization_code" "refresh_token" ]; + access_token_signed_response_alg = "none"; + userinfo_signing_algorithm = "none"; + token_endpoint_auth_method = "client_secret_basic"; + } ]; }; }; diff --git a/hosts/web-arm/modules/nextcloud/default.nix b/hosts/web-arm/modules/nextcloud/default.nix index 5492839..ef2b59f 100644 --- a/hosts/web-arm/modules/nextcloud/default.nix +++ b/hosts/web-arm/modules/nextcloud/default.nix @@ -10,14 +10,14 @@ in enable = true; hostName = "nextcloud.cloonar.com"; https = true; - package = pkgs.nextcloud33; + package = pkgs.nextcloud32; # Instead of using pkgs.nextcloud27Packages.apps, # we'll reference the package version specified above extraApps = { inherit (config.services.nextcloud.package.packages.apps) calendar contacts deck groupfolders mail richdocuments tasks; oidc_login = pkgs.fetchNextcloudApp rec { - url = "https://github.com/pulsejet/nextcloud-oidc-login/releases/download/v3.3.1/oidc_login.tar.gz"; - sha256 = "sha256-KBa8A7aC0uS6FQoOSa7nIkaaYe+A2KeAtzfqoKw0Gn4="; + url = "https://github.com/pulsejet/nextcloud-oidc-login/releases/download/v3.2.5/oidc_login.tar.gz"; + sha256 = "sha256-Qtqcw1OspTHg0QRIgDMxNru6ZGL8y5XhJ5gdgqn6/Wc="; license = "gpl3"; }; }; diff --git a/hosts/web-arm/modules/ocis.nix b/hosts/web-arm/modules/ocis.nix new file mode 100644 index 0000000..90c0253 --- /dev/null +++ b/hosts/web-arm/modules/ocis.nix @@ -0,0 +1,93 @@ +{ config, lib, pkgs, ... }: + +{ + sops.secrets.ocis-admin-password = { + owner = "ocis"; + }; + + # Upstream services.ocis module adds ReadOnlyPaths = [ configDir ] to the + # systemd unit, which makes systemd fail the namespace setup if the path + # does not exist, and it never runs `ocis init` to populate ocis.yaml with + # the service's internal secrets. Run init in a separate oneshot so the + # sandbox restrictions of ocis.service don't block writes to configDir. + systemd.services.ocis-init = { + description = "Initialize oCIS config (one-shot)"; + before = [ "ocis.service" ]; + requiredBy = [ "ocis.service" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + User = "ocis"; + Group = "ocis"; + StateDirectory = "ocis"; + LoadCredential = "admin-password:${config.sops.secrets.ocis-admin-password.path}"; + }; + + script = '' + install -d -m 0700 /var/lib/ocis/config + if [ ! -f /var/lib/ocis/config/ocis.yaml ]; then + ${lib.getExe pkgs.ocis_5-bin} init \ + --config-path /var/lib/ocis/config \ + --admin-password "$(cat "$CREDENTIALS_DIRECTORY/admin-password")" \ + --insecure true + fi + ''; + }; + + services.ocis = { + enable = true; + url = "https://files.cloonar.com"; + address = "127.0.0.1"; + port = 9200; + stateDir = "/var/lib/ocis"; + configDir = "/var/lib/ocis/config"; + environment = { + # Proxy - SSL terminated at nginx + PROXY_TLS = "false"; + OCIS_INSECURE = "false"; + + # OIDC - Authelia + PROXY_OIDC_ISSUER = "https://auth.cloonar.com"; + PROXY_OIDC_REWRITE_WELLKNOWN = "true"; + PROXY_OIDC_ACCESS_TOKEN_VERIFY_METHOD = "none"; + PROXY_OIDC_SKIP_USER_INFO = "false"; + WEB_OIDC_CLIENT_ID = "ocis"; + + # Auto-provision user accounts from OIDC claims + PROXY_AUTOPROVISION_ACCOUNTS = "true"; + PROXY_AUTOPROVISION_CLAIM_USERNAME = "preferred_username"; + PROXY_AUTOPROVISION_CLAIM_EMAIL = "email"; + PROXY_AUTOPROVISION_CLAIM_DISPLAYNAME = "name"; + PROXY_AUTOPROVISION_CLAIM_GROUPS = "groups"; + + # Disable demo users + IDM_CREATE_DEMO_USERS = "false"; + + # Move internal services off their defaults where Prometheus exporters + # already bind on this host: + # - node-exporter owns 9100 (oCIS web default) + # - blackbox-exporter owns 9115 (oCIS webdav default) + WEB_HTTP_ADDR = "127.0.0.1:19100"; + WEBDAV_HTTP_ADDR = "127.0.0.1:19115"; + }; + }; + + # Nginx reverse proxy + services.nginx.virtualHosts."files.cloonar.com" = { + forceSSL = true; + enableACME = true; + acmeRoot = null; + + locations."/" = { + proxyPass = "http://127.0.0.1:9200"; + proxyWebsockets = true; + extraConfig = '' + client_max_body_size 10G; + proxy_read_timeout 600s; + proxy_send_timeout 600s; + ''; + }; + }; +} diff --git a/hosts/web-arm/modules/supabase/default.nix b/hosts/web-arm/modules/supabase/default.nix index 4519e7e..19df8bb 100644 --- a/hosts/web-arm/modules/supabase/default.nix +++ b/hosts/web-arm/modules/supabase/default.nix @@ -74,27 +74,6 @@ in ExecStart = "${envGenerateScript} ${config.sops.secrets.supabase-env.path}"; }; }; - # Seed the edge-runtime's bootstrap `main` function. The container's - # entrypoint requires `/home/deno/functions/main/index.ts` to exist; - # without it edge-runtime fails with "could not find an appropriate - # entrypoint". Re-seed on every activation so updates to the bootstrap - # are picked up, while leaving user-authored functions untouched. - supabase-functions-seed = { - description = "Seed Supabase edge-functions main bootstrap"; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - script = '' - install -d -m 0755 /var/lib/supabase/functions/main - install -m 0644 ${./functions/main/index.ts} /var/lib/supabase/functions/main/index.ts - ''; - }; - podman-supabase-functions = { - after = [ "supabase-functions-seed.service" ]; - requires = [ "supabase-functions-seed.service" ]; - }; } ]); diff --git a/hosts/web-arm/modules/supabase/functions/main/index.ts b/hosts/web-arm/modules/supabase/functions/main/index.ts deleted file mode 100644 index 05b6ad6..0000000 --- a/hosts/web-arm/modules/supabase/functions/main/index.ts +++ /dev/null @@ -1,144 +0,0 @@ -import * as jose from 'https://deno.land/x/jose@v4.14.4/index.ts' - -console.log('main function started') - -const JWT_SECRET = Deno.env.get('JWT_SECRET') -const SUPABASE_URL = Deno.env.get('SUPABASE_URL') -const VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true' - -// Create JWKS for ES256/RS256 tokens (newer tokens) -let SUPABASE_JWT_KEYS: ReturnType | null = null -if (SUPABASE_URL) { - try { - SUPABASE_JWT_KEYS = jose.createRemoteJWKSet( - new URL('/auth/v1/.well-known/jwks.json', SUPABASE_URL) - ) - } catch (e) { - console.error('Failed to fetch JWKS from SUPABASE_URL:', e) - } -} - -function getAuthToken(req: Request) { - const authHeader = req.headers.get('authorization') - if (!authHeader) { - throw new Error('Missing authorization header') - } - const [bearer, token] = authHeader.split(' ') - if (bearer !== 'Bearer') { - throw new Error(`Auth header is not 'Bearer {token}'`) - } - return token -} - -async function isValidLegacyJWT(jwt: string): Promise { - if (!JWT_SECRET) { - console.error('JWT_SECRET not available for HS256 token verification') - return false - } - - const encoder = new TextEncoder(); - const secretKey = encoder.encode(JWT_SECRET) - - try { - await jose.jwtVerify(jwt, secretKey); - } catch (e) { - console.error('Symmetric Legacy JWT verification error', e); - return false; - } - return true; -} - -async function isValidJWT(jwt: string): Promise { - if (!SUPABASE_JWT_KEYS) { - console.error('JWKS not available for ES256/RS256 token verification') - return false - } - - try { - await jose.jwtVerify(jwt, SUPABASE_JWT_KEYS) - } catch (e) { - console.error('Asymmetric JWT verification error', e); - return false - } - - return true; -} - -async function isValidHybridJWT(jwt: string): Promise { - const { alg: jwtAlgorithm } = jose.decodeProtectedHeader(jwt) - - if (jwtAlgorithm === 'HS256') { - console.log(`Legacy token type detected, attempting ${jwtAlgorithm} verification.`) - - return await isValidLegacyJWT(jwt) - } - - if (jwtAlgorithm === 'ES256' || jwtAlgorithm === 'RS256') { - return await isValidJWT(jwt) - } - - return false; -} - -Deno.serve(async (req: Request) => { - if (req.method !== 'OPTIONS' && VERIFY_JWT) { - try { - const token = getAuthToken(req) - const isValidJWT = await isValidHybridJWT(token); - - if (!isValidJWT) { - return new Response(JSON.stringify({ msg: 'Invalid JWT' }), { - status: 401, - headers: { 'Content-Type': 'application/json' }, - }) - } - } catch (e) { - console.error(e) - return new Response(JSON.stringify({ msg: e.toString() }), { - status: 401, - headers: { 'Content-Type': 'application/json' }, - }) - } - } - - const url = new URL(req.url) - const { pathname } = url - const path_parts = pathname.split('/') - const service_name = path_parts[1] - - if (!service_name || service_name === '') { - const error = { msg: 'missing function name in request' } - return new Response(JSON.stringify(error), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }) - } - - const servicePath = `/home/deno/functions/${service_name}` - console.error(`serving the request with ${servicePath}`) - - const memoryLimitMb = 150 - const workerTimeoutMs = 1 * 60 * 1000 - const noModuleCache = false - const importMapPath = null - const envVarsObj = Deno.env.toObject() - const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]]) - - try { - const worker = await EdgeRuntime.userWorkers.create({ - servicePath, - memoryLimitMb, - workerTimeoutMs, - noModuleCache, - importMapPath, - envVars, - }) - return await worker.fetch(req) - } catch (e) { - const error = { msg: e.toString() } - return new Response(JSON.stringify(error), { - status: 500, - headers: { 'Content-Type': 'application/json' }, - }) - } -}) diff --git a/hosts/web-arm/sites/fueltide.io.nix b/hosts/web-arm/sites/fueltide.io.nix index 2186673..9239883 100644 --- a/hosts/web-arm/sites/fueltide.io.nix +++ b/hosts/web-arm/sites/fueltide.io.nix @@ -1,19 +1,4 @@ { pkgs, lib, config, ... }: -let - # Universal Links / Associated Domains for the iOS workout app - appleAppSiteAssociation = { - applinks = { - details = [ - { - appIDs = [ "XWJ4DC7TBH.io.fueltide.workout" ]; - components = [ - { "/" = "/auth/*"; } - ]; - } - ]; - }; - }; -in { # SOPS secret for fueltide.io DNS credentials (separate Hetzner API token) sops.secrets.fueltide-lego-credentials = { }; @@ -32,10 +17,6 @@ in credentialsFile = config.sops.secrets.fueltide-lego-credentials.path; }; - security.acme.certs."link.fueltide.io" = { - credentialsFile = config.sops.secrets.fueltide-lego-credentials.path; - }; - security.acme.certs."stage.fueltide.io" = { credentialsFile = config.sops.secrets.fueltide-lego-credentials.path; }; @@ -44,16 +25,6 @@ in credentialsFile = config.sops.secrets.fueltide-lego-credentials.path; }; - services.nginx.virtualHosts."link.fueltide.io" = { - enableACME = true; - forceSSL = true; - - locations."= /.well-known/apple-app-site-association".extraConfig = '' - default_type application/json; - return 200 '${builtins.toJSON appleAppSiteAssociation}'; - ''; - }; - services.webstack.instances."stage.fueltide.io" = { enablePhp = false; enableDefaultLocations = false; diff --git a/utils/pkgs/claude-code/default.nix b/utils/pkgs/claude-code/default.nix index 109c5b3..352cf7a 100644 --- a/utils/pkgs/claude-code/default.nix +++ b/utils/pkgs/claude-code/default.nix @@ -1,11 +1,11 @@ { lib, pkgs, runCommand, claude-code }: let - version = "2.1.111"; + version = "2.1.75"; src = pkgs.fetchzip { url = "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-${version}.tgz"; - hash = "sha256-K3qhZXVJ2DIKv7YL9f/CHkuUYnK0lkIR1wjEa+xeSCk="; + hash = "sha256-EgwxqiCl7c8PoRYyHDvcgvK8txDd0XJeZD1vybZyp4E="; }; # Create a modified source with our package-lock.json @@ -22,7 +22,7 @@ in npmDeps = pkgs.fetchNpmDeps { src = srcWithLock; - hash = "sha256-EFh1nVImvToqY+scUcodg50emRpF6Rzi+AJNPi59AVY="; + hash = "sha256-DIyV2ZyrEI+iCOb4VcKbdd6NWyFqRUpH/rKv/HvCcG8="; }; # Remove the old postPatch since srcWithLock already includes package-lock.json diff --git a/utils/pkgs/claude-code/package-lock.json b/utils/pkgs/claude-code/package-lock.json index 7aeec21..8a99d45 100644 --- a/utils/pkgs/claude-code/package-lock.json +++ b/utils/pkgs/claude-code/package-lock.json @@ -5,13 +5,13 @@ "packages": { "": { "dependencies": { - "@anthropic-ai/claude-code": "^2.1.111" + "@anthropic-ai/claude-code": "^2.1.75" } }, "node_modules/@anthropic-ai/claude-code": { - "version": "2.1.111", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-2.1.111.tgz", - "integrity": "sha512-zNZcINqvtMpDM4lZqnOZcrou57k9rUBfCZziH47nMG9FNks7I6azN7+SMU3zhwqBwYrvx6o4i7Ecu7mDCi0AmA==", + "version": "2.1.75", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-2.1.75.tgz", + "integrity": "sha512-K5xwpR53WfZcKXhQedgoxcRbi6arH7S9YrhVXifA3klZ/5L4zcv+wENzITISygm1MwbxX5w2c2N1Qd/oUSY42w==", "license": "SEE LICENSE IN README.md", "bin": { "claude": "cli.js"