This repository has no description
5.7 kB
153 lines
1{
2 config,
3 pkgs,
4 ...
5}:
6# Per-machine Radicle identity and node for day-to-day development
7# (rad clone/push/patch), as opposed to the public seed in ./radicle.lassul.us.nix.
8#
9# One radicle-node = one keypair = one Node ID; a node cannot serve several
10# identities. So every machine gets its own key (generator `rad`) and hence
11# its own DID, and repos that should accept pushes from all machines list each
12# machine's DID as a delegate (`rad id update --delegate did:key:<nid>`).
13#
14# Radicle has no multi-user mode: the `rad` CLI and git-remote-rad write into
15# the profile's storage themselves, signing with the key in keys/radicle. To
16# make one identity usable by several accounts the profile lives outside any
17# home, in /var/lib/rad, group-shared: the node runs as the `rad` system user,
18# everything is setgid `rad` with group rwx, and the CLI/remote-helper wrappers
19# below force umask 002 so files one side creates stay writable for the other.
20# RAD_HOME points there for login shells; membership in `rad` is what grants
21# use of the identity (mainUser is a member, add others per machine).
22# Keys and config.json are symlinks into the clan vars secret dir / nix store,
23# maintained by tmpfiles; storage and the control socket are ordinary state.
24#
25# The node only dials out (no listen address): the laptops sit behind NAT and
26# the public seed neoprism relays for them, so nothing needs a port opened.
27let
28 generator = config.clan.core.vars.generators.rad;
29 radHome = "/var/lib/rad";
30
31 settings = {
32 publicExplorer = "https://app.radicle.xyz/nodes/$host/$rid$path";
33 preferredSeeds = [
34 # our own seed (2configs/radicle.lassul.us.nix on neoprism)
35 "z6MkhTe9WWbqNdRAnWLHxL23gedRQWdhjajRbNAt5fCpae6o@radicle.lassul.us:8776"
36 "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@seed.radicle.xyz:8776"
37 "z6Mkmqogy2qEM2ummccUthFEaaHvyYmYBYh3dbe9W4ebScxo@iris.radicle.network:8776"
38 ];
39 cli.hints = true;
40 node = {
41 alias = config.networking.hostName;
42 listen = [ ];
43 network = "main";
44 log = "INFO";
45 relay = "auto";
46 # Only seed what this machine explicitly initialised, cloned or `rad seed`ed.
47 seedingPolicy.default = "block";
48 };
49 };
50
51 # Validate with `rad config` at build time (same trick as the nixpkgs seed
52 # module) so a typo does not leave every machine's node crash-looping.
53 configFile =
54 pkgs.runCommand "rad-config.json"
55 {
56 nativeBuildInputs = [ pkgs.buildPackages.radicle-node ];
57 json = builtins.toJSON settings;
58 passAsFile = [ "json" ];
59 preferLocalBuild = true;
60 }
61 ''
62 install -D -m 644 "$jsonPath" $out
63 ln -s $out config.json
64 install -D -m 644 /dev/stdin keys/radicle.pub <<<"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBgFMhajUng+Rjj/sCFXI9PzG8BQjru2n7JgUVF1Kbv5 snakeoil"
65 RAD_HOME=$PWD rad config >/dev/null
66 '';
67
68 # `rad` and `git-remote-rad` (what git runs for rad:// remotes) forced to
69 # umask 002; first in the join so they shadow the plain radicle-node ones.
70 wrap =
71 bin:
72 pkgs.writeShellScriptBin bin ''
73 # ${radHome} is shared with the rad-node service via group rad.
74 umask 002
75 exec ${pkgs.radicle-node}/bin/${bin} "$@"
76 '';
77 radicle-shared = pkgs.symlinkJoin {
78 name = "radicle-shared";
79 paths = [
80 (wrap "rad")
81 (wrap "git-remote-rad")
82 pkgs.radicle-node
83 ];
84 };
85in
86{
87 clan.core.vars.generators.rad = {
88 files."radicle.key" = {
89 owner = "rad";
90 group = "rad";
91 mode = "0440";
92 };
93 files."radicle.pub".secret = false;
94 runtimeInputs = [ pkgs.openssh ];
95 script = ''
96 ssh-keygen -t ed25519 -N "" -C "radicle ${config.networking.hostName}" -f "$out/radicle.key"
97 mv "$out/radicle.key.pub" "$out/radicle.pub"
98 '';
99 };
100
101 users.users.rad = {
102 isSystemUser = true;
103 group = "rad";
104 home = radHome;
105 };
106 users.groups.rad = { };
107 users.users.mainUser.extraGroups = [ "rad" ];
108
109 environment.systemPackages = [ radicle-shared ];
110 environment.variables.RAD_HOME = radHome;
111
112 systemd.tmpfiles.rules = [
113 "d ${radHome} 2770 rad rad -"
114 "d ${radHome}/cobs 2770 rad rad -"
115 "d ${radHome}/keys 2770 rad rad -"
116 "d ${radHome}/node 2770 rad rad -"
117 "d ${radHome}/storage 2770 rad rad -"
118 "L+ ${radHome}/keys/radicle - - - - ${generator.files."radicle.key".path}"
119 "L+ ${radHome}/keys/radicle.pub - - - - ${generator.files."radicle.pub".path}"
120 "L+ ${radHome}/config.json - - - - ${configFile}"
121 # SQLite creates databases 0644 regardless of umask, which would lock the
122 # second party out ("attempt to write a readonly database"). Pre-create
123 # them empty and group-writable (an empty file is a valid empty database;
124 # -wal/-shm copy the main file's mode). Adjusts mode on every boot too.
125 "f ${radHome}/cobs/cache.db 0664 rad rad -"
126 "f ${radHome}/node/node.db 0664 rad rad -"
127 "f ${radHome}/node/notifications.db 0664 rad rad -"
128 "f ${radHome}/node/policies.db 0664 rad rad -"
129 ];
130
131 systemd.services.rad-node = {
132 description = "Radicle node (machine identity, ${radHome})";
133 documentation = [ "man:radicle-node(1)" ];
134 wantedBy = [ "multi-user.target" ];
135 wants = [ "network-online.target" ];
136 after = [ "network-online.target" ];
137 environment = {
138 # rad stats $HOME/.gitconfig
139 HOME = radHome;
140 RAD_HOME = radHome;
141 };
142 path = [ pkgs.gitMinimal ];
143 serviceConfig = {
144 User = "rad";
145 Group = "rad";
146 UMask = "0002";
147 # --force: reclaim a control socket left behind by an unclean shutdown.
148 ExecStart = "${pkgs.radicle-node}/bin/radicle-node --force";
149 Restart = "on-failure";
150 RestartSec = 5;
151 };
152 };
153}