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