upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--nix/example-configuration.nix111
-rw-r--r--nix/module.nix539
2 files changed, 382 insertions, 268 deletions
diff --git a/nix/example-configuration.nix b/nix/example-configuration.nix
index a00d970..34615be 100644
--- a/nix/example-configuration.nix
+++ b/nix/example-configuration.nix
@@ -1,4 +1,4 @@
1# Example NixOS configuration using ngit-grasp module 1# Example NixOS configurations using ngit-grasp module
2# 2#
3# Usage: 3# Usage:
4# 1. Add to your server's flake.nix inputs: 4# 1. Add to your server's flake.nix inputs:
@@ -7,48 +7,54 @@
7# 2. Import the module in your configuration: 7# 2. Import the module in your configuration:
8# imports = [ inputs.ngit-grasp.nixosModules.default ]; 8# imports = [ inputs.ngit-grasp.nixosModules.default ];
9# 9#
10# 3. Configure the service (example below) 10# 3. Configure one or more instances (examples below)
11 11
12{ inputs, ... }: 12{ inputs, ... }:
13 13
14{ 14{
15 imports = [ inputs.ngit-grasp.nixosModules.default ]; 15 imports = [ inputs.ngit-grasp.nixosModules.default ];
16 16
17 services.ngit-grasp = { 17 # ============================================================================
18 # EXAMPLE 1: Single Instance Configuration
19 # ============================================================================
20
21 services.ngit-grasp.production = {
18 enable = true; 22 enable = true;
19 domain = "ngit.danconwaydev.com"; 23 domain = "ngit.danconwaydev.com";
20 24
21 # Network 25 # Network
22 bindAddress = "127.0.0.1"; 26 bindAddress = "127.0.0.1";
23 port = 8082; # Same port as current ngit-relay for Caddy compatibility 27 port = 8082;
24 28
25 # Storage (reuse existing persistent path pattern) 29 # Storage
26 dataDir = "/persistent/ngit-danconwaydev-com-ngit-grasp"; 30 dataDir = "/persistent/ngit-danconwaydev-com-ngit-grasp";
27 31
28 # Identity 32 # Identity
29 relayName = "DanConwayDev's ngit-grasp"; 33 relayName = "DanConwayDev's ngit-grasp";
30 relayDescription = "personal instance of ngit-grasp, a Rust GRASP implementation with proactive sync"; 34 relayDescription =
31 35 "personal instance of ngit-grasp, a Rust GRASP implementation with proactive sync";
36
32 # Option 1: Use nsec file (recommended - more secure) 37 # Option 1: Use nsec file (recommended - more secure)
33 relayOwnerNsecFile = "/persistent/ngit-danconwaydev-com-ngit-grasp/relay-owner.nsec"; 38 relayOwnerNsecFile =
34 39 "/persistent/ngit-danconwaydev-com-ngit-grasp/relay-owner.nsec";
40
35 # Option 2: Inline nsec (less secure, ends up in nix store) 41 # Option 2: Inline nsec (less secure, ends up in nix store)
36 # relayOwnerNsec = "nsec1..."; 42 # relayOwnerNsec = "nsec1...";
37 43
38 # Option 3: Auto-generate (default if neither above is set) 44 # Option 3: Auto-generate (default if neither above is set)
39 # ngit-grasp will create .relay-owner.nsec in dataDir automatically 45 # ngit-grasp will create .relay-owner.nsec in dataDir automatically
40 46
41 # Sync 47 # Sync
42 syncBootstrapRelayUrl = "wss://relay.ngit.dev"; 48 syncBootstrapRelayUrl = "wss://relay.ngit.dev";
43 49
44 # Metrics 50 # Metrics
45 metricsEnabled = true; 51 metricsEnabled = true;
46 52
47 # Logging 53 # Logging
48 logLevel = "info"; # Options: trace, debug, info, warn, error 54 logLevel = "info"; # Options: trace, debug, info, warn, error
49 }; 55 };
50 56
51 # Caddy reverse proxy (unchanged from current setup) 57 # Caddy reverse proxy for production instance
52 services.caddy.virtualHosts."ngit.danconwaydev.com" = { 58 services.caddy.virtualHosts."ngit.danconwaydev.com" = {
53 extraConfig = '' 59 extraConfig = ''
54 reverse_proxy 127.0.0.1:8082 { 60 reverse_proxy 127.0.0.1:8082 {
@@ -57,4 +63,73 @@
57 } 63 }
58 ''; 64 '';
59 }; 65 };
66
67 # ============================================================================
68 # EXAMPLE 2: Multiple Instances on Same Server
69 # ============================================================================
70
71 # Uncomment to run multiple instances:
72
73 # # Production instance
74 # services.ngit-grasp.prod = {
75 # enable = true;
76 # domain = "ngit.example.com";
77 # port = 8082;
78 # dataDir = "/persistent/ngit-production";
79 # relayName = "Production GRASP Relay";
80 # syncBootstrapRelayUrl = "wss://relay.ngit.dev";
81 # logLevel = "info";
82 # };
83 #
84 # # Testing/staging instance
85 # services.ngit-grasp.staging = {
86 # enable = true;
87 # domain = "ngit-staging.example.com";
88 # port = 8083;
89 # dataDir = "/persistent/ngit-staging";
90 # relayName = "Staging GRASP Relay";
91 # syncBootstrapRelayUrl = "wss://relay.ngit.dev";
92 # logLevel = "debug"; # More verbose logging for testing
93 # };
94 #
95 # # Development instance with in-memory database
96 # services.ngit-grasp.dev = {
97 # enable = true;
98 # domain = "localhost";
99 # bindAddress = "127.0.0.1";
100 # port = 8084;
101 # dataDir = "/tmp/ngit-dev";
102 # databaseBackend = "memory"; # No persistence
103 # relayName = "Development GRASP Relay";
104 # metricsEnabled = false;
105 # logLevel = "trace"; # Maximum verbosity for debugging
106 # };
107 #
108 # # Caddy configuration for multiple instances
109 # services.caddy.virtualHosts = {
110 # "ngit.example.com" = {
111 # extraConfig = "reverse_proxy 127.0.0.1:8082";
112 # };
113 # "ngit-staging.example.com" = {
114 # extraConfig = "reverse_proxy 127.0.0.1:8083";
115 # };
116 # };
117
118 # ============================================================================
119 # NOTES
120 # ============================================================================
121
122 # Instance names (e.g., "production", "prod", "staging") can be anything.
123 # They are used for:
124 # - systemd service names: ngit-grasp-<name>
125 # - default user names: ngit-grasp-<name>
126 # - default data directories: /var/lib/ngit-grasp-<name>
127
128 # Systemd service management:
129 # systemctl status ngit-grasp-production
130 # systemctl restart ngit-grasp-staging
131 # journalctl -u ngit-grasp-prod -f
132
133 # Each instance runs as a separate user but shares the same group by default.
134 # You can customize user/group per instance if needed.
60} 135}
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 @@
3with lib; 3with lib;
4 4
5let 5let
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
25in { 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 294in {
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}