close
View as single-page

Knot self-hosting guide

So you want to run your own knot server? Great! Here are a few prerequisites:

  1. A server of some kind (a VPS, a Raspberry Pi, etc.). Preferably running a Linux distribution of some kind.
  2. A (sub)domain name. People generally use knot.example.com.
  3. A valid SSL certificate for your domain.

NixOS

Refer to the knot module for a full list of options. Sample configurations:

Docker

Refer to @tangled.org/knot-docker. Note that this is community maintained.

Manual setup

First, clone this repository:

git clone https://tangled.org/@tangled.org/core

Then, build the knot CLI. This is the knot administration and operation tool. For the purpose of this guide, we’re only concerned with these subcommands:

  • knot server: the main knot server process, typically run as a supervised service
  • knot guard: handles role-based access control for git over SSH (you’ll never have to run this yourself)
  • knot keys: fetches SSH keys associated with your knot; we’ll use this to generate the SSH AuthorizedKeysCommand
cd core
export CGO_ENABLED=1
go build -o knot ./cmd/knot

Next, move the knot binary to a location owned by root/usr/local/bin/ is a good choice. Make sure the binary itself is also owned by root:

sudo mv knot /usr/local/bin/knot
sudo chown root:root /usr/local/bin/knot

This is necessary because SSH AuthorizedKeysCommand requires really specific permissions. The AuthorizedKeysCommand specifies a command that is run by sshd to retrieve a user’s public SSH keys dynamically for authentication. Let’s set that up.

sudo tee /etc/ssh/sshd_config.d/authorized_keys_command.conf <<EOF
Match User git
  AuthorizedKeysCommand /usr/local/bin/knot keys -o authorized-keys
  AuthorizedKeysCommandUser nobody
EOF

Then, reload sshd:

sudo systemctl reload ssh

Next, create the git user. We’ll use the git user’s home directory to store repositories:

sudo adduser git

Create /home/git/.knot.env with the following, updating the values as necessary. The KNOT_SERVER_OWNER should be set to your DID, you can find your DID in the Settings page.

KNOT_REPO_SCAN_PATH=/home/git
KNOT_SERVER_HOSTNAME=knot.example.com
APPVIEW_ENDPOINT=https://tangled.org
KNOT_SERVER_OWNER=did:plc:foobar
KNOT_SERVER_INTERNAL_LISTEN_ADDR=127.0.0.1:5444
KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555

If you run a Linux distribution that uses systemd, you can use the provided service file to run the server. Copy knotserver.service to /etc/systemd/system/. Then, run:

systemctl enable knotserver
systemctl start knotserver

The last step is to configure a reverse proxy like Nginx or Caddy to front your knot. Here’s an example configuration for Nginx:

server {
    listen 80;
    listen [::]:80;
    server_name knot.example.com;

    location / {
        proxy_pass http://localhost:5555;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # wss endpoint for git events
    location /events {
        proxy_set_header   X-Forwarded-For $remote_addr;
        proxy_set_header   Host $http_host;
        proxy_set_header Upgrade websocket;
        proxy_set_header Connection Upgrade;
        proxy_pass http://localhost:5555;
    }
  # additional config for SSL/TLS go here.
}

Remember to use Let’s Encrypt or similar to procure a certificate for your knot domain.

You should now have a running knot server! You can finalize your registration by hitting the verify button on the /settings/knots page. This simply creates a record on your PDS to announce the existence of the knot.

Custom paths

(This section applies to manual setup only. Docker users should edit the mounts in docker-compose.yml instead.)

Right now, the database and repositories of your knot lives in /home/git. You can move these paths if you’d like to store them in another folder. Be careful when adjusting these paths:

  • Stop your knot when moving data (e.g. systemctl stop knotserver) to prevent any possible side effects. Remember to restart it once you’re done.
  • Make backups before moving in case something goes wrong.
  • Make sure the git user can read and write from the new paths.

Database

As an example, let’s say the current database is at /home/git/knotserver.db, and we want to move it to /home/git/database/knotserver.db.

Copy the current database to the new location. Make sure to copy the .db-shm and .db-wal files if they exist.

mkdir /home/git/database
cp /home/git/knotserver.db* /home/git/database

In the environment (e.g. /home/git/.knot.env), set KNOT_SERVER_DB_PATH to the new file path (not the directory):

KNOT_SERVER_DB_PATH=/home/git/database/knotserver.db

Repositories

As an example, let’s say the repositories are currently in /home/git, and we want to move them into /home/git/repositories.

Create the new folder, then move the existing repositories (if there are any):

mkdir /home/git/repositories
# move all DIDs into the new folder; these will vary for you!
mv /home/git/did:plc:wshs7t2adsemcrrd4snkeqli /home/git/repositories

In the environment (e.g. /home/git/.knot.env), update KNOT_REPO_SCAN_PATH to the new directory:

KNOT_REPO_SCAN_PATH=/home/git/repositories

Similarly, update your sshd AuthorizedKeysCommand to use the updated repository path:

sudo tee /etc/ssh/sshd_config.d/authorized_keys_command.conf <<EOF
Match User git
  AuthorizedKeysCommand /usr/local/bin/knot keys -o authorized-keys -git-dir /home/git/repositories
  AuthorizedKeysCommandUser nobody
EOF

Make sure to restart your SSH server!

MOTD (message of the day)

To configure the MOTD used (“Welcome to this knot!” by default), edit the /home/git/motd file:

printf "Hi from this knot!\n" > /home/git/motd

Note that you should add a newline at the end if setting a non-empty message since the knot won’t do this for you.

Secure Mode

Secure Mode isolates each git subprocess to the repository it is operating on, using two mechanisms:

  • Linux Landlock restricts the filesystem paths the subprocess can access – it can only read/write its own repository and the system directories it needs to run.
  • UID isolation runs each subprocess as a virtual UID assigned to the repository owner, so that repositories belonging to different owners are isolated from each other at the OS level even if Landlock were somehow bypassed.

Secure Mode requires:

  • Linux kernel >= 5.19 (Landlock V2). This is the minimum needed for git push to work, because receive-pack’s quarantine migration uses cross-directory rename which requires the Landlock REFER access right (added in V2). Kernels 5.13-5.18 support Landlock V1 and clones will work, but pushes will fail with cross-device link errors. On kernels without any Landlock support (< 5.13), the sandbox call is a no-op: UID isolation still applies but no filesystem restriction is enforced.
  • CAP_SETUID, CAP_SETGID, and CAP_CHOWN available to the knot process. The NixOS module grants these automatically; for manual setups see the setcap step below.

NixOS

Add server.secureMode = true; to your knot module configuration:

services.tangled.knot = {
  server.secureMode = true;
  # ... other options
};

The NixOS module handles everything else automatically:

  • Grants the required capabilities to the knot service via AmbientCapabilities in the systemd unit.
  • Installs a capability-bearing wrapper at /run/wrappers/bin/knot via security.wrappers, so that SSH-invoked git operations (pushes) also run under the correct UID without requiring the service to run as root.
  • Runs knot migrate-isolation at service start to chown existing repositories to their virtual UIDs.

Manual setup

Step 1. Grant the required capabilities to the knot binary. This allows the knot process to switch to virtual UIDs at runtime without running as root. You will need to repeat this step whenever the binary is updated.

sudo setcap cap_setuid,cap_setgid,cap_chown+eip /usr/local/bin/knot

Step 2. Run the migration tool to assign virtual UIDs to all existing repositories and set their filesystem permissions. This must be run as root:

sudo knot migrate-isolation \
  --git-dir /home/git \
  --db /home/git/knotserver.db \
  --internal-api 127.0.0.1:5444

You can re-run this at any time with --force to reapply permissions (e.g. after a manual repair or after updating the binary).

Step 2a. Ensure the home directory is traversable by non-group users. Git subprocesses run as virtual UIDs that are not in the git group, and they need to resolve $HOME/.config/git/config to load the global config:

sudo chmod o+x /home/git

This adds only the execute bit, not read – the virtual UIDs can traverse to known paths but cannot list directory contents.

Step 3. Enable Secure Mode in your environment file:

KNOT_SERVER_SECURE_MODE=true

Or pass it as a flag:

knot server --secure-mode

Step 4. Regenerate the AuthorizedKeysCommand with the -secure-mode flag. This causes knot keys to emit guard command lines that include -secure-mode, so SSH pushes also get UID isolation:

sudo tee /etc/ssh/sshd_config.d/authorized_keys_command.conf <<EOF
Match User git
  AuthorizedKeysCommand /usr/local/bin/knot keys \
    -o authorized-keys -secure-mode
  AuthorizedKeysCommandUser nobody
EOF

Reload sshd after making this change.

Note: the server will refuse to start in Secure Mode if any repositories have not yet been isolation-migrated. Re-run migrate-isolation if you see this error.

Troubleshooting

If you run your own knot, you may run into some of these common issues. You can always join the IRC or Discord if this section does not help.

Unable to push

If you are unable to push to your knot or repository:

  1. First, ensure that you have added your SSH public key to your account
  2. Check to see that your knot has synced the key by running knot keys
  3. Check to see if git is supplying the correct private key when pushing: GIT_SSH_COMMAND="ssh -v" git push ...
  4. Check to see if sshd on the knot is rejecting the push for some reason: journalctl -xeu ssh (or sshd, depending on your machine). These logs are unavailable if using docker.
  5. Check to see if the knot itself is rejecting the push, depending on your setup, the logs might be in one of the following paths:
    • /tmp/knotguard.log
    • /home/git/log
    • /home/git/guard.log