diff --git a/flake.lock b/flake.lock index 7e21bfb6..240b0cd1 100644 --- a/flake.lock +++ b/flake.lock @@ -64,6 +64,21 @@ "type": "github" } }, + "crane": { + "locked": { + "lastModified": 1785284101, + "narHash": "sha256-ghcXEpYEM4a7pbEkoqbn8c0ptJJqgGzuFiG3T6W5g4I=", + "owner": "ipetkov", + "repo": "crane", + "rev": "756d6d07c3818ea95d1e2cdac63fa7d02fe3e61b", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, "disko": { "inputs": { "nixpkgs": [ @@ -442,6 +457,30 @@ "type": "github" } }, + "rag-nix": { + "inputs": { + "crane": "crane", + "nixpkgs": [ + "nixpkgs" + ], + "treefmt-nix": [ + "treefmt-nix" + ] + }, + "locked": { + "lastModified": 1785830820, + "narHash": "sha256-yRrirhXt7Q8M/+7DTy1vw1xZO5pPoYYfqiQTBVE4rmI=", + "owner": "mulatta", + "repo": "rag.nix", + "rev": "aaa9d45bf7caded03276cb5fdb533cd6df056b1a", + "type": "github" + }, + "original": { + "owner": "mulatta", + "repo": "rag.nix", + "type": "github" + } + }, "rhwp-nextcloud": { "inputs": { "flake-parts": [ @@ -522,6 +561,7 @@ "nixbot": "nixbot", "nixos-images": "nixos-images", "nixpkgs": "nixpkgs", + "rag-nix": "rag-nix", "rhwp-nextcloud": "rhwp-nextcloud", "rustfs": "rustfs", "sops-nix": "sops-nix", diff --git a/flake.nix b/flake.nix index ff460314..5eb06b08 100644 --- a/flake.nix +++ b/flake.nix @@ -61,6 +61,12 @@ }; # Applications. + rag-nix = { + url = "github:mulatta/rag.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.treefmt-nix.follows = "treefmt-nix"; + }; + multievolve-nix = { url = "github:SBEE-Lab/multievolve-nix"; inputs.nixpkgs.follows = "nixpkgs"; diff --git a/hosts/eta.nix b/hosts/eta.nix index 6309bd46..0a7846fb 100644 --- a/hosts/eta.nix +++ b/hosts/eta.nix @@ -3,6 +3,13 @@ let inherit (config.networking.sbee) hosts; wgAdminAddr = config.networking.sbee.currentHost.wg-admin; + mkInternalCert = { + dnsProvider = "cloudflare"; + environmentFile = config.sops.secrets.cloudflare-credentials.path; + webroot = null; + group = "acme"; + }; + blackboxConfig = pkgs.writeText "blackbox.yml" ( builtins.toJSON { modules = { @@ -111,44 +118,24 @@ in remoteUser = "acme-sync-vaultwarden"; remoteHost = hosts.tau.wg-admin; } + { + domain = "omnigraph.sjanglab.org"; + serviceName = "acme-sync-omnigraph-to-tau"; + remoteUser = "acme-sync-omnigraph"; + remoteHost = hosts.tau.wg-admin; + } ]; disko.rootDisk = "/dev/vda"; # ACME certificates for internal services - security.acme.certs."status.sjanglab.org" = { - dnsProvider = "cloudflare"; - environmentFile = config.sops.secrets.cloudflare-credentials.path; - webroot = null; - group = "acme"; - }; - - security.acme.certs."logging.sjanglab.org" = { - dnsProvider = "cloudflare"; - environmentFile = config.sops.secrets.cloudflare-credentials.path; - webroot = null; - group = "acme"; - }; - - security.acme.certs."multievolve.sjanglab.org" = { - dnsProvider = "cloudflare"; - environmentFile = config.sops.secrets.cloudflare-credentials.path; - webroot = null; - group = "acme"; - }; - - security.acme.certs."tei.sjanglab.org" = { - dnsProvider = "cloudflare"; - environmentFile = config.sops.secrets.cloudflare-credentials.path; - webroot = null; - group = "acme"; - }; - - security.acme.certs."vault.sjanglab.org" = { - dnsProvider = "cloudflare"; - environmentFile = config.sops.secrets.cloudflare-credentials.path; - webroot = null; - group = "acme"; + security.acme.certs = { + "status.sjanglab.org" = mkInternalCert; + "logging.sjanglab.org" = mkInternalCert; + "multievolve.sjanglab.org" = mkInternalCert; + "tei.sjanglab.org" = mkInternalCert; + "vault.sjanglab.org" = mkInternalCert; + "omnigraph.sjanglab.org" = mkInternalCert; }; networking.hostName = "eta"; diff --git a/hosts/tau.nix b/hosts/tau.nix index dbba42e6..23850dfb 100644 --- a/hosts/tau.nix +++ b/hosts/tau.nix @@ -1,4 +1,19 @@ -{ ... }: +{ + config, + lib, + pkgs, + self, + ... +}: +let + inherit (config.networking.sbee) hosts; + wgAdminAddr = config.networking.sbee.currentHost.wg-admin; + omnigraphBucket = "omnigraph"; + omnigraphCluster = "s3://${omnigraphBucket}/clusters/main"; + omnigraphPrefix = "clusters/main"; + omnigraphBucketArn = "arn:aws:s3:::${omnigraphBucket}"; + omnigraphObjectArn = "${omnigraphBucketArn}/${omnigraphPrefix}/*"; +in { imports = [ ../modules/hardware/asrock-deskmini-x600.nix @@ -8,6 +23,8 @@ ../modules/tailscale ../modules/postgresql/replica.nix ../modules/rustfs + ../modules/omnigraph + ../modules/omnigraph/reverse-proxy.nix ../modules/backup/primary.nix ../modules/monitoring/vector/monitor-services.nix ../modules/nextcloud @@ -46,11 +63,130 @@ group = "rustfs"; mode = "0400"; }; + omnigraph-bearer-tokens = { + owner = "omnigraph"; + group = "omnigraph"; + mode = "0400"; + }; + omnigraph-env = { + owner = "omnigraph"; + group = "omnigraph"; + mode = "0400"; + }; + omnigraph-rustfs-server-secret-key = { + owner = "rustfs"; + group = "rustfs"; + mode = "0400"; + }; + omnigraph-rustfs-admin-secret-key = { + owner = "rustfs"; + group = "rustfs"; + mode = "0400"; + }; }; - services.rustfs.enable = true; - services.sbee.backups = { - primary = { + environment.systemPackages = [ self.packages.${pkgs.stdenv.hostPlatform.system}.omnigraph-cli ]; + + services = { + rustfs = { + enable = true; + ensureBuckets = [ omnigraphBucket ]; + ensurePolicies = { + omnigraph-server.statements = [ + { + actions = [ "s3:ListBucket" ]; + resources = [ omnigraphBucketArn ]; + condition.StringLike."s3:prefix" = [ + omnigraphPrefix + "${omnigraphPrefix}/*" + ]; + } + { + actions = [ "s3:GetBucketLocation" ]; + resources = [ omnigraphBucketArn ]; + } + { + actions = [ + "s3:GetObject" + "s3:PutObject" + "s3:DeleteObject" + "s3:AbortMultipartUpload" + "s3:ListMultipartUploadParts" + ]; + resources = [ omnigraphObjectArn ]; + } + ]; + + omnigraph-admin.statements = [ + { + actions = [ "s3:ListBucket" ]; + resources = [ omnigraphBucketArn ]; + condition.StringLike."s3:prefix" = [ + omnigraphPrefix + "${omnigraphPrefix}/*" + ]; + } + { + actions = [ "s3:GetBucketLocation" ]; + resources = [ omnigraphBucketArn ]; + } + { + actions = [ + "s3:GetObject" + "s3:PutObject" + "s3:DeleteObject" + "s3:AbortMultipartUpload" + "s3:ListMultipartUploadParts" + ]; + resources = [ omnigraphObjectArn ]; + } + ]; + }; + ensureUsers = [ + { + name = "omnigraph-server"; + secretKeyFile = config.sops.secrets.omnigraph-rustfs-server-secret-key.path; + policies = [ "omnigraph-server" ]; + } + { + name = "omnigraph-admin"; + secretKeyFile = config.sops.secrets.omnigraph-rustfs-admin-secret-key.path; + policies = [ "omnigraph-admin" ]; + } + ]; + }; + + omnigraph = { + enable = true; + # Enable after omnigraph-clusters applies the first cluster revision to RustFS. + autoStart = false; + cluster = omnigraphCluster; + listenAddress = "127.0.0.1"; + port = 8300; + bearerTokensFile = config.sops.secrets.omnigraph-bearer-tokens.path; + environment = { + AWS_ACCESS_KEY_ID = "omnigraph-server"; + AWS_ALLOW_HTTP = "true"; + AWS_ENDPOINT_URL_S3 = "http://${wgAdminAddr}:9100"; + AWS_REGION = "us-east-1"; + AWS_S3_FORCE_PATH_STYLE = "true"; + + OMNIGRAPH_EMBED_PROVIDER = "openai-compatible"; + OMNIGRAPH_EMBED_BASE_URL = "http://${hosts.psi.wg-admin}:8201/v1"; + OMNIGRAPH_EMBED_MODEL = "Qwen/Qwen3-Embedding-0.6B"; + OPENAI_API_KEY = "unused-for-internal-tei"; + }; + environmentFiles = [ config.sops.secrets.omnigraph-env.path ]; + }; + + vector.settings = lib.sbee.monitoring.mkJournaldLokiPipeline { + name = "omnigraph"; + hostName = config.networking.hostName; + endpoint = "http://${hosts.rho.wg-admin}:3100"; + units = [ "omnigraph-server.service" ]; + }; + + sbee.backups.primary = { psiProtected.enable = true; vaultwarden.enable = true; postgresql.enable = true; diff --git a/hosts/tau.yaml b/hosts/tau.yaml index 847af987..abf1d31e 100644 --- a/hosts/tau.yaml +++ b/hosts/tau.yaml @@ -6,6 +6,10 @@ pg-replicator-password: ENC[AES256_GCM,data:nXtdREXtZwCihwS8oHC4oJBHeaK5m3tQIqvG wg-admin-key: ENC[AES256_GCM,data:wk2POccfghaQh0nnSlvx3ja/dDPD3vY2xldLdmoOjzxY1B+8E4pHsgAv5oU=,iv:YmDgO99EL34shrHo+eGpBzThuSPmkL4kN74W5kmbUaY=,tag:BhY/n5sTZ274H1g67gUDqg==,type:str] rustfs-access-key: ENC[AES256_GCM,data:4tWROJKmg+4xCz+mVDPYb/Nyfk6zALTHRar1bU1JmswAI0k=,iv:cyKNRv6CoBwSLuT1lVhfc3u1Q1Yt2UZqk9ymkooQ7ew=,tag:IgGmNGsjmHlDckqjvOuZhg==,type:str] rustfs-secret-key: ENC[AES256_GCM,data:LTY/TTIMUHqWl20hrpZSBzBF0r+oLaJSHedWfurkjbLglmpfXK3t1g5r8OxXO2dNobZqbeeF2BieqRBUmUVBvA==,iv:drtZplJ/IVkyFVXuvKBRpDw8QziLCU6rb2+k+/nA2yw=,tag:JQ13+sss0QjEIp42kVR/ow==,type:str] +omnigraph-rustfs-server-secret-key: ENC[AES256_GCM,data:+7inJXttImZTIM/HCxFO9+scbYxyvGDjJkT0a2v2VA19lNLD4zIc8EfG6L9sqorOPGsJLrC7gFP/enqDr4YbxA==,iv:gYNYay30KEKLrEup+8CyHAMn3TOgKihmqPrpaKwCiG0=,tag:vnoO97NT6+XDHae4BBWuHQ==,type:str] +omnigraph-rustfs-admin-secret-key: ENC[AES256_GCM,data:2vUgL3GboReKlB6rkBorKQIX0D+/yZ4WaXiWzYeoeXntCpDPE1KKSmSziVphIZ7RzQMPxTOXF9KBP6whdTn2Tw==,iv:jhxyBDMsqk0uPXLfykO2ADYuYogbK2C0L5aNg/gf8fA=,tag:IV31GLrS4Yo40WhaaTRe4g==,type:str] +omnigraph-env: ENC[AES256_GCM,data:u99wq6OBhNesh1Amw6Yapl1Nfer6Mgu9gMqIwQngBaldVgcl/N3MdM08GdBZ375kA6btCQ9UFAtKQxozDx7AjJTuqectMSxSxT4BNbwgPgVyAkVW7If5,iv:5BtLsUS+01orXdCy7yBkHEoupOggZW04yXiqcgfNJy8=,tag:vJGrHJBsof3PESTaKnMfVw==,type:str] +omnigraph-bearer-tokens: ENC[AES256_GCM,data:u76SCFZQl/91JPZy8231KBqol6Jg9TBAF/uQC2Kbo7ey2qEq8PGpZ+xDJ+D89C4uDU4yUDYLOXskWlMzOcnKkb6tgETspEXBEs2Mj41Jezrv8vN3rq+87Ns=,iv:JjRdgiCiUg7AGjPRQbGkI09M6ifGEqRMe8pUP8KLdCg=,tag:kKAE/FtW0/YZYrTHLV/BcA==,type:str] sops: age: - enc: | @@ -26,7 +30,7 @@ sops: y8bOtNfzHUJm+QhlUnQr7gca/k7Zd7kXUAthChiTOU9PjrfOWC8nvg== -----END AGE ENCRYPTED FILE----- recipient: age1730f3cxdyh56zw8xcvlmpa7u2x7353wu4u0e58kyx24rsefgp98sxehm6s - lastmodified: "2026-07-08T11:43:38Z" - mac: ENC[AES256_GCM,data:nl+adjYFHacZB+XupnvIem2Qs/Pal0VbHHP3KYZsbVQx4QH7dCmgZR595hmokWsdxJkK5Y1EDqbd/578dOZNT2yDTrlfaPNcnUQjaMeLiegdyyIvOfH2mKZ/6RldiANuJjVki8me63+BTifXY10PHtK+jawcCaQG4d9p+KMjcjQ=,iv:F/sZjS0MbZaCRU2fhH2xF+TmSQJVB9hKyZCGOeWTDuc=,tag:HCCMAnhmWBXGc8z8zlr0xg==,type:str] + lastmodified: "2026-07-09T18:18:03Z" + mac: ENC[AES256_GCM,data:/LhXIXKqU3eKH692RYLbkxFzf0BryHCDLuT4h4fZr9CA9bHrClwcwhTZ8CSxHEPGecGIsyXTy7FIFmuHL32dSBsDSbgwR99w33nXD3fgz+FmoKjtp9orIIAFTUbT0YkUaSwr3ePewbMpwvGFTJuIZ/o8jP0buPyEXGod6XUp/Gk=,iv:WNIt4R8u+VHc+KwvvegJeAjTimNy1EKwmavgvT/M5Lg=,tag:qjWaNXibt3w3dDUmebAYgg==,type:str] unencrypted_suffix: _unencrypted version: 3.11.0 diff --git a/modules/headscale/default.nix b/modules/headscale/default.nix index 51d8e349..09751737 100644 --- a/modules/headscale/default.nix +++ b/modules/headscale/default.nix @@ -78,6 +78,11 @@ type = "A"; value = "100.64.0.3"; # tau headscale IP } + { + name = "omnigraph.sjanglab.org"; + type = "A"; + value = "100.64.0.3"; # tau headscale IP + } { name = "upterm.sjanglab.org"; type = "A"; diff --git a/modules/monitoring/audit/nginx-access-logs.nix b/modules/monitoring/audit/nginx-access-logs.nix index ad6cdbd7..fbcb7cf4 100644 --- a/modules/monitoring/audit/nginx-access-logs.nix +++ b/modules/monitoring/audit/nginx-access-logs.nix @@ -8,6 +8,7 @@ let "logging.sjanglab.org" = "grafana"; "multievolve.sjanglab.org" = "multievolve"; "n8n.sjanglab.org" = "n8n"; + "omnigraph.sjanglab.org" = "omnigraph"; "status.sjanglab.org" = "gatus"; "tei.sjanglab.org" = "tei"; "vault.sjanglab.org" = "vaultwarden"; diff --git a/modules/monitoring/loki/rules.nix b/modules/monitoring/loki/rules.nix index 2e452857..7d9af29b 100644 --- a/modules/monitoring/loki/rules.nix +++ b/modules/monitoring/loki/rules.nix @@ -245,6 +245,20 @@ let summary = "nginx access logs missing"; description = "rho Loki has not received nginx access logs for 15 minutes"; }) + (mkAlert { + alert = "OmnigraphNonTailnetAccess"; + expr = '' + sum by (host, service, ingress_network) ( + count_over_time({log_type="nginx_access", service="omnigraph", ingress_network!~"tailnet|wg-admin"}[5m]) + ) > 0 + ''; + for = "1m"; + labels = auditWarning // { + service = "omnigraph"; + }; + summary = "Omnigraph non-tailnet access observed"; + description = "{{ $labels.host }} received Omnigraph access from {{ $labels.ingress_network }}"; + }) (mkAlert { alert = "ContainerRegistryAuthFailureBurst"; expr = '' diff --git a/modules/omnigraph/config.nix b/modules/omnigraph/config.nix new file mode 100644 index 00000000..e806ce6a --- /dev/null +++ b/modules/omnigraph/config.nix @@ -0,0 +1,163 @@ +{ + config, + lib, + ... +}: +let + cfg = config.services.omnigraph; + + authEnvVars = [ + "OMNIGRAPH_SERVER_BEARER_TOKEN" + "OMNIGRAPH_SERVER_BEARER_TOKENS_JSON" + "OMNIGRAPH_SERVER_BEARER_TOKENS_FILE" + ]; + + hasNonEmptyEnv = name: lib.trim (cfg.environment.${name} or "") != ""; + + usesAwsSecretManager = hasNonEmptyEnv "OMNIGRAPH_SERVER_BEARER_TOKENS_AWS_SECRET"; + + hasAuthEnv = lib.any hasNonEmptyEnv authEnvVars; + + formattedListenAddress = + if lib.hasPrefix "[" cfg.listenAddress && lib.hasSuffix "]" cfg.listenAddress then + cfg.listenAddress + else if lib.hasInfix ":" cfg.listenAddress then + "[${cfg.listenAddress}]" + else + cfg.listenAddress; + + bind = "${formattedListenAddress}:${toString cfg.port}"; + + clusterIsAbsolutePath = lib.hasPrefix "/" cfg.cluster; + + localClusterPaths = lib.optional clusterIsAbsolutePath cfg.cluster; + + readWritePaths = [ cfg.dataDir ] ++ localClusterPaths ++ cfg.writablePaths; + + isProtectedHomePath = + path: + lib.any (root: path == root || lib.hasPrefix "${root}/" path) [ + "/home" + "/root" + "/run/user" + ]; + + managedArgs = [ + "--cluster" + "--bind" + "--unauthenticated" + "--require-all-graphs" + ]; + + isManagedArg = arg: lib.any (flag: arg == flag || lib.hasPrefix "${flag}=" arg) managedArgs; + + commandArgs = [ + "--cluster" + cfg.cluster + "--bind" + bind + ] + ++ lib.optional cfg.unauthenticated "--unauthenticated" + ++ lib.optional cfg.requireAllGraphs "--require-all-graphs" + ++ cfg.extraArgs; +in +{ + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.dataDir != "/"; + message = "services.omnigraph.dataDir must not be the filesystem root"; + } + { + assertion = !lib.any isProtectedHomePath readWritePaths; + message = "services.omnigraph writable paths must not be hidden by ProtectHome"; + } + { + assertion = !lib.any isManagedArg cfg.extraArgs; + message = "services.omnigraph.extraArgs must not repeat arguments managed by typed options"; + } + { + assertion = + cfg.bearerTokensFile == null || !lib.hasAttr "OMNIGRAPH_SERVER_BEARER_TOKENS_FILE" cfg.environment; + message = "services.omnigraph.bearerTokensFile conflicts with environment.OMNIGRAPH_SERVER_BEARER_TOKENS_FILE"; + } + ]; + + warnings = + lib.optional (!cfg.unauthenticated && cfg.bearerTokensFile == null && !hasAuthEnv) ( + if cfg.environmentFiles == [ ] then + "services.omnigraph: no bearer token source configured; omnigraph-server will refuse startup" + else + "services.omnigraph: auth supplied through environmentFiles is checked only when the server starts" + ) + ++ lib.optional usesAwsSecretManager "services.omnigraph: default omnigraph-server package lacks optional AWS Secrets Manager bearer-token support; use bearerTokensFile or environmentFiles"; + + users.groups = lib.mkIf cfg.createUser { + ${cfg.group} = { }; + }; + + users.users = lib.mkIf cfg.createUser { + ${cfg.user} = { + isSystemUser = true; + inherit (cfg) group; + home = cfg.dataDir; + }; + }; + + systemd.tmpfiles.settings."10-omnigraph".${cfg.dataDir}.d = { + inherit (cfg) user; + inherit (cfg) group; + mode = "0750"; + }; + + systemd.services.omnigraph-server = { + description = "Omnigraph HTTP server"; + documentation = [ "https://github.com/ModernRelay/omnigraph" ]; + wantedBy = lib.optional cfg.autoStart "multi-user.target"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + + inherit (cfg) environment; + + script = '' + set -euo pipefail + ${lib.optionalString (cfg.bearerTokensFile != null) '' + export OMNIGRAPH_SERVER_BEARER_TOKENS_FILE="$CREDENTIALS_DIRECTORY/bearer-tokens" + ''} + export HOME=${lib.escapeShellArg cfg.dataDir} + exec ${lib.getExe cfg.package} ${lib.escapeShellArgs commandArgs} + ''; + + serviceConfig = { + User = cfg.user; + Group = cfg.group; + Restart = "on-failure"; + RestartSec = "5s"; + EnvironmentFile = cfg.environmentFiles; + LoadCredential = lib.optional ( + cfg.bearerTokensFile != null + ) "bearer-tokens:${cfg.bearerTokensFile}"; + WorkingDirectory = cfg.dataDir; + UMask = "0077"; + + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = readWritePaths; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictAddressFamilies = [ + "AF_UNIX" + "AF_INET" + "AF_INET6" + ]; + RestrictSUIDSGID = true; + LockPersonality = true; + }; + }; + + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ]; + }; +} diff --git a/modules/omnigraph/default.nix b/modules/omnigraph/default.nix new file mode 100644 index 00000000..89d5a51f --- /dev/null +++ b/modules/omnigraph/default.nix @@ -0,0 +1,16 @@ +{ + lib, + pkgs, + self, + ... +}: +{ + imports = [ + ./options.nix + ./config.nix + ]; + + services.omnigraph.package = + lib.mkDefault + self.packages.${pkgs.stdenv.hostPlatform.system}.omnigraph-server; +} diff --git a/modules/omnigraph/options.nix b/modules/omnigraph/options.nix new file mode 100644 index 00000000..e4764e93 --- /dev/null +++ b/modules/omnigraph/options.nix @@ -0,0 +1,147 @@ +{ lib, ... }: +let + clusterLocationType = lib.types.either lib.types.externalPath ( + lib.types.strMatching "^[A-Za-z][A-Za-z0-9+.-]*://.+" + ); +in +{ + options.services.omnigraph = { + enable = lib.mkEnableOption "Omnigraph HTTP server"; + + package = lib.mkOption { + type = lib.types.package; + description = "Omnigraph server package to run."; + }; + + cluster = lib.mkOption { + type = clusterLocationType; + example = "s3://omnigraph/clusters/company-brain"; + description = '' + Cluster boot source passed to omnigraph-server --cluster. Use an + absolute local cluster directory or an object-storage root URI. + ''; + }; + + listenAddress = lib.mkOption { + type = lib.types.nonEmptyStr; + default = "127.0.0.1"; + description = "Address omnigraph-server binds to."; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 8080; + description = "TCP port omnigraph-server binds to."; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Open the Omnigraph TCP port in the firewall."; + }; + + autoStart = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Start omnigraph-server during boot. Disable while bootstrapping a + remote cluster root that has not received its first applied revision. + ''; + }; + + unauthenticated = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Start in unauthenticated mode. Use only for local development; shared + deployments should configure a bearer token source instead. + ''; + }; + + requireAllGraphs = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Abort startup when any graph in the applied cluster revision cannot be + served, instead of quarantining failed graphs. + ''; + }; + + bearerTokensFile = lib.mkOption { + type = lib.types.nullOr lib.types.externalPath; + default = null; + example = "/run/secrets/omnigraph-bearer-tokens.json"; + description = '' + Runtime path to a JSON token map of actor IDs to bearer tokens. The file + is passed through a systemd credential and exposed to the server via + OMNIGRAPH_SERVER_BEARER_TOKENS_FILE. Default package does not enable + optional AWS Secrets Manager bearer-token backend; S3 storage remains + fully supported. + ''; + }; + + environment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = '' + Non-secret environment variables for omnigraph-server, such as AWS + endpoint settings or workload limits. Values are exposed through the + Nix store; use environmentFiles for credentials and API keys. + ''; + }; + + environmentFiles = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "/run/secrets/omnigraph.env" ]; + description = '' + Environment files loaded by systemd. Use this for S3 credentials, + embedding provider keys, or bearer token variables managed outside Nix. + The module cannot inspect these files when checking auth configuration. + ''; + }; + + extraArgs = lib.mkOption { + type = lib.types.listOf lib.types.nonEmptyStr; + default = [ ]; + description = '' + Additional command-line arguments passed to omnigraph-server. Do not + repeat --cluster, --bind, --unauthenticated, or --require-all-graphs. + ''; + }; + + user = lib.mkOption { + type = lib.types.nonEmptyStr; + default = "omnigraph"; + description = "User account that runs omnigraph-server."; + }; + + group = lib.mkOption { + type = lib.types.nonEmptyStr; + default = "omnigraph"; + description = "Group account that runs omnigraph-server."; + }; + + createUser = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Create the configured system user and group."; + }; + + dataDir = lib.mkOption { + type = lib.types.externalPath; + default = "/var/lib/omnigraph"; + description = "State directory used as HOME and writable local storage."; + }; + + writablePaths = lib.mkOption { + type = lib.types.listOf lib.types.externalPath; + default = [ ]; + example = [ "/srv/omnigraph" ]; + description = '' + Extra local paths omnigraph-server may write. Add local cluster roots + outside dataDir here when ProtectSystem=strict is enabled. + ''; + }; + }; +} diff --git a/modules/omnigraph/reverse-proxy.nix b/modules/omnigraph/reverse-proxy.nix new file mode 100644 index 00000000..5a2a8fd6 --- /dev/null +++ b/modules/omnigraph/reverse-proxy.nix @@ -0,0 +1,47 @@ +{ config, ... }: +let + domain = "omnigraph.sjanglab.org"; + certDir = "/var/lib/acme/${domain}"; + port = config.services.omnigraph.port; +in +{ + imports = [ ../acme/sync.nix ]; + + acmeSyncer.mkReceiver = [ + { + inherit domain; + user = "acme-sync-omnigraph"; + } + ]; + + services.nginx = { + enable = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + + virtualHosts.${domain} = { + onlySSL = true; + sslCertificate = "${certDir}/fullchain.pem"; + sslCertificateKey = "${certDir}/key.pem"; + extraConfig = '' + access_log /var/log/nginx/access-audit/omnigraph.log nginx_access_json; + client_max_body_size 32M; + + allow 100.64.0.0/10; + allow 10.100.0.0/24; + deny all; + ''; + + locations."/" = { + proxyPass = "http://127.0.0.1:${toString port}"; + proxyWebsockets = true; + extraConfig = '' + proxy_read_timeout 300s; + proxy_send_timeout 300s; + ''; + }; + }; + }; + + networking.firewall.interfaces.tailscale0.allowedTCPPorts = [ 443 ]; +} diff --git a/packages/flake-module.nix b/packages/flake-module.nix index b9582c74..6d6e9360 100644 --- a/packages/flake-module.nix +++ b/packages/flake-module.nix @@ -1,4 +1,4 @@ -{ self, ... }: +{ inputs, self, ... }: { perSystem = { @@ -10,10 +10,15 @@ packages = { docker-auth = pkgs.callPackage ./docker_auth { }; infra-alert-bridge = pkgs.callPackage ./infra-alert-bridge { }; + inherit (inputs.rag-nix.packages.${pkgs.stdenv.hostPlatform.system}) + omnigraph-cli + omnigraph-server + ; slack-cli = pkgs.callPackage ./slack-cli { }; updater = pkgs.callPackage ./updater { }; } // lib.optionalAttrs pkgs.stdenv.isLinux { + inherit (inputs.rag-nix.packages.${pkgs.stdenv.hostPlatform.system}) omnigraph; installer = pkgs.callPackage ./image-installer { inherit pkgs self; }; kexec = pkgs.callPackage ./kexec-installer { inherit pkgs self; }; text-embeddings-inference = pkgs.callPackage ./text-embeddings-inference {