Peer-to-peer Git: Radicle Seed Node on OpenBSD
While Git is decentralized by design, in many cases it still depends on a classical server-client architecture. Many projects rely on GitHub, GitLab, or another centralized platform to host their repositories and thereby make them available to everyone. What if we could have Git, but without depending on any centralized servers at all, and instead use it peer-to-peer?
Update 2024-08-27
Unfortunately, Radicle turned out to be just as unstable as it has been for the past years, rendering the efforts of hosting a seed node pointless. With its APIs and configurations changing virtually every other month, it is a PITA to maintain. I do not recommend hosting a seed node, especially on OpenBSD, a platform that doesn’t appear to receive any official support by the project.
Maybe one day, after what feels like the tenth iteration, Radicle will become a useful tool. However, as of today, it sadly is not.
Even though Git is decentralized by design, in reality, it is very much centralized, with plenty of popular projects being in the hands of just a few organizations: GitHub (Microsoft), GitLab, Launchpad (Canonical), and Codeberg, just to name some the most recognized ones. If any of these organizations decide to boot a project – or worse, shut the whole service down – it will definitely lead to at least some headaches and inconveniences for maintainers, as well as users.
Ideally, the Git infrastructure that we use would instead also be fully decentralized, using a peer-to-peer architecture in which no repository lives on only a single server, but is being replicated by many different servers, hosted by different individuals and organizations. This is where Radicle comes in:
Radicle is an open source, peer-to-peer code collaboration stack built on Git. Unlike centralized code hosting platforms, there is no single entity controlling the network. Repositories are replicated across peers in a decentralized manner, and users are in full control of their data and workflow.
In this write-up we’ll be looking at how to set up our seed node, allowing us to not only replicate existing projects on Radicle but also host our repositories and make them available to the rest of the network.
Preparation
Usually, when I build an OpenBSD-based infrastructure, I use a Vultr VPS instance. You can start with the lowest VPS configuration and upgrade over time if necessary. Anything with at least 1GB of RAM and 10GB of storage should be fine.
We’re going to be using the fresh-out-the-oven OpenBSD 7.5 for this setup.
Note: I won’t explicitly mention under which user I’m running each command, hence please read carefully. When the prompt shows
seed#
, I’m acting asroot
user, otherwise, when it showsseed%
, I’m using the_seed
user.
As soon as the OpenBSD VPS has booted, we can log in via SSH and perform a quick update of the system:
seed# syspatch
seed# pkg_add -u
Next, perform a system upgrade to -current
. We want this because -stable
releases especially for Rust (which we will require for building Radicle v0.9.0)
are a bit too outdated:
seed# sysupgrade -s
The system will reboot and when you log in you should be greeted with a version
that says -current
:
OpenBSD 7.5-current (GENERIC) #23: Thu Apr 11 12:18:37 MDT 2024
Welcome to OpenBSD: The proactively secure Unix-like operating system.
After that, let’s do a quick upgrade of the existing packages and install some handy tools:
seed# pkg_add -uvi
seed# pkg_add git zsh neovim wget mosh rsync htop
What I like to do is link nvim
to vim
, because typing vim
is in my muscle
memory. I also like to use zsh
as shell. Since that’s all preference, these
steps are optional:
seed# ln -s /usr/local/bin/nvim /usr/local/bin/vim
seed# chsh -s /usr/local/bin/zsh root
Another optional but useful thing that I like to do is to change the SSH port
and disable password authentication / enable pubkey authentication. Make sure
you have an authorized_keys
entry with your pubkey in place before applying
this change:
seed# sed -i 's/^#Port 22/Port 31231/g;\
s/^#PubkeyAuthentication .*/PubkeyAuthentication yes/g;\
s/^#PasswordAuthentication .*/PasswordAuthentication no/g' \
/etc/ssh/sshd_config
seed# rcctl restart sshd
Afterwards, disconnect from SSH, and re-connect, ideally using mosh
, and
launch tmux
for the sake of comfort. See this
post on how to
make the mosh
experience even smoother.
Radicle
First, let’s install the required software to build Radicle:
seed# pkg_add rust
Then, install Radicle using cargo
, the Rust package manager:
FYI: This documentation is using the
root
user to build the required components. If you don’t feel comfortable with this, feel free to jump to creating the_seed
user first and then come back to perform these steps underneath that user.
seed# #https://github.com/rust-lang/cargo/issues/11435#issuecomment-1740163332
seed# ulimit -n 1024
seed# cargo install --force --locked \
--git https://seed.radicle.xyz/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git \
radicle-cli \
radicle-node \
radicle-remote-helper
seed# mv ~/.cargo/bin/* /usr/local/bin/
FYI: Depending on the performance of your machine, this process might take more than a cup of coffee in time. If you’re setting up a single vCPU VPS, consider running it on a beefier machine and transferring the built binaries to your server.
Update 2024-07-30
Unfortunately Radicle appears to be falling back to their previous behavior from a year ago, where they keep changing things without informing anyone outside of their inner circle – which these days is basically their Zulip chat, that is just another thing that makes you #facepalm, considering that Radicle is a project in the decentralization space, not using a decentralized community (e.g. Matrix, XMPP, or at least IRC).
This time, again, they introduced breaking changes, that effectively kill a fully functional seed, without informing users in their updates section, forcing administrators to upgrade their seed nodes in order to stay compatible with the rest of the network:
Could not parse the request
The response received from the seed does not match the expected schema. The node you are fetching from seems to be outdated, make sure the httpd API version is at least 4.0.0 currently 0.1.0.
Unfortunately, they also changed how radicle-httpd
is supposed to be built
and
installed
without any of their official
documents
or social channels
informing about this. Instead, Radicle appears to be relying on its users
reading their commit messages instead.
I was able find out about this change, test the upgrade and update this post. During the upgrade, however, another issue came up:
2024-07-30T15:10:28.883896Z INFO starting http daemon..
2024-07-30T15:10:28.884217Z INFO version pre-release (a73c5490)
2024-07-30T15:10:28.891133Z INFO git version 2.45.2
2024-07-30T15:10:28.891779Z INFO listening on http://127.0.0.1:8080
2024-07-30T15:10:28.910664Z INFO using radicle home at /home/_seed/.radicle
2024-07-30T15:10:33.220206Z DEBUG request{id=0}: started processing request
2024-07-30T15:10:33.224040Z ERROR request{id=0}: Error getting node config: command error: (de)serialization failed: missing field `type` at line 1 column 20
I tried initializing a fresh node configuration, only to see other errors popping up:
/usr/local/bin/rad config init --alias seed.xn--gckvb8fzb.com
✓ Initialized new Radicle configuration at /home/_seed/.radicle/config.json
seed% radicle-node --listen 0.0.0.0:8776 --force
2024-07-30T15:28:26.698Z INFO node Starting node..
2024-07-30T15:28:26.699Z INFO node Version pre-release (574ac356)
2024-07-30T15:28:26.699Z INFO node Unlocking node keystore..
2024-07-30T15:28:26.699Z INFO node Node ID is z6MksHwp2VUdawHNWNnd8YwDEkP1E6LuERxCGvTHHarEjYPi
2024-07-30T15:28:26.701Z ERROR node Fatal: failed to load configuration from /home/_seed/.radicle/config.json: missing field `target` at line 20 column 6
Upon further investigation into radicle-node
, I found that for some reason its
version displays 0.9.0
, even though the official release has apparently
reached 1.0.0-rc.13
. Even after repeated
cargo install
via the Radicle Git seed, I keep ending up with 0.9.0
.
I have updated the radicle-httpd
section below, and it now contains the new
repository URL. However, I have been unable to get my own node up and running
again with even the default configuration generated by rad config init
.
Hence, consider the instructions in this post non-functional as of right
now.
I will try to give it another go once Radicle reaches 1.0.0
and it becomes
available through their official seed or crates.io, but I’m not going to keep
playing cat and mouse much longer.
Radicle is making it impossible for users to love it. I ditched it once before and I might ditch it again if the project won’t finally change their move fast, break a lot of things and tell no one way of acting. I, like probably many others who are depending on a reliable Git infrastructure, are unlikely to appreciate breaking changes every few months. If Radicle wants to be taken seriously by the masses, it needs to make it possible for people to easily build and install their client and node software, it needs to properly inform its users about changes, in places where their users are (hint), and it has to come up with a plan for backwards compatibility and/or proper deprecation of previous versions. The current state of Radicle, from an outsider’s (and normal user’s) perspective, is a mess at best, and digging through their Zulip chats doesn’t help either.
<OBSOLETE>
Normally we would also compile the radicle-httpd
package together with all
the others. Unfortunately, there’s currently an issue on OpenBSD that prevents
it from building:
Compiling radicle-term v0.9.0 (/root/.cargo/git/checkouts/z3gqcJUoA1n9HaHKufZs5FCSGazv5-cab4735f10a16009/574ac35/radicle-term)
error[E0425]: cannot find value `cmd_name` in this scope
--> radicle-httpd/src/commands/web.rs:200:36
|
200 | let mut cmd = Command::new(cmd_name);
| ^^^^^^^^ not found in this scope
error[E0425]: cannot find value `cmd_name` in this scope
--> radicle-httpd/src/commands/web.rs:215:71
|
215 | term::error(format!("Could not open web browser via `{cmd_name}`"));
| ^^^^^^^^ not found in this scope
For more information about this error, try `rustc --explain E0425`.
error: could not compile `radicle-httpd` (lib) due to 2 previous errors
We can, however, manually fix that issue. For that, we need to clone the
heartwood
repository and patch a single file within the radicle-httpd
folder:
seed# git clone https://seed.radicle.xyz/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git
seed# cd z3gqcJUoA1n9HaHKufZs5FCSGazv5/
seed# git diff radicle-httpd/
diff --git a/radicle-httpd/src/commands/web.rs b/radicle-httpd/src/commands/web.rs
index 3229ebf5..fb246539 100644
--- a/radicle-httpd/src/commands/web.rs
+++ b/radicle-httpd/src/commands/web.rs
@@ -196,6 +196,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let cmd_name = "open";
#[cfg(target_os = "linux")]
let cmd_name = "xdg-open";
+ #[cfg(target_os = "openbsd")]
+ let cmd_name = "echo";
let mut cmd = Command::new(cmd_name);
match cmd.arg(auth_url.as_str()).spawn() {
We can then proceed to manually build and install the binary for
radicle-httpd
:
seed# cd radicle-httpd
seed# cargo build --release
seed# mv ~/.cargo/bin/* /usr/local/bin/
<\OBSOLETE>
We can build and install radicle-httpd
using cargo
and the
radicle-explorer
repository:
seed# cargo install --force --locked \
--git https://seed.radicle.xyz/z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git \
radicle-httpd
seed# mv ~/.cargo/bin/* /usr/local/bin/
Seed User
Next, we create the _seed
group and user on the system:
seed# groupadd _seed
seed# useradd -d /home/_seed -m -c "Radicle Seed" \
-g _seed -L daemon -s /sbin/nologin _seed
After that, we can log in as _seed
user and configure Radicle:
seed# su -l -s /usr/local/bin/zsh _seed -
seed% /usr/local/bin/rad auth --alias seed.domain.com
As seed nodes do not typically sign permanent artifacts with their key, it is not generally necessary to set up a passphrase, so we can simply skip the prompt by pressing enter.
Now, let’s open the Radicle node configuration to adjust it:
seed% /usr/local/bin/rad config edit
First of all, we need to decide what seeding policy we’d like our seed to have. We can decide between permissive and selective seeding. To understand the differences, let’s have a quick look at the official Radicle manual:
A permissive or “open” policy is said to be fully-replicating, meaning your seed will try to have a fully copy of all repository data available on the network.
A selective or restricted policy requires you, the operator, to manually allow repositories to be seeded. This means that the node will ignore all repositories, except the ones that are pre-configured to allow seeding.
Tl;dr: If you’re looking to simply host a public seed to support the Radicle network, you’d want to go with a permissive policy. If, however, you’re looking to build more of a private seed with only your repositories, or repositories you care about, the selective policy is what you’d want.
For the permissive policy, configure the following values:
{
"node": {
...
"policy": "allow",
"scope": "all"
}
}
For the selective policy, configure the following values instead:
{
"node": {
...
"policy": "block",
"scope": "all"
}
}
Next, we configure the node’s external address with a subdomain/domain that points to the server and for which we’re later on going to create an SSL certificate:
{
"node": {
...
"externalAddresses": ["seed.domain.com:8776"]
}
}
Now, we create the rc.d
files and adjust the permissions:
seed# cat /etc/rc.d/radicle
#!/bin/ksh
daemon="/usr/local/bin/rad"
daemon_flags="node start -- --listen 0.0.0.0:8776"
daemon_user="_seed"
. /etc/rc.d/rc.subr
rc_cmd $1
seed# chmod 555 /etc/rc.d/radicle
seed# cat /etc/rc.d/radiclehttpd
#!/bin/ksh
daemon="/usr/local/bin/radicle-httpd"
daemon_flags="--listen 127.0.0.1:8080"
daemon_user="_seed"
. /etc/rc.d/rc.subr
pexp="$daemon"
rc_bg=YES
rc_cmd $1
seed# chmod 555 /etc/rc.d/radiclehttpd
Make sure to enable and start the services:
seed# rcctl enable radicle
seed# rcctl start radicle
seed# rcctl enable radiclehttpd
seed# rcctl start radiclehttpd
We can test that radicle-httpd
is working now:
seed# curl http://127.0.0.1:8080/api/v1
SSL
To get an SSL certificate (from Let’s Encrypt) we’ll use OpenBSD’s
acme-client
and set up its configuration, as well as a cron job that will make
sure our certificate gets prolonged automatically.
First, create the file /etc/acme-client.conf
with the following content:
authority letsencrypt {
api url "https://acme-v02.api.letsencrypt.org/directory"
account key "/etc/ssl/private/letsencrypt.key"
}
domain seed.domain.com {
domain key "/etc/ssl/private/seed.domain.com.key"
domain certificate "/etc/ssl/seed.domain.com.crt"
domain full chain certificate "/etc/ssl/seed.domain.com.pem"
sign with letsencrypt
}
Make sure to replace seed.domain.com
with the (sub)domain you pointed to your
cloud instance. Next, we’ll need to configure httpd to run on port 80
and
provide access to the ACME challenge. Create the file /etc/httpd.conf
with
the following content:
server "seed.domain.com" {
listen on * port 80
root "/htdocs/seed.domain.com"
location "/.well-known/acme-challenge/*" {
root "/acme"
request strip 2
}
}
Additionally, create the folder /var/www/htdocs/seed.domain.com/
and place an
empty index.html
into it.
Let’s enable httpd:
znc# rcctl enable httpd
znc# rcctl restart httpd
We can now try to issue our SSL certificate by running the following command:
znc# acme-client -v seed.domain.com
Your newly issued SSL certificate should be available under
/etc/ssl/seed.domain.com.{crt,pem}
and /etc/ssl/private/seed.domain.com.key
.
Next, run crontab -e
. This will open the current crontab for editing. Add the
following line at the very end of the file:
0 0 * * * acme-client -v seed.domain.com && rcctl
restart relayd
This will make sure our SSL certificate won’t expire.
relayd
Next we set up relayd
as a reverse proxy for the Radicle HTTP backend.
relayd
takes care of handling SSL termination. We’re going to use the
following configuration under /etc/relayd.conf
:
table <radicle-default-host> { 127.0.0.1 }
http protocol radicle-https {
match request header append "X-Real-IP" value "$REMOTE_ADDR"
match request header append "Host" value "$HOST"
match request header append "X-Forwarded-For" value "$REMOTE_ADDR"
match request header append "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT"
match request path "/*" forward to <radicle-default-host>
tcp { nodelay, sack, backlog 128 }
tls keypair seed.domain.com
tls { no tlsv1.0, ciphers HIGH }
}
relay radicle-https-relay {
listen on egress port 443 tls
protocol radicle-https
forward to <radicle-default-host> port 8080
}
In /etc/rc.conf.local
change relayd_flags
to `` (empty). Then launch relayd:
seed# rcctl enable relayd
seed# rcctl start relayd
We can now verify that the API (radicle-httpd
) is available over the internet:
your-computer$ curl https://seed.domain.com/api/v1
In addition, we can also verify that the Radicle web app can access the node by opening the following link in our web browser:
https://app.radicle.xyz/nodes/seed.domain.com/
And that’s about it. If you have chosen the selective policy for your node,
you might now go ahead and start picking repositories that you want to seed
using the rad seed <uri>
command, e.g.:
seed% /usr/local/bin/rad seed rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
To remove a seeded repository, use the /usr/local/bin/rad unseed <uri>
command. Make sure to run these commands as the _seed
user.
Hint: Make sure to always specify the full path (
/usr/local/bin/rad
), otherwise you end up calling OpenBSD’s router advertisement daemon (/usr/sbin/rad
). You can also adjust yourPATH
for/usr/local/bin
to take precedence over/usr/sbin/
.
If you’d like to collaborate with me on Radicle, check out the projects on my
seed and feel free to sync with it:
DELETED
(see update at the top)
Enjoyed this? Support me via Monero, Bitcoin, Lightning, or Ethereum! More info.