upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-01-10 21:55:28 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-01-10 21:55:28 +0000
commit8536be07962ee6b23ecca0f1c084db11a3c104e0 (patch)
treeeb53898684353527958a6ff3ae16c5cd19df8c56
parenta9ff76e7e294fb54ae3a6876bca3e30ac6a5bdef (diff)
feat: add NixOS module for deployment
- Create nix/module.nix with comprehensive systemd service - Support both relayOwnerNsecFile and relayOwnerNsec options - Auto-generate nsec if neither specified - Add security hardening (NoNewPrivileges, ProtectSystem, etc.) - Expose as nixosModules.default and nixosModules.ngit-grasp - Include example configuration in nix/example-configuration.nix - Add outputHashes for nostr git dependency
-rw-r--r--flake.nix47
-rw-r--r--nix/example-configuration.nix60
-rw-r--r--nix/module.nix302
3 files changed, 384 insertions, 25 deletions
diff --git a/flake.nix b/flake.nix
index 6edab71..71749b9 100644
--- a/flake.nix
+++ b/flake.nix
@@ -8,28 +8,20 @@
8 }; 8 };
9 9
10 outputs = { self, nixpkgs, rust-overlay, flake-utils }: 10 outputs = { self, nixpkgs, rust-overlay, flake-utils }:
11 flake-utils.lib.eachDefaultSystem (system: 11 (flake-utils.lib.eachDefaultSystem (system:
12 let 12 let
13 overlays = [ (import rust-overlay) ]; 13 overlays = [ (import rust-overlay) ];
14 pkgs = import nixpkgs { 14 pkgs = import nixpkgs { inherit system overlays; };
15 inherit system overlays; 15
16 };
17
18 rustToolchain = pkgs.rust-bin.stable.latest.default.override { 16 rustToolchain = pkgs.rust-bin.stable.latest.default.override {
19 extensions = [ "rust-src" "rust-analyzer" ]; 17 extensions = [ "rust-src" "rust-analyzer" ];
20 }; 18 };
21 in 19 in {
22 {
23 devShells.default = pkgs.mkShell { 20 devShells.default = pkgs.mkShell {
24 buildInputs = with pkgs; [ 21 buildInputs = with pkgs; [ rustToolchain pkg-config openssl git ];
25 rustToolchain
26 pkg-config
27 openssl
28 git
29 ];
30 22
31 RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library"; 23 RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library";
32 24
33 shellHook = '' 25 shellHook = ''
34 echo "🚀 ngit-grasp development environment" 26 echo "🚀 ngit-grasp development environment"
35 echo "Rust version: $(rustc --version)" 27 echo "Rust version: $(rustc --version)"
@@ -46,16 +38,21 @@
46 pname = "ngit-grasp"; 38 pname = "ngit-grasp";
47 version = "0.1.0"; 39 version = "0.1.0";
48 src = ./.; 40 src = ./.;
49 cargoLock.lockFile = ./Cargo.lock; 41 cargoLock = {
50 42 lockFile = ./Cargo.lock;
51 nativeBuildInputs = with pkgs; [ 43 outputHashes = {
52 pkg-config 44 "nostr-0.44.1" =
53 ]; 45 "sha256-02cawkx6bxfi3bn1sb5ws8cn9wzcwsk8cdv1vx8h8lad1jdic1qg";
54 46 };
55 buildInputs = with pkgs; [ 47 };
56 openssl 48
57 ]; 49 nativeBuildInputs = with pkgs; [ pkg-config ];
50
51 buildInputs = with pkgs; [ openssl ];
58 }; 52 };
59 } 53 })) // {
60 ); 54 # NixOS module for deployment
55 nixosModules.default = import ./nix/module.nix;
56 nixosModules.ngit-grasp = self.nixosModules.default;
57 };
61} 58}
diff --git a/nix/example-configuration.nix b/nix/example-configuration.nix
new file mode 100644
index 0000000..a00d970
--- /dev/null
+++ b/nix/example-configuration.nix
@@ -0,0 +1,60 @@
1# Example NixOS configuration using ngit-grasp module
2#
3# Usage:
4# 1. Add to your server's flake.nix inputs:
5# inputs.ngit-grasp.url = "github:DanConwayDev/ngit-grasp";
6#
7# 2. Import the module in your configuration:
8# imports = [ inputs.ngit-grasp.nixosModules.default ];
9#
10# 3. Configure the service (example below)
11
12{ inputs, ... }:
13
14{
15 imports = [ inputs.ngit-grasp.nixosModules.default ];
16
17 services.ngit-grasp = {
18 enable = true;
19 domain = "ngit.danconwaydev.com";
20
21 # Network
22 bindAddress = "127.0.0.1";
23 port = 8082; # Same port as current ngit-relay for Caddy compatibility
24
25 # Storage (reuse existing persistent path pattern)
26 dataDir = "/persistent/ngit-danconwaydev-com-ngit-grasp";
27
28 # Identity
29 relayName = "DanConwayDev's ngit-grasp";
30 relayDescription = "personal instance of ngit-grasp, a Rust GRASP implementation with proactive sync";
31
32 # Option 1: Use nsec file (recommended - more secure)
33 relayOwnerNsecFile = "/persistent/ngit-danconwaydev-com-ngit-grasp/relay-owner.nsec";
34
35 # Option 2: Inline nsec (less secure, ends up in nix store)
36 # relayOwnerNsec = "nsec1...";
37
38 # Option 3: Auto-generate (default if neither above is set)
39 # ngit-grasp will create .relay-owner.nsec in dataDir automatically
40
41 # Sync
42 syncBootstrapRelayUrl = "wss://relay.ngit.dev";
43
44 # Metrics
45 metricsEnabled = true;
46
47 # Logging
48 logLevel = "info"; # Options: trace, debug, info, warn, error
49 };
50
51 # Caddy reverse proxy (unchanged from current setup)
52 services.caddy.virtualHosts."ngit.danconwaydev.com" = {
53 extraConfig = ''
54 reverse_proxy 127.0.0.1:8082 {
55 header_down X-Real-IP {http.request.remote}
56 header_down X-Forwarded-For {http.request.remote}
57 }
58 '';
59 };
60}
diff --git a/nix/module.nix b/nix/module.nix
new file mode 100644
index 0000000..35154d2
--- /dev/null
+++ b/nix/module.nix
@@ -0,0 +1,302 @@
1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.ngit-grasp;
7
8 # Build ngit-grasp package
9 ngit-grasp = pkgs.rustPlatform.buildRustPackage {
10 pname = "ngit-grasp";
11 version = "0.1.0";
12 src = ../.;
13 cargoLock = {
14 lockFile = ../Cargo.lock;
15 outputHashes = {
16 "nostr-0.44.1" =
17 "sha256-02cawkx6bxfi3bn1sb5ws8cn9wzcwsk8cdv1vx8h8lad1jdic1qg";
18 };
19 };
20
21 nativeBuildInputs = with pkgs; [ pkg-config ];
22 buildInputs = with pkgs; [ openssl ];
23 };
24
25in {
26 options.services.ngit-grasp = {
27 enable = mkEnableOption "ngit-grasp GRASP relay";
28
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
36 bindAddress = mkOption {
37 type = types.str;
38 default = "127.0.0.1";
39 description = "IP address to bind to";
40 };
41
42 port = mkOption {
43 type = types.port;
44 default = 8080;
45 description = "Port to listen on";
46 };
47
48 dataDir = mkOption {
49 type = types.path;
50 default = "/var/lib/ngit-grasp";
51 description = "Base directory for data storage";
52 };
53
54 relayName = mkOption {
55 type = types.nullOr types.str;
56 default = null;
57 example = "My GRASP Relay";
58 description =
59 "Relay name for NIP-11 (defaults to \${domain} grasp relay)";
60 };
61
62 relayDescription = mkOption {
63 type = types.str;
64 default = "Git Nostr Relay - a grasp implementation";
65 description = "Relay description for NIP-11";
66 };
67
68 relayOwnerNsecFile = mkOption {
69 type = types.nullOr types.path;
70 default = null;
71 example = "/persistent/ngit-grasp/relay-owner.nsec";
72 description = ''
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
79 relayOwnerNsec = mkOption {
80 type = types.nullOr types.str;
81 default = null;
82 example = "nsec1...";
83 description = ''
84 Relay owner's nsec (private key) for signing and authentication.
85 Less secure than relayOwnerNsecFile as it ends up in nix store.
86 Only used if relayOwnerNsecFile is not set.
87 '';
88 };
89
90 syncBootstrapRelayUrl = mkOption {
91 type = types.nullOr types.str;
92 default = null;
93 example = "wss://relay.ngit.dev";
94 description = "Bootstrap relay URL to sync from on startup (optional)";
95 };
96
97 databaseBackend = mkOption {
98 type = types.enum [ "lmdb" "nostr-db" "memory" ];
99 default = "lmdb";
100 description = ''
101 Database backend type:
102 - lmdb: LMDB backend (persistent, general purpose)
103 - nostr-db: NostrDB backend (persistent, optimized for Nostr)
104 - memory: In-memory database (fastest, no persistence)
105 '';
106 };
107
108 metricsEnabled = mkOption {
109 type = types.bool;
110 default = true;
111 description = "Enable Prometheus metrics endpoint at /metrics";
112 };
113
114 metricsConnectionPerIpAbuseThreshold = mkOption {
115 type = types.int;
116 default = 10;
117 description =
118 "Connections per IP before flagging as potential abuse in metrics";
119 };
120
121 metricsTopNRepos = mkOption {
122 type = types.int;
123 default = 10;
124 description = "Number of top bandwidth repos to track in metrics";
125 };
126
127 logLevel = mkOption {
128 type = types.enum [ "trace" "debug" "info" "warn" "error" ];
129 default = "info";
130 description = "Logging level for RUST_LOG environment variable";
131 };
132
133 syncMaxBackoffSecs = mkOption {
134 type = types.int;
135 default = 3600;
136 description =
137 "Maximum backoff time in seconds for sync relay reconnection (default: 1 hour)";
138 };
139
140 syncDisconnectCheckIntervalSecs = mkOption {
141 type = types.int;
142 default = 60;
143 description = "Interval in seconds for checking disconnected relays";
144 };
145
146 syncBaseBackoffSecs = mkOption {
147 type = types.int;
148 default = 5;
149 description = "Base backoff time in seconds for relay reconnection";
150 };
151
152 syncDisableNegentropy = mkOption {
153 type = types.bool;
154 default = false;
155 description = "Disable NIP-77 negentropy sync (use REQ+EOSE instead)";
156 };
157
158 rejectedHotCacheDurationSecs = mkOption {
159 type = types.int;
160 default = 120;
161 description =
162 "Hot cache duration in seconds for rejected announcements (default: 2 minutes)";
163 };
164
165 rejectedColdIndexExpirySecs = mkOption {
166 type = types.int;
167 default = 604800;
168 description =
169 "Cold index expiry in seconds for rejected announcements (default: 7 days)";
170 };
171
172 naughtyListExpirationHours = mkOption {
173 type = types.int;
174 default = 12;
175 description = "Hours before removing relay from naughty list";
176 };
177
178 user = mkOption {
179 type = types.str;
180 default = "ngit-grasp";
181 description = "User account under which ngit-grasp runs";
182 };
183
184 group = mkOption {
185 type = types.str;
186 default = "ngit-grasp";
187 description = "Group under which ngit-grasp runs";
188 };
189 };
190
191 config = mkIf cfg.enable {
192 # Create user and group
193 users.users.${cfg.user} = {
194 isSystemUser = true;
195 group = cfg.group;
196 description = "ngit-grasp service user";
197 home = cfg.dataDir;
198 };
199
200 users.groups.${cfg.group} = { };
201
202 # Create systemd service
203 systemd.services.ngit-grasp = {
204 description = "ngit-grasp GRASP relay";
205 after = [ "network.target" ];
206 wantedBy = [ "multi-user.target" ];
207
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
242 serviceConfig = {
243 Type = "simple";
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 # Ensure data directories exist before starting
294 preStart = ''
295 mkdir -p ${cfg.dataDir}/git
296 mkdir -p ${cfg.dataDir}/relay
297 chown -R ${cfg.user}:${cfg.group} ${cfg.dataDir}
298 chmod 750 ${cfg.dataDir}
299 '';
300 };
301 };
302}