diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-10 21:57:37 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-10 21:57:37 +0000 |
| commit | fe40518a2d2d30f29b8b668c42ce2afa059d95a8 (patch) | |
| tree | dd0b32d5ecdc5b8330ecd04b8616ef2b0ea55a75 /nix/module.nix | |
| parent | 8536be07962ee6b23ecca0f1c084db11a3c104e0 (diff) | |
feat: support multiple ngit-grasp instances in NixOS module
- Convert module from single service to attrsOf instances
- Each instance gets separate systemd service: ngit-grasp-<name>
- Each instance gets separate user: ngit-grasp-<name> (customizable)
- Default dataDir per instance: /var/lib/ngit-grasp-<name>
- Update example to show single and multiple instance configs
- Add notes on systemd service management per instance
Diffstat (limited to 'nix/module.nix')
| -rw-r--r-- | nix/module.nix | 539 |
1 files changed, 289 insertions, 250 deletions
diff --git a/nix/module.nix b/nix/module.nix index 35154d2..39e7d8a 100644 --- a/nix/module.nix +++ b/nix/module.nix | |||
| @@ -3,9 +3,7 @@ | |||
| 3 | with lib; | 3 | with lib; |
| 4 | 4 | ||
| 5 | let | 5 | let |
| 6 | cfg = config.services.ngit-grasp; | 6 | # Build ngit-grasp package (shared across all instances) |
| 7 | |||
| 8 | # Build ngit-grasp package | ||
| 9 | ngit-grasp = pkgs.rustPlatform.buildRustPackage { | 7 | ngit-grasp = pkgs.rustPlatform.buildRustPackage { |
| 10 | pname = "ngit-grasp"; | 8 | pname = "ngit-grasp"; |
| 11 | version = "0.1.0"; | 9 | version = "0.1.0"; |
| @@ -22,281 +20,322 @@ let | |||
| 22 | buildInputs = with pkgs; [ openssl ]; | 20 | buildInputs = with pkgs; [ openssl ]; |
| 23 | }; | 21 | }; |
| 24 | 22 | ||
| 25 | in { | 23 | # Per-instance options |
| 26 | options.services.ngit-grasp = { | 24 | instanceOptions = { name, ... }: { |
| 27 | enable = mkEnableOption "ngit-grasp GRASP relay"; | 25 | options = { |
| 28 | 26 | enable = mkEnableOption "this ngit-grasp instance"; | |
| 29 | domain = mkOption { | ||
| 30 | type = types.str; | ||
| 31 | example = "ngit.example.com"; | ||
| 32 | description = | ||
| 33 | "Domain where this relay is hosted (used in GRASP validation)"; | ||
| 34 | }; | ||
| 35 | 27 | ||
| 36 | bindAddress = mkOption { | 28 | domain = mkOption { |
| 37 | type = types.str; | 29 | type = types.str; |
| 38 | default = "127.0.0.1"; | 30 | example = "ngit.example.com"; |
| 39 | description = "IP address to bind to"; | 31 | description = |
| 40 | }; | 32 | "Domain where this relay is hosted (used in GRASP validation)"; |
| 33 | }; | ||
| 41 | 34 | ||
| 42 | port = mkOption { | 35 | bindAddress = mkOption { |
| 43 | type = types.port; | 36 | type = types.str; |
| 44 | default = 8080; | 37 | default = "127.0.0.1"; |
| 45 | description = "Port to listen on"; | 38 | description = "IP address to bind to"; |
| 46 | }; | 39 | }; |
| 47 | 40 | ||
| 48 | dataDir = mkOption { | 41 | port = mkOption { |
| 49 | type = types.path; | 42 | type = types.port; |
| 50 | default = "/var/lib/ngit-grasp"; | 43 | default = 8080; |
| 51 | description = "Base directory for data storage"; | 44 | description = "Port to listen on"; |
| 52 | }; | 45 | }; |
| 53 | 46 | ||
| 54 | relayName = mkOption { | 47 | dataDir = mkOption { |
| 55 | type = types.nullOr types.str; | 48 | type = types.path; |
| 56 | default = null; | 49 | default = "/var/lib/ngit-grasp-${name}"; |
| 57 | example = "My GRASP Relay"; | 50 | description = "Base directory for data storage"; |
| 58 | description = | 51 | }; |
| 59 | "Relay name for NIP-11 (defaults to \${domain} grasp relay)"; | ||
| 60 | }; | ||
| 61 | 52 | ||
| 62 | relayDescription = mkOption { | 53 | relayName = mkOption { |
| 63 | type = types.str; | 54 | type = types.nullOr types.str; |
| 64 | default = "Git Nostr Relay - a grasp implementation"; | 55 | default = null; |
| 65 | description = "Relay description for NIP-11"; | 56 | example = "My GRASP Relay"; |
| 66 | }; | 57 | description = |
| 58 | "Relay name for NIP-11 (defaults to \${domain} grasp relay)"; | ||
| 59 | }; | ||
| 67 | 60 | ||
| 68 | relayOwnerNsecFile = mkOption { | 61 | relayDescription = mkOption { |
| 69 | type = types.nullOr types.path; | 62 | type = types.str; |
| 70 | default = null; | 63 | default = "Git Nostr Relay - a grasp implementation"; |
| 71 | example = "/persistent/ngit-grasp/relay-owner.nsec"; | 64 | description = "Relay description for NIP-11"; |
| 72 | description = '' | 65 | }; |
| 73 | Path to file containing relay owner's nsec (private key). | ||
| 74 | If file doesn't exist, ngit-grasp will auto-generate a random nsec and save it. | ||
| 75 | Takes precedence over relayOwnerNsec if both are set. | ||
| 76 | ''; | ||
| 77 | }; | ||
| 78 | 66 | ||
| 79 | relayOwnerNsec = mkOption { | 67 | relayOwnerNsecFile = mkOption { |
| 80 | type = types.nullOr types.str; | 68 | type = types.nullOr types.path; |
| 81 | default = null; | 69 | default = null; |
| 82 | example = "nsec1..."; | 70 | example = "/persistent/ngit-grasp/relay-owner.nsec"; |
| 83 | description = '' | 71 | description = '' |
| 84 | Relay owner's nsec (private key) for signing and authentication. | 72 | Path to file containing relay owner's nsec (private key). |
| 85 | Less secure than relayOwnerNsecFile as it ends up in nix store. | 73 | If file doesn't exist, ngit-grasp will auto-generate a random nsec and save it. |
| 86 | Only used if relayOwnerNsecFile is not set. | 74 | Takes precedence over relayOwnerNsec if both are set. |
| 87 | ''; | 75 | ''; |
| 88 | }; | 76 | }; |
| 89 | 77 | ||
| 90 | syncBootstrapRelayUrl = mkOption { | 78 | relayOwnerNsec = mkOption { |
| 91 | type = types.nullOr types.str; | 79 | type = types.nullOr types.str; |
| 92 | default = null; | 80 | default = null; |
| 93 | example = "wss://relay.ngit.dev"; | 81 | example = "nsec1..."; |
| 94 | description = "Bootstrap relay URL to sync from on startup (optional)"; | 82 | description = '' |
| 95 | }; | 83 | Relay owner's nsec (private key) for signing and authentication. |
| 84 | Less secure than relayOwnerNsecFile as it ends up in nix store. | ||
| 85 | Only used if relayOwnerNsecFile is not set. | ||
| 86 | ''; | ||
| 87 | }; | ||
| 96 | 88 | ||
| 97 | databaseBackend = mkOption { | 89 | syncBootstrapRelayUrl = mkOption { |
| 98 | type = types.enum [ "lmdb" "nostr-db" "memory" ]; | 90 | type = types.nullOr types.str; |
| 99 | default = "lmdb"; | 91 | default = null; |
| 100 | description = '' | 92 | example = "wss://relay.ngit.dev"; |
| 101 | Database backend type: | 93 | description = "Bootstrap relay URL to sync from on startup (optional)"; |
| 102 | - lmdb: LMDB backend (persistent, general purpose) | 94 | }; |
| 103 | - nostr-db: NostrDB backend (persistent, optimized for Nostr) | ||
| 104 | - memory: In-memory database (fastest, no persistence) | ||
| 105 | ''; | ||
| 106 | }; | ||
| 107 | 95 | ||
| 108 | metricsEnabled = mkOption { | 96 | databaseBackend = mkOption { |
| 109 | type = types.bool; | 97 | type = types.enum [ "lmdb" "nostr-db" "memory" ]; |
| 110 | default = true; | 98 | default = "lmdb"; |
| 111 | description = "Enable Prometheus metrics endpoint at /metrics"; | 99 | description = '' |
| 112 | }; | 100 | Database backend type: |
| 101 | - lmdb: LMDB backend (persistent, general purpose) | ||
| 102 | - nostr-db: NostrDB backend (persistent, optimized for Nostr) | ||
| 103 | - memory: In-memory database (fastest, no persistence) | ||
| 104 | ''; | ||
| 105 | }; | ||
| 113 | 106 | ||
| 114 | metricsConnectionPerIpAbuseThreshold = mkOption { | 107 | metricsEnabled = mkOption { |
| 115 | type = types.int; | 108 | type = types.bool; |
| 116 | default = 10; | 109 | default = true; |
| 117 | description = | 110 | description = "Enable Prometheus metrics endpoint at /metrics"; |
| 118 | "Connections per IP before flagging as potential abuse in metrics"; | 111 | }; |
| 119 | }; | ||
| 120 | 112 | ||
| 121 | metricsTopNRepos = mkOption { | 113 | metricsConnectionPerIpAbuseThreshold = mkOption { |
| 122 | type = types.int; | 114 | type = types.int; |
| 123 | default = 10; | 115 | default = 10; |
| 124 | description = "Number of top bandwidth repos to track in metrics"; | 116 | description = |
| 125 | }; | 117 | "Connections per IP before flagging as potential abuse in metrics"; |
| 118 | }; | ||
| 126 | 119 | ||
| 127 | logLevel = mkOption { | 120 | metricsTopNRepos = mkOption { |
| 128 | type = types.enum [ "trace" "debug" "info" "warn" "error" ]; | 121 | type = types.int; |
| 129 | default = "info"; | 122 | default = 10; |
| 130 | description = "Logging level for RUST_LOG environment variable"; | 123 | description = "Number of top bandwidth repos to track in metrics"; |
| 131 | }; | 124 | }; |
| 132 | 125 | ||
| 133 | syncMaxBackoffSecs = mkOption { | 126 | logLevel = mkOption { |
| 134 | type = types.int; | 127 | type = types.enum [ "trace" "debug" "info" "warn" "error" ]; |
| 135 | default = 3600; | 128 | default = "info"; |
| 136 | description = | 129 | description = "Logging level for RUST_LOG environment variable"; |
| 137 | "Maximum backoff time in seconds for sync relay reconnection (default: 1 hour)"; | 130 | }; |
| 138 | }; | ||
| 139 | 131 | ||
| 140 | syncDisconnectCheckIntervalSecs = mkOption { | 132 | syncMaxBackoffSecs = mkOption { |
| 141 | type = types.int; | 133 | type = types.int; |
| 142 | default = 60; | 134 | default = 3600; |
| 143 | description = "Interval in seconds for checking disconnected relays"; | 135 | description = |
| 144 | }; | 136 | "Maximum backoff time in seconds for sync relay reconnection (default: 1 hour)"; |
| 137 | }; | ||
| 145 | 138 | ||
| 146 | syncBaseBackoffSecs = mkOption { | 139 | syncDisconnectCheckIntervalSecs = mkOption { |
| 147 | type = types.int; | 140 | type = types.int; |
| 148 | default = 5; | 141 | default = 60; |
| 149 | description = "Base backoff time in seconds for relay reconnection"; | 142 | description = "Interval in seconds for checking disconnected relays"; |
| 150 | }; | 143 | }; |
| 151 | 144 | ||
| 152 | syncDisableNegentropy = mkOption { | 145 | syncBaseBackoffSecs = mkOption { |
| 153 | type = types.bool; | 146 | type = types.int; |
| 154 | default = false; | 147 | default = 5; |
| 155 | description = "Disable NIP-77 negentropy sync (use REQ+EOSE instead)"; | 148 | description = "Base backoff time in seconds for relay reconnection"; |
| 156 | }; | 149 | }; |
| 157 | 150 | ||
| 158 | rejectedHotCacheDurationSecs = mkOption { | 151 | syncDisableNegentropy = mkOption { |
| 159 | type = types.int; | 152 | type = types.bool; |
| 160 | default = 120; | 153 | default = false; |
| 161 | description = | 154 | description = "Disable NIP-77 negentropy sync (use REQ+EOSE instead)"; |
| 162 | "Hot cache duration in seconds for rejected announcements (default: 2 minutes)"; | 155 | }; |
| 163 | }; | ||
| 164 | 156 | ||
| 165 | rejectedColdIndexExpirySecs = mkOption { | 157 | rejectedHotCacheDurationSecs = mkOption { |
| 166 | type = types.int; | 158 | type = types.int; |
| 167 | default = 604800; | 159 | default = 120; |
| 168 | description = | 160 | description = |
| 169 | "Cold index expiry in seconds for rejected announcements (default: 7 days)"; | 161 | "Hot cache duration in seconds for rejected announcements (default: 2 minutes)"; |
| 170 | }; | 162 | }; |
| 171 | 163 | ||
| 172 | naughtyListExpirationHours = mkOption { | 164 | rejectedColdIndexExpirySecs = mkOption { |
| 173 | type = types.int; | 165 | type = types.int; |
| 174 | default = 12; | 166 | default = 604800; |
| 175 | description = "Hours before removing relay from naughty list"; | 167 | description = |
| 176 | }; | 168 | "Cold index expiry in seconds for rejected announcements (default: 7 days)"; |
| 169 | }; | ||
| 177 | 170 | ||
| 178 | user = mkOption { | 171 | naughtyListExpirationHours = mkOption { |
| 179 | type = types.str; | 172 | type = types.int; |
| 180 | default = "ngit-grasp"; | 173 | default = 12; |
| 181 | description = "User account under which ngit-grasp runs"; | 174 | description = "Hours before removing relay from naughty list"; |
| 182 | }; | 175 | }; |
| 183 | 176 | ||
| 184 | group = mkOption { | 177 | user = mkOption { |
| 185 | type = types.str; | 178 | type = types.str; |
| 186 | default = "ngit-grasp"; | 179 | default = "ngit-grasp-${name}"; |
| 187 | description = "Group under which ngit-grasp runs"; | 180 | description = "User account under which this instance runs"; |
| 181 | }; | ||
| 182 | |||
| 183 | group = mkOption { | ||
| 184 | type = types.str; | ||
| 185 | default = "ngit-grasp"; | ||
| 186 | description = "Group under which this instance runs"; | ||
| 187 | }; | ||
| 188 | }; | 188 | }; |
| 189 | }; | 189 | }; |
| 190 | 190 | ||
| 191 | config = mkIf cfg.enable { | 191 | # Create systemd service config for an instance |
| 192 | # Create user and group | 192 | mkService = name: cfg: { |
| 193 | users.users.${cfg.user} = { | 193 | description = "ngit-grasp GRASP relay (${name})"; |
| 194 | isSystemUser = true; | 194 | after = [ "network.target" ]; |
| 195 | group = cfg.group; | 195 | wantedBy = [ "multi-user.target" ]; |
| 196 | description = "ngit-grasp service user"; | 196 | |
| 197 | home = cfg.dataDir; | 197 | environment = { |
| 198 | NGIT_DOMAIN = cfg.domain; | ||
| 199 | NGIT_BIND_ADDRESS = "${cfg.bindAddress}:${toString cfg.port}"; | ||
| 200 | NGIT_GIT_DATA_PATH = "${cfg.dataDir}/git"; | ||
| 201 | NGIT_RELAY_DATA_PATH = "${cfg.dataDir}/relay"; | ||
| 202 | NGIT_RELAY_DESCRIPTION = cfg.relayDescription; | ||
| 203 | NGIT_DATABASE_BACKEND = cfg.databaseBackend; | ||
| 204 | NGIT_METRICS_CONNECTION_PER_IP_ABUSE_THRESHOLD = | ||
| 205 | toString cfg.metricsConnectionPerIpAbuseThreshold; | ||
| 206 | NGIT_METRICS_TOP_N_REPOS = toString cfg.metricsTopNRepos; | ||
| 207 | NGIT_SYNC_MAX_BACKOFF_SECS = toString cfg.syncMaxBackoffSecs; | ||
| 208 | NGIT_SYNC_DISCONNECT_CHECK_INTERVAL_SECS = | ||
| 209 | toString cfg.syncDisconnectCheckIntervalSecs; | ||
| 210 | NGIT_SYNC_BASE_BACKOFF_SECS = toString cfg.syncBaseBackoffSecs; | ||
| 211 | NGIT_REJECTED_HOT_CACHE_DURATION_SECS = | ||
| 212 | toString cfg.rejectedHotCacheDurationSecs; | ||
| 213 | NGIT_REJECTED_COLD_INDEX_EXPIRY_SECS = | ||
| 214 | toString cfg.rejectedColdIndexExpirySecs; | ||
| 215 | NGIT_NAUGHTY_LIST_EXPIRATION_HOURS = | ||
| 216 | toString cfg.naughtyListExpirationHours; | ||
| 217 | RUST_LOG = cfg.logLevel; | ||
| 218 | } // optionalAttrs (cfg.relayName != null) { | ||
| 219 | NGIT_RELAY_NAME = cfg.relayName; | ||
| 220 | } // optionalAttrs cfg.metricsEnabled { NGIT_METRICS_ENABLED = "true"; } | ||
| 221 | // optionalAttrs (cfg.syncBootstrapRelayUrl != null) { | ||
| 222 | NGIT_SYNC_BOOTSTRAP_RELAY_URL = cfg.syncBootstrapRelayUrl; | ||
| 223 | } // optionalAttrs cfg.syncDisableNegentropy { | ||
| 224 | NGIT_SYNC_DISABLE_NEGENTROPY = "true"; | ||
| 225 | } // optionalAttrs | ||
| 226 | (cfg.relayOwnerNsec != null && cfg.relayOwnerNsecFile == null) { | ||
| 227 | # Only set inline nsec if file is not specified | ||
| 228 | NGIT_RELAY_OWNER_NSEC = cfg.relayOwnerNsec; | ||
| 229 | }; | ||
| 230 | |||
| 231 | serviceConfig = { | ||
| 232 | Type = "simple"; | ||
| 233 | User = cfg.user; | ||
| 234 | Group = cfg.group; | ||
| 235 | |||
| 236 | # Working directory where .relay-owner.nsec will be created if needed | ||
| 237 | WorkingDirectory = cfg.dataDir; | ||
| 238 | |||
| 239 | # Command to run | ||
| 240 | ExecStart = if cfg.relayOwnerNsecFile != null then | ||
| 241 | # Use nsec from file | ||
| 242 | "${ngit-grasp}/bin/ngit-grasp --relay-owner-nsec $(cat ${cfg.relayOwnerNsecFile})" | ||
| 243 | else | ||
| 244 | # Let ngit-grasp auto-generate nsec in .relay-owner.nsec file in dataDir | ||
| 245 | "${ngit-grasp}/bin/ngit-grasp"; | ||
| 246 | |||
| 247 | # Restart policy | ||
| 248 | Restart = "always"; | ||
| 249 | RestartSec = "10s"; | ||
| 250 | |||
| 251 | # Hardening | ||
| 252 | NoNewPrivileges = true; | ||
| 253 | PrivateTmp = true; | ||
| 254 | ProtectSystem = "strict"; | ||
| 255 | ProtectHome = true; | ||
| 256 | ReadWritePaths = [ cfg.dataDir ]; | ||
| 257 | |||
| 258 | # If using nsecFile, grant read access | ||
| 259 | ReadOnlyPaths = | ||
| 260 | optionals (cfg.relayOwnerNsecFile != null) [ cfg.relayOwnerNsecFile ]; | ||
| 261 | |||
| 262 | # Additional hardening | ||
| 263 | ProtectKernelTunables = true; | ||
| 264 | ProtectKernelModules = true; | ||
| 265 | ProtectControlGroups = true; | ||
| 266 | RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; | ||
| 267 | RestrictNamespaces = true; | ||
| 268 | LockPersonality = true; | ||
| 269 | RestrictRealtime = true; | ||
| 270 | RestrictSUIDSGID = true; | ||
| 271 | PrivateDevices = true; | ||
| 272 | |||
| 273 | # Capabilities | ||
| 274 | CapabilityBoundingSet = ""; | ||
| 275 | AmbientCapabilities = ""; | ||
| 276 | |||
| 277 | # System call filtering | ||
| 278 | SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ]; | ||
| 279 | SystemCallErrorNumber = "EPERM"; | ||
| 198 | }; | 280 | }; |
| 199 | 281 | ||
| 200 | users.groups.${cfg.group} = { }; | 282 | # Ensure data directories exist before starting |
| 201 | 283 | preStart = '' | |
| 202 | # Create systemd service | 284 | mkdir -p ${cfg.dataDir}/git |
| 203 | systemd.services.ngit-grasp = { | 285 | mkdir -p ${cfg.dataDir}/relay |
| 204 | description = "ngit-grasp GRASP relay"; | 286 | chown -R ${cfg.user}:${cfg.group} ${cfg.dataDir} |
| 205 | after = [ "network.target" ]; | 287 | chmod 750 ${cfg.dataDir} |
| 206 | wantedBy = [ "multi-user.target" ]; | 288 | ''; |
| 207 | 289 | }; | |
| 208 | environment = { | ||
| 209 | NGIT_DOMAIN = cfg.domain; | ||
| 210 | NGIT_BIND_ADDRESS = "${cfg.bindAddress}:${toString cfg.port}"; | ||
| 211 | NGIT_GIT_DATA_PATH = "${cfg.dataDir}/git"; | ||
| 212 | NGIT_RELAY_DATA_PATH = "${cfg.dataDir}/relay"; | ||
| 213 | NGIT_RELAY_DESCRIPTION = cfg.relayDescription; | ||
| 214 | NGIT_DATABASE_BACKEND = cfg.databaseBackend; | ||
| 215 | NGIT_METRICS_CONNECTION_PER_IP_ABUSE_THRESHOLD = | ||
| 216 | toString cfg.metricsConnectionPerIpAbuseThreshold; | ||
| 217 | NGIT_METRICS_TOP_N_REPOS = toString cfg.metricsTopNRepos; | ||
| 218 | NGIT_SYNC_MAX_BACKOFF_SECS = toString cfg.syncMaxBackoffSecs; | ||
| 219 | NGIT_SYNC_DISCONNECT_CHECK_INTERVAL_SECS = | ||
| 220 | toString cfg.syncDisconnectCheckIntervalSecs; | ||
| 221 | NGIT_SYNC_BASE_BACKOFF_SECS = toString cfg.syncBaseBackoffSecs; | ||
| 222 | NGIT_REJECTED_HOT_CACHE_DURATION_SECS = | ||
| 223 | toString cfg.rejectedHotCacheDurationSecs; | ||
| 224 | NGIT_REJECTED_COLD_INDEX_EXPIRY_SECS = | ||
| 225 | toString cfg.rejectedColdIndexExpirySecs; | ||
| 226 | NGIT_NAUGHTY_LIST_EXPIRATION_HOURS = | ||
| 227 | toString cfg.naughtyListExpirationHours; | ||
| 228 | RUST_LOG = cfg.logLevel; | ||
| 229 | } // optionalAttrs (cfg.relayName != null) { | ||
| 230 | NGIT_RELAY_NAME = cfg.relayName; | ||
| 231 | } // optionalAttrs cfg.metricsEnabled { NGIT_METRICS_ENABLED = "true"; } | ||
| 232 | // optionalAttrs (cfg.syncBootstrapRelayUrl != null) { | ||
| 233 | NGIT_SYNC_BOOTSTRAP_RELAY_URL = cfg.syncBootstrapRelayUrl; | ||
| 234 | } // optionalAttrs cfg.syncDisableNegentropy { | ||
| 235 | NGIT_SYNC_DISABLE_NEGENTROPY = "true"; | ||
| 236 | } // optionalAttrs | ||
| 237 | (cfg.relayOwnerNsec != null && cfg.relayOwnerNsecFile == null) { | ||
| 238 | # Only set inline nsec if file is not specified | ||
| 239 | NGIT_RELAY_OWNER_NSEC = cfg.relayOwnerNsec; | ||
| 240 | }; | ||
| 241 | 290 | ||
| 242 | serviceConfig = { | 291 | enabledInstances = |
| 243 | Type = "simple"; | 292 | filterAttrs (_: cfg: cfg.enable) config.services.ngit-grasp; |
| 244 | User = cfg.user; | ||
| 245 | Group = cfg.group; | ||
| 246 | |||
| 247 | # Working directory where .relay-owner.nsec will be created if needed | ||
| 248 | WorkingDirectory = cfg.dataDir; | ||
| 249 | |||
| 250 | # Command to run | ||
| 251 | ExecStart = if cfg.relayOwnerNsecFile != null then | ||
| 252 | # Use nsec from file | ||
| 253 | "${ngit-grasp}/bin/ngit-grasp --relay-owner-nsec $(cat ${cfg.relayOwnerNsecFile})" | ||
| 254 | else | ||
| 255 | # Let ngit-grasp auto-generate nsec in .relay-owner.nsec file in dataDir | ||
| 256 | "${ngit-grasp}/bin/ngit-grasp"; | ||
| 257 | |||
| 258 | # Restart policy | ||
| 259 | Restart = "always"; | ||
| 260 | RestartSec = "10s"; | ||
| 261 | |||
| 262 | # Hardening | ||
| 263 | NoNewPrivileges = true; | ||
| 264 | PrivateTmp = true; | ||
| 265 | ProtectSystem = "strict"; | ||
| 266 | ProtectHome = true; | ||
| 267 | ReadWritePaths = [ cfg.dataDir ]; | ||
| 268 | |||
| 269 | # If using nsecFile, grant read access | ||
| 270 | ReadOnlyPaths = | ||
| 271 | optionals (cfg.relayOwnerNsecFile != null) [ cfg.relayOwnerNsecFile ]; | ||
| 272 | |||
| 273 | # Additional hardening | ||
| 274 | ProtectKernelTunables = true; | ||
| 275 | ProtectKernelModules = true; | ||
| 276 | ProtectControlGroups = true; | ||
| 277 | RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; | ||
| 278 | RestrictNamespaces = true; | ||
| 279 | LockPersonality = true; | ||
| 280 | RestrictRealtime = true; | ||
| 281 | RestrictSUIDSGID = true; | ||
| 282 | PrivateDevices = true; | ||
| 283 | |||
| 284 | # Capabilities | ||
| 285 | CapabilityBoundingSet = ""; | ||
| 286 | AmbientCapabilities = ""; | ||
| 287 | |||
| 288 | # System call filtering | ||
| 289 | SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ]; | ||
| 290 | SystemCallErrorNumber = "EPERM"; | ||
| 291 | }; | ||
| 292 | 293 | ||
| 293 | # Ensure data directories exist before starting | 294 | in { |
| 294 | preStart = '' | 295 | options.services.ngit-grasp = mkOption { |
| 295 | mkdir -p ${cfg.dataDir}/git | 296 | type = types.attrsOf (types.submodule instanceOptions); |
| 296 | mkdir -p ${cfg.dataDir}/relay | 297 | default = { }; |
| 297 | chown -R ${cfg.user}:${cfg.group} ${cfg.dataDir} | 298 | description = '' |
| 298 | chmod 750 ${cfg.dataDir} | 299 | ngit-grasp GRASP relay instances. |
| 299 | ''; | 300 | |
| 300 | }; | 301 | Multiple instances can be configured with different domains and ports. |
| 302 | Each instance runs as a separate systemd service. | ||
| 303 | ''; | ||
| 304 | example = literalExpression '' | ||
| 305 | { | ||
| 306 | production = { | ||
| 307 | enable = true; | ||
| 308 | domain = "ngit.example.com"; | ||
| 309 | port = 8082; | ||
| 310 | dataDir = "/persistent/ngit-production"; | ||
| 311 | }; | ||
| 312 | |||
| 313 | testing = { | ||
| 314 | enable = true; | ||
| 315 | domain = "ngit-test.example.com"; | ||
| 316 | port = 8083; | ||
| 317 | dataDir = "/persistent/ngit-testing"; | ||
| 318 | }; | ||
| 319 | } | ||
| 320 | ''; | ||
| 321 | }; | ||
| 322 | |||
| 323 | config = mkIf (enabledInstances != { }) { | ||
| 324 | # Create users for all enabled instances | ||
| 325 | users.users = mapAttrs' (name: cfg: | ||
| 326 | nameValuePair cfg.user { | ||
| 327 | isSystemUser = true; | ||
| 328 | group = cfg.group; | ||
| 329 | description = "ngit-grasp service user (${name})"; | ||
| 330 | home = cfg.dataDir; | ||
| 331 | }) enabledInstances; | ||
| 332 | |||
| 333 | # Create shared group (all instances use the same group by default) | ||
| 334 | users.groups.ngit-grasp = { }; | ||
| 335 | |||
| 336 | # Create systemd services for all enabled instances | ||
| 337 | systemd.services = mapAttrs' | ||
| 338 | (name: cfg: nameValuePair "ngit-grasp-${name}" (mkService name cfg)) | ||
| 339 | enabledInstances; | ||
| 301 | }; | 340 | }; |
| 302 | } | 341 | } |