mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
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
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Example NixOS configuration using ngit-grasp module
|
||||
# Example NixOS configurations using ngit-grasp module
|
||||
#
|
||||
# Usage:
|
||||
# 1. Add to your server's flake.nix inputs:
|
||||
@@ -7,30 +7,36 @@
|
||||
# 2. Import the module in your configuration:
|
||||
# imports = [ inputs.ngit-grasp.nixosModules.default ];
|
||||
#
|
||||
# 3. Configure the service (example below)
|
||||
# 3. Configure one or more instances (examples below)
|
||||
|
||||
{ inputs, ... }:
|
||||
|
||||
{
|
||||
imports = [ inputs.ngit-grasp.nixosModules.default ];
|
||||
|
||||
services.ngit-grasp = {
|
||||
# ============================================================================
|
||||
# EXAMPLE 1: Single Instance Configuration
|
||||
# ============================================================================
|
||||
|
||||
services.ngit-grasp.production = {
|
||||
enable = true;
|
||||
domain = "ngit.danconwaydev.com";
|
||||
|
||||
# Network
|
||||
bindAddress = "127.0.0.1";
|
||||
port = 8082; # Same port as current ngit-relay for Caddy compatibility
|
||||
port = 8082;
|
||||
|
||||
# Storage (reuse existing persistent path pattern)
|
||||
# Storage
|
||||
dataDir = "/persistent/ngit-danconwaydev-com-ngit-grasp";
|
||||
|
||||
# Identity
|
||||
relayName = "DanConwayDev's ngit-grasp";
|
||||
relayDescription = "personal instance of ngit-grasp, a Rust GRASP implementation with proactive sync";
|
||||
relayDescription =
|
||||
"personal instance of ngit-grasp, a Rust GRASP implementation with proactive sync";
|
||||
|
||||
# Option 1: Use nsec file (recommended - more secure)
|
||||
relayOwnerNsecFile = "/persistent/ngit-danconwaydev-com-ngit-grasp/relay-owner.nsec";
|
||||
relayOwnerNsecFile =
|
||||
"/persistent/ngit-danconwaydev-com-ngit-grasp/relay-owner.nsec";
|
||||
|
||||
# Option 2: Inline nsec (less secure, ends up in nix store)
|
||||
# relayOwnerNsec = "nsec1...";
|
||||
@@ -45,10 +51,10 @@
|
||||
metricsEnabled = true;
|
||||
|
||||
# Logging
|
||||
logLevel = "info"; # Options: trace, debug, info, warn, error
|
||||
logLevel = "info"; # Options: trace, debug, info, warn, error
|
||||
};
|
||||
|
||||
# Caddy reverse proxy (unchanged from current setup)
|
||||
# Caddy reverse proxy for production instance
|
||||
services.caddy.virtualHosts."ngit.danconwaydev.com" = {
|
||||
extraConfig = ''
|
||||
reverse_proxy 127.0.0.1:8082 {
|
||||
@@ -57,4 +63,73 @@
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
# ============================================================================
|
||||
# EXAMPLE 2: Multiple Instances on Same Server
|
||||
# ============================================================================
|
||||
|
||||
# Uncomment to run multiple instances:
|
||||
|
||||
# # Production instance
|
||||
# services.ngit-grasp.prod = {
|
||||
# enable = true;
|
||||
# domain = "ngit.example.com";
|
||||
# port = 8082;
|
||||
# dataDir = "/persistent/ngit-production";
|
||||
# relayName = "Production GRASP Relay";
|
||||
# syncBootstrapRelayUrl = "wss://relay.ngit.dev";
|
||||
# logLevel = "info";
|
||||
# };
|
||||
#
|
||||
# # Testing/staging instance
|
||||
# services.ngit-grasp.staging = {
|
||||
# enable = true;
|
||||
# domain = "ngit-staging.example.com";
|
||||
# port = 8083;
|
||||
# dataDir = "/persistent/ngit-staging";
|
||||
# relayName = "Staging GRASP Relay";
|
||||
# syncBootstrapRelayUrl = "wss://relay.ngit.dev";
|
||||
# logLevel = "debug"; # More verbose logging for testing
|
||||
# };
|
||||
#
|
||||
# # Development instance with in-memory database
|
||||
# services.ngit-grasp.dev = {
|
||||
# enable = true;
|
||||
# domain = "localhost";
|
||||
# bindAddress = "127.0.0.1";
|
||||
# port = 8084;
|
||||
# dataDir = "/tmp/ngit-dev";
|
||||
# databaseBackend = "memory"; # No persistence
|
||||
# relayName = "Development GRASP Relay";
|
||||
# metricsEnabled = false;
|
||||
# logLevel = "trace"; # Maximum verbosity for debugging
|
||||
# };
|
||||
#
|
||||
# # Caddy configuration for multiple instances
|
||||
# services.caddy.virtualHosts = {
|
||||
# "ngit.example.com" = {
|
||||
# extraConfig = "reverse_proxy 127.0.0.1:8082";
|
||||
# };
|
||||
# "ngit-staging.example.com" = {
|
||||
# extraConfig = "reverse_proxy 127.0.0.1:8083";
|
||||
# };
|
||||
# };
|
||||
|
||||
# ============================================================================
|
||||
# NOTES
|
||||
# ============================================================================
|
||||
|
||||
# Instance names (e.g., "production", "prod", "staging") can be anything.
|
||||
# They are used for:
|
||||
# - systemd service names: ngit-grasp-<name>
|
||||
# - default user names: ngit-grasp-<name>
|
||||
# - default data directories: /var/lib/ngit-grasp-<name>
|
||||
|
||||
# Systemd service management:
|
||||
# systemctl status ngit-grasp-production
|
||||
# systemctl restart ngit-grasp-staging
|
||||
# journalctl -u ngit-grasp-prod -f
|
||||
|
||||
# Each instance runs as a separate user but shares the same group by default.
|
||||
# You can customize user/group per instance if needed.
|
||||
}
|
||||
|
||||
+314
-275
@@ -3,9 +3,7 @@
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.ngit-grasp;
|
||||
|
||||
# Build ngit-grasp package
|
||||
# Build ngit-grasp package (shared across all instances)
|
||||
ngit-grasp = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "ngit-grasp";
|
||||
version = "0.1.0";
|
||||
@@ -22,281 +20,322 @@ let
|
||||
buildInputs = with pkgs; [ openssl ];
|
||||
};
|
||||
|
||||
in {
|
||||
options.services.ngit-grasp = {
|
||||
enable = mkEnableOption "ngit-grasp GRASP relay";
|
||||
# Per-instance options
|
||||
instanceOptions = { name, ... }: {
|
||||
options = {
|
||||
enable = mkEnableOption "this ngit-grasp instance";
|
||||
|
||||
domain = mkOption {
|
||||
type = types.str;
|
||||
example = "ngit.example.com";
|
||||
description =
|
||||
"Domain where this relay is hosted (used in GRASP validation)";
|
||||
};
|
||||
|
||||
bindAddress = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "IP address to bind to";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 8080;
|
||||
description = "Port to listen on";
|
||||
};
|
||||
|
||||
dataDir = mkOption {
|
||||
type = types.path;
|
||||
default = "/var/lib/ngit-grasp";
|
||||
description = "Base directory for data storage";
|
||||
};
|
||||
|
||||
relayName = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "My GRASP Relay";
|
||||
description =
|
||||
"Relay name for NIP-11 (defaults to \${domain} grasp relay)";
|
||||
};
|
||||
|
||||
relayDescription = mkOption {
|
||||
type = types.str;
|
||||
default = "Git Nostr Relay - a grasp implementation";
|
||||
description = "Relay description for NIP-11";
|
||||
};
|
||||
|
||||
relayOwnerNsecFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
example = "/persistent/ngit-grasp/relay-owner.nsec";
|
||||
description = ''
|
||||
Path to file containing relay owner's nsec (private key).
|
||||
If file doesn't exist, ngit-grasp will auto-generate a random nsec and save it.
|
||||
Takes precedence over relayOwnerNsec if both are set.
|
||||
'';
|
||||
};
|
||||
|
||||
relayOwnerNsec = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "nsec1...";
|
||||
description = ''
|
||||
Relay owner's nsec (private key) for signing and authentication.
|
||||
Less secure than relayOwnerNsecFile as it ends up in nix store.
|
||||
Only used if relayOwnerNsecFile is not set.
|
||||
'';
|
||||
};
|
||||
|
||||
syncBootstrapRelayUrl = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "wss://relay.ngit.dev";
|
||||
description = "Bootstrap relay URL to sync from on startup (optional)";
|
||||
};
|
||||
|
||||
databaseBackend = mkOption {
|
||||
type = types.enum [ "lmdb" "nostr-db" "memory" ];
|
||||
default = "lmdb";
|
||||
description = ''
|
||||
Database backend type:
|
||||
- lmdb: LMDB backend (persistent, general purpose)
|
||||
- nostr-db: NostrDB backend (persistent, optimized for Nostr)
|
||||
- memory: In-memory database (fastest, no persistence)
|
||||
'';
|
||||
};
|
||||
|
||||
metricsEnabled = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Enable Prometheus metrics endpoint at /metrics";
|
||||
};
|
||||
|
||||
metricsConnectionPerIpAbuseThreshold = mkOption {
|
||||
type = types.int;
|
||||
default = 10;
|
||||
description =
|
||||
"Connections per IP before flagging as potential abuse in metrics";
|
||||
};
|
||||
|
||||
metricsTopNRepos = mkOption {
|
||||
type = types.int;
|
||||
default = 10;
|
||||
description = "Number of top bandwidth repos to track in metrics";
|
||||
};
|
||||
|
||||
logLevel = mkOption {
|
||||
type = types.enum [ "trace" "debug" "info" "warn" "error" ];
|
||||
default = "info";
|
||||
description = "Logging level for RUST_LOG environment variable";
|
||||
};
|
||||
|
||||
syncMaxBackoffSecs = mkOption {
|
||||
type = types.int;
|
||||
default = 3600;
|
||||
description =
|
||||
"Maximum backoff time in seconds for sync relay reconnection (default: 1 hour)";
|
||||
};
|
||||
|
||||
syncDisconnectCheckIntervalSecs = mkOption {
|
||||
type = types.int;
|
||||
default = 60;
|
||||
description = "Interval in seconds for checking disconnected relays";
|
||||
};
|
||||
|
||||
syncBaseBackoffSecs = mkOption {
|
||||
type = types.int;
|
||||
default = 5;
|
||||
description = "Base backoff time in seconds for relay reconnection";
|
||||
};
|
||||
|
||||
syncDisableNegentropy = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Disable NIP-77 negentropy sync (use REQ+EOSE instead)";
|
||||
};
|
||||
|
||||
rejectedHotCacheDurationSecs = mkOption {
|
||||
type = types.int;
|
||||
default = 120;
|
||||
description =
|
||||
"Hot cache duration in seconds for rejected announcements (default: 2 minutes)";
|
||||
};
|
||||
|
||||
rejectedColdIndexExpirySecs = mkOption {
|
||||
type = types.int;
|
||||
default = 604800;
|
||||
description =
|
||||
"Cold index expiry in seconds for rejected announcements (default: 7 days)";
|
||||
};
|
||||
|
||||
naughtyListExpirationHours = mkOption {
|
||||
type = types.int;
|
||||
default = 12;
|
||||
description = "Hours before removing relay from naughty list";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "ngit-grasp";
|
||||
description = "User account under which ngit-grasp runs";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "ngit-grasp";
|
||||
description = "Group under which ngit-grasp runs";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
# Create user and group
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
description = "ngit-grasp service user";
|
||||
home = cfg.dataDir;
|
||||
};
|
||||
|
||||
users.groups.${cfg.group} = { };
|
||||
|
||||
# Create systemd service
|
||||
systemd.services.ngit-grasp = {
|
||||
description = "ngit-grasp GRASP relay";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
environment = {
|
||||
NGIT_DOMAIN = cfg.domain;
|
||||
NGIT_BIND_ADDRESS = "${cfg.bindAddress}:${toString cfg.port}";
|
||||
NGIT_GIT_DATA_PATH = "${cfg.dataDir}/git";
|
||||
NGIT_RELAY_DATA_PATH = "${cfg.dataDir}/relay";
|
||||
NGIT_RELAY_DESCRIPTION = cfg.relayDescription;
|
||||
NGIT_DATABASE_BACKEND = cfg.databaseBackend;
|
||||
NGIT_METRICS_CONNECTION_PER_IP_ABUSE_THRESHOLD =
|
||||
toString cfg.metricsConnectionPerIpAbuseThreshold;
|
||||
NGIT_METRICS_TOP_N_REPOS = toString cfg.metricsTopNRepos;
|
||||
NGIT_SYNC_MAX_BACKOFF_SECS = toString cfg.syncMaxBackoffSecs;
|
||||
NGIT_SYNC_DISCONNECT_CHECK_INTERVAL_SECS =
|
||||
toString cfg.syncDisconnectCheckIntervalSecs;
|
||||
NGIT_SYNC_BASE_BACKOFF_SECS = toString cfg.syncBaseBackoffSecs;
|
||||
NGIT_REJECTED_HOT_CACHE_DURATION_SECS =
|
||||
toString cfg.rejectedHotCacheDurationSecs;
|
||||
NGIT_REJECTED_COLD_INDEX_EXPIRY_SECS =
|
||||
toString cfg.rejectedColdIndexExpirySecs;
|
||||
NGIT_NAUGHTY_LIST_EXPIRATION_HOURS =
|
||||
toString cfg.naughtyListExpirationHours;
|
||||
RUST_LOG = cfg.logLevel;
|
||||
} // optionalAttrs (cfg.relayName != null) {
|
||||
NGIT_RELAY_NAME = cfg.relayName;
|
||||
} // optionalAttrs cfg.metricsEnabled { NGIT_METRICS_ENABLED = "true"; }
|
||||
// optionalAttrs (cfg.syncBootstrapRelayUrl != null) {
|
||||
NGIT_SYNC_BOOTSTRAP_RELAY_URL = cfg.syncBootstrapRelayUrl;
|
||||
} // optionalAttrs cfg.syncDisableNegentropy {
|
||||
NGIT_SYNC_DISABLE_NEGENTROPY = "true";
|
||||
} // optionalAttrs
|
||||
(cfg.relayOwnerNsec != null && cfg.relayOwnerNsecFile == null) {
|
||||
# Only set inline nsec if file is not specified
|
||||
NGIT_RELAY_OWNER_NSEC = cfg.relayOwnerNsec;
|
||||
};
|
||||
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
|
||||
# Working directory where .relay-owner.nsec will be created if needed
|
||||
WorkingDirectory = cfg.dataDir;
|
||||
|
||||
# Command to run
|
||||
ExecStart = if cfg.relayOwnerNsecFile != null then
|
||||
# Use nsec from file
|
||||
"${ngit-grasp}/bin/ngit-grasp --relay-owner-nsec $(cat ${cfg.relayOwnerNsecFile})"
|
||||
else
|
||||
# Let ngit-grasp auto-generate nsec in .relay-owner.nsec file in dataDir
|
||||
"${ngit-grasp}/bin/ngit-grasp";
|
||||
|
||||
# Restart policy
|
||||
Restart = "always";
|
||||
RestartSec = "10s";
|
||||
|
||||
# Hardening
|
||||
NoNewPrivileges = true;
|
||||
PrivateTmp = true;
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
ReadWritePaths = [ cfg.dataDir ];
|
||||
|
||||
# If using nsecFile, grant read access
|
||||
ReadOnlyPaths =
|
||||
optionals (cfg.relayOwnerNsecFile != null) [ cfg.relayOwnerNsecFile ];
|
||||
|
||||
# Additional hardening
|
||||
ProtectKernelTunables = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectControlGroups = true;
|
||||
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
|
||||
RestrictNamespaces = true;
|
||||
LockPersonality = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
PrivateDevices = true;
|
||||
|
||||
# Capabilities
|
||||
CapabilityBoundingSet = "";
|
||||
AmbientCapabilities = "";
|
||||
|
||||
# System call filtering
|
||||
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
|
||||
SystemCallErrorNumber = "EPERM";
|
||||
domain = mkOption {
|
||||
type = types.str;
|
||||
example = "ngit.example.com";
|
||||
description =
|
||||
"Domain where this relay is hosted (used in GRASP validation)";
|
||||
};
|
||||
|
||||
# Ensure data directories exist before starting
|
||||
preStart = ''
|
||||
mkdir -p ${cfg.dataDir}/git
|
||||
mkdir -p ${cfg.dataDir}/relay
|
||||
chown -R ${cfg.user}:${cfg.group} ${cfg.dataDir}
|
||||
chmod 750 ${cfg.dataDir}
|
||||
'';
|
||||
bindAddress = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "IP address to bind to";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 8080;
|
||||
description = "Port to listen on";
|
||||
};
|
||||
|
||||
dataDir = mkOption {
|
||||
type = types.path;
|
||||
default = "/var/lib/ngit-grasp-${name}";
|
||||
description = "Base directory for data storage";
|
||||
};
|
||||
|
||||
relayName = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "My GRASP Relay";
|
||||
description =
|
||||
"Relay name for NIP-11 (defaults to \${domain} grasp relay)";
|
||||
};
|
||||
|
||||
relayDescription = mkOption {
|
||||
type = types.str;
|
||||
default = "Git Nostr Relay - a grasp implementation";
|
||||
description = "Relay description for NIP-11";
|
||||
};
|
||||
|
||||
relayOwnerNsecFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
example = "/persistent/ngit-grasp/relay-owner.nsec";
|
||||
description = ''
|
||||
Path to file containing relay owner's nsec (private key).
|
||||
If file doesn't exist, ngit-grasp will auto-generate a random nsec and save it.
|
||||
Takes precedence over relayOwnerNsec if both are set.
|
||||
'';
|
||||
};
|
||||
|
||||
relayOwnerNsec = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "nsec1...";
|
||||
description = ''
|
||||
Relay owner's nsec (private key) for signing and authentication.
|
||||
Less secure than relayOwnerNsecFile as it ends up in nix store.
|
||||
Only used if relayOwnerNsecFile is not set.
|
||||
'';
|
||||
};
|
||||
|
||||
syncBootstrapRelayUrl = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "wss://relay.ngit.dev";
|
||||
description = "Bootstrap relay URL to sync from on startup (optional)";
|
||||
};
|
||||
|
||||
databaseBackend = mkOption {
|
||||
type = types.enum [ "lmdb" "nostr-db" "memory" ];
|
||||
default = "lmdb";
|
||||
description = ''
|
||||
Database backend type:
|
||||
- lmdb: LMDB backend (persistent, general purpose)
|
||||
- nostr-db: NostrDB backend (persistent, optimized for Nostr)
|
||||
- memory: In-memory database (fastest, no persistence)
|
||||
'';
|
||||
};
|
||||
|
||||
metricsEnabled = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Enable Prometheus metrics endpoint at /metrics";
|
||||
};
|
||||
|
||||
metricsConnectionPerIpAbuseThreshold = mkOption {
|
||||
type = types.int;
|
||||
default = 10;
|
||||
description =
|
||||
"Connections per IP before flagging as potential abuse in metrics";
|
||||
};
|
||||
|
||||
metricsTopNRepos = mkOption {
|
||||
type = types.int;
|
||||
default = 10;
|
||||
description = "Number of top bandwidth repos to track in metrics";
|
||||
};
|
||||
|
||||
logLevel = mkOption {
|
||||
type = types.enum [ "trace" "debug" "info" "warn" "error" ];
|
||||
default = "info";
|
||||
description = "Logging level for RUST_LOG environment variable";
|
||||
};
|
||||
|
||||
syncMaxBackoffSecs = mkOption {
|
||||
type = types.int;
|
||||
default = 3600;
|
||||
description =
|
||||
"Maximum backoff time in seconds for sync relay reconnection (default: 1 hour)";
|
||||
};
|
||||
|
||||
syncDisconnectCheckIntervalSecs = mkOption {
|
||||
type = types.int;
|
||||
default = 60;
|
||||
description = "Interval in seconds for checking disconnected relays";
|
||||
};
|
||||
|
||||
syncBaseBackoffSecs = mkOption {
|
||||
type = types.int;
|
||||
default = 5;
|
||||
description = "Base backoff time in seconds for relay reconnection";
|
||||
};
|
||||
|
||||
syncDisableNegentropy = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Disable NIP-77 negentropy sync (use REQ+EOSE instead)";
|
||||
};
|
||||
|
||||
rejectedHotCacheDurationSecs = mkOption {
|
||||
type = types.int;
|
||||
default = 120;
|
||||
description =
|
||||
"Hot cache duration in seconds for rejected announcements (default: 2 minutes)";
|
||||
};
|
||||
|
||||
rejectedColdIndexExpirySecs = mkOption {
|
||||
type = types.int;
|
||||
default = 604800;
|
||||
description =
|
||||
"Cold index expiry in seconds for rejected announcements (default: 7 days)";
|
||||
};
|
||||
|
||||
naughtyListExpirationHours = mkOption {
|
||||
type = types.int;
|
||||
default = 12;
|
||||
description = "Hours before removing relay from naughty list";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "ngit-grasp-${name}";
|
||||
description = "User account under which this instance runs";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "ngit-grasp";
|
||||
description = "Group under which this instance runs";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Create systemd service config for an instance
|
||||
mkService = name: cfg: {
|
||||
description = "ngit-grasp GRASP relay (${name})";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
environment = {
|
||||
NGIT_DOMAIN = cfg.domain;
|
||||
NGIT_BIND_ADDRESS = "${cfg.bindAddress}:${toString cfg.port}";
|
||||
NGIT_GIT_DATA_PATH = "${cfg.dataDir}/git";
|
||||
NGIT_RELAY_DATA_PATH = "${cfg.dataDir}/relay";
|
||||
NGIT_RELAY_DESCRIPTION = cfg.relayDescription;
|
||||
NGIT_DATABASE_BACKEND = cfg.databaseBackend;
|
||||
NGIT_METRICS_CONNECTION_PER_IP_ABUSE_THRESHOLD =
|
||||
toString cfg.metricsConnectionPerIpAbuseThreshold;
|
||||
NGIT_METRICS_TOP_N_REPOS = toString cfg.metricsTopNRepos;
|
||||
NGIT_SYNC_MAX_BACKOFF_SECS = toString cfg.syncMaxBackoffSecs;
|
||||
NGIT_SYNC_DISCONNECT_CHECK_INTERVAL_SECS =
|
||||
toString cfg.syncDisconnectCheckIntervalSecs;
|
||||
NGIT_SYNC_BASE_BACKOFF_SECS = toString cfg.syncBaseBackoffSecs;
|
||||
NGIT_REJECTED_HOT_CACHE_DURATION_SECS =
|
||||
toString cfg.rejectedHotCacheDurationSecs;
|
||||
NGIT_REJECTED_COLD_INDEX_EXPIRY_SECS =
|
||||
toString cfg.rejectedColdIndexExpirySecs;
|
||||
NGIT_NAUGHTY_LIST_EXPIRATION_HOURS =
|
||||
toString cfg.naughtyListExpirationHours;
|
||||
RUST_LOG = cfg.logLevel;
|
||||
} // optionalAttrs (cfg.relayName != null) {
|
||||
NGIT_RELAY_NAME = cfg.relayName;
|
||||
} // optionalAttrs cfg.metricsEnabled { NGIT_METRICS_ENABLED = "true"; }
|
||||
// optionalAttrs (cfg.syncBootstrapRelayUrl != null) {
|
||||
NGIT_SYNC_BOOTSTRAP_RELAY_URL = cfg.syncBootstrapRelayUrl;
|
||||
} // optionalAttrs cfg.syncDisableNegentropy {
|
||||
NGIT_SYNC_DISABLE_NEGENTROPY = "true";
|
||||
} // optionalAttrs
|
||||
(cfg.relayOwnerNsec != null && cfg.relayOwnerNsecFile == null) {
|
||||
# Only set inline nsec if file is not specified
|
||||
NGIT_RELAY_OWNER_NSEC = cfg.relayOwnerNsec;
|
||||
};
|
||||
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
|
||||
# Working directory where .relay-owner.nsec will be created if needed
|
||||
WorkingDirectory = cfg.dataDir;
|
||||
|
||||
# Command to run
|
||||
ExecStart = if cfg.relayOwnerNsecFile != null then
|
||||
# Use nsec from file
|
||||
"${ngit-grasp}/bin/ngit-grasp --relay-owner-nsec $(cat ${cfg.relayOwnerNsecFile})"
|
||||
else
|
||||
# Let ngit-grasp auto-generate nsec in .relay-owner.nsec file in dataDir
|
||||
"${ngit-grasp}/bin/ngit-grasp";
|
||||
|
||||
# Restart policy
|
||||
Restart = "always";
|
||||
RestartSec = "10s";
|
||||
|
||||
# Hardening
|
||||
NoNewPrivileges = true;
|
||||
PrivateTmp = true;
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
ReadWritePaths = [ cfg.dataDir ];
|
||||
|
||||
# If using nsecFile, grant read access
|
||||
ReadOnlyPaths =
|
||||
optionals (cfg.relayOwnerNsecFile != null) [ cfg.relayOwnerNsecFile ];
|
||||
|
||||
# Additional hardening
|
||||
ProtectKernelTunables = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectControlGroups = true;
|
||||
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
|
||||
RestrictNamespaces = true;
|
||||
LockPersonality = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
PrivateDevices = true;
|
||||
|
||||
# Capabilities
|
||||
CapabilityBoundingSet = "";
|
||||
AmbientCapabilities = "";
|
||||
|
||||
# System call filtering
|
||||
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
|
||||
SystemCallErrorNumber = "EPERM";
|
||||
};
|
||||
|
||||
# Ensure data directories exist before starting
|
||||
preStart = ''
|
||||
mkdir -p ${cfg.dataDir}/git
|
||||
mkdir -p ${cfg.dataDir}/relay
|
||||
chown -R ${cfg.user}:${cfg.group} ${cfg.dataDir}
|
||||
chmod 750 ${cfg.dataDir}
|
||||
'';
|
||||
};
|
||||
|
||||
enabledInstances =
|
||||
filterAttrs (_: cfg: cfg.enable) config.services.ngit-grasp;
|
||||
|
||||
in {
|
||||
options.services.ngit-grasp = mkOption {
|
||||
type = types.attrsOf (types.submodule instanceOptions);
|
||||
default = { };
|
||||
description = ''
|
||||
ngit-grasp GRASP relay instances.
|
||||
|
||||
Multiple instances can be configured with different domains and ports.
|
||||
Each instance runs as a separate systemd service.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{
|
||||
production = {
|
||||
enable = true;
|
||||
domain = "ngit.example.com";
|
||||
port = 8082;
|
||||
dataDir = "/persistent/ngit-production";
|
||||
};
|
||||
|
||||
testing = {
|
||||
enable = true;
|
||||
domain = "ngit-test.example.com";
|
||||
port = 8083;
|
||||
dataDir = "/persistent/ngit-testing";
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
config = mkIf (enabledInstances != { }) {
|
||||
# Create users for all enabled instances
|
||||
users.users = mapAttrs' (name: cfg:
|
||||
nameValuePair cfg.user {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
description = "ngit-grasp service user (${name})";
|
||||
home = cfg.dataDir;
|
||||
}) enabledInstances;
|
||||
|
||||
# Create shared group (all instances use the same group by default)
|
||||
users.groups.ngit-grasp = { };
|
||||
|
||||
# Create systemd services for all enabled instances
|
||||
systemd.services = mapAttrs'
|
||||
(name: cfg: nameValuePair "ngit-grasp-${name}" (mkService name cfg))
|
||||
enabledInstances;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user