A Python-based self-service SSH server that enables dn42 network maintainers to automatically establish and manage peering connections with AS4242420263.
This project implements a custom SSH server using Paramiko that authenticates users against the dn42 registry. It allows network operators to request peering sessions without manual intervention, storing all peering information in a SQLite database.
- Debian >= 12
- Python 3
- Clone this repository
- Clone the dn42 registry
- Install dependencies:
apt install git python3-dnspython python3-packaging python3-paramiko python3-psutil python3-rich
- Generate a SSH host key with
ssh-keygen
- Set up environment variables (see Configuration section)
- Run the server:
$ python dn42-autopeer.py --server
2025-04-06 22:15:34,391:INFO:[SSHServerBase] Listening thread started
python dn42-autopeer.py --help
usage: dn42-sshd [-h] (--server | --genconfig | --gaming)
options:
-h, --help show this help message and exit
--server Start the auto peering server
--genconfig Generate the configuration from the peering db
--gaming Start the gaming server
These environment variables can be set to configure the service
Variable | Description | Default |
---|---|---|
DN42_SSH_HOST_KEY |
Path to SSH host key file | files/ssh-keys/ssh_host_rsa_key |
DN42_SSH_LISTEN_ADDRESS |
Listen address | ::1 |
DN42_SSH_PORT |
SSH port | 4242 |
DN42_SERVER |
Public domain name of the server | nl-ams2.flap42.eu |
DN42_SSH_MOTD_PATH |
Path to custom MOTD file | files/motd/$DN42_SERVER |
DN42_DB_PATH |
Path to SQLite database file | files/db/$DN42_SERVER |
DN42_REGISTRY_DIRECTORY |
Path to dn42 registry clone | files/registry |
DN42_ASN |
Your dn42 Autonomous System Number | 4242420263 |
DN42_WG_PUB_KEY |
WireGuard public key for tunnels | rj0SORruOE/hGV... |
DN42_WG_PRIV_KEY |
WireGuard private key used in --genconfig |
**REPLACEME** |
DN42_WG_LINK_LOCAL |
IPv6 link-local base address (without last 4 bytes) | fe80:0263:: |
DN42_WG_BASE_PORT |
Base WireGuard port | 52000 |
DN42_RESERVED_NETWORK |
Network where peering is restricted | None |
DN42_BIRD_CONFIG_DIR |
Place to generate the bird config files in --genconfig |
files/bird |
DN42_WG_CONFIG_DIR |
Place to generate the wg config files in --genconfig |
files/wireguard |
I deploy this service on Debian using my own ansible role.
This is an example of a systemd service unit (/etc/systemd/system/dn42-sshd.service
).
[Unit]
Description=DN42 SSHD Service
After=network.target
[Service]
Type=simple
User=dn42-sshd
Group=dn42-sshd
WorkingDirectory=/home/dn42-sshd/dn42-sshd
ExecStart=python3 dn42-sshd.py --peering
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
Environment="DN42_SSH_HOST_KEY=/home/dn42-sshd/.ssh/id_rsa"
Environment="DN42_SSH_LISTEN_ADDRESS=::"
Environment="DN42_SSH_PORT=4242"
Environment="DN42_DB_PATH=/home/dn42-sshd/peering.db"
Environment="DN42_REGISTRY_DIRECTORY=/home/dn42-sshd/dn42-registry"
Environment="DN42_ASN=4242420263"
Environment="DN42_WG_PUB_KEY=C3Wlu6y+v84FN/vreuTqL6r5wEtGTMXX5rKgHkxDaTI="
Environment="DN42_WG_LINK_LOCAL=fe80:0263::"
Environment="DN42_WG_BASE_PORT=52000"
Environment="DN42_RESERVED_NETWORK=2001:0bc8:3feb::/48"
# Security hardening
ProtectSystem=full
PrivateTmp=true
NoNewPrivileges=true
ProtectControlGroups=true
ProtectKernelModules=true
ProtectKernelTunables=true
RestrictNamespaces=true
[Install]
WantedBy=multi-user.target
It assumes that:
- a local user
dn42-sshd
exists - the present git repository is cloned in
/home/dn42-sshd/dn42-sshd
- the dn42 registry is cloned in
/home/dn42-sshd/dn42-registry
- the SQLite database file is
/home/dn42-sshd/peering.db
and is writeable
It can be enabled and started using the systemctl
command:
$ systemctl enable --now dn42-sshd
Created symlink /etc/systemd/system/multi-user.target.wants/dn42-sshd.service → /etc/systemd/system/dn42-sshd.service
The logs are readable using the journalctl
command:
$ journalctl -n 100 -f -u dn42-sshd.service
...
Apr 06 21:57:24 nl-ams2 systemd[1]: Started dn42-sshd.service - DN42 SSHD Service.
Apr 06 21:57:31 nl-ams2 python3[9780]: 2025-04-06 21:57:31,576:INFO:[SSHServerBase] Listening thread started
Apr 06 22:12:28 nl-ams2 python3[9780]: 2025-04-06 22:12:28,505:INFO:[SSHServerBase] Accepted connection from ('2a01:...', 48396, 0, 0)
Apr 06 22:12:28 nl-ams2 python3[9780]: 2025-04-06 22:12:28,509:INFO:Connected (version 2.0, client OpenSSH_9.9)
Apr 06 22:12:28 nl-ams2 python3[9780]: 2025-04-06 22:12:28,777:INFO:Auth rejected (none).
Apr 06 22:12:28 nl-ams2 python3[9780]: 2025-04-06 22:12:28,877:INFO:[AuthDn42] Authentication successful for hcartiaux
Apr 06 22:12:28 nl-ams2 python3[9780]: 2025-04-06 22:12:28,910:INFO:[AuthDn42] Authentication successful for hcartiaux
Apr 06 22:12:28 nl-ams2 python3[9780]: 2025-04-06 22:12:28,911:INFO:Auth granted (publickey).
Apr 06 22:12:28 nl-ams2 python3[9780]: 2025-04-06 22:12:28,942:INFO:[SSHServerBase] Started thread 139777270654656 for hcartiaux@('2a01:...', 48396, 0, 0)
Apr 06 22:14:51 nl-ams2 python3[9780]: 2025-04-06 22:14:51,924:INFO:[139777270654656][SSHServerShell] User hcartiaux disconnected
Anonymous, class SSHServerAuthNone
Accept all connections
dn42 registry, class SSHServerAuthDn42
Accept connections based on the dn42 maintainer objects, using the defined public key(s)
- username: lowercase maintainer name, without the
-MNT
suffix - public keys: supports
ssh-rsa
andssh-ed25519
keys defined in theauth
field
Common constructor parameters:
server_interface
- can be set toSSHServerAuthNone
orSSHServerAuthDn42
host_key_file
- path of the ssh host key file
Piping, class SSHServerPipe(server_interface, cmd, host_key_file)
cmd
- command to be executed and piped to the user
shell_class
- name of theCmd
subclass to be used as shell instances.
The custom shell is implemented by the class ShellDn42(username, ...)
.
It extends the Cmd
class heavily.
The SQLite database is entirely managed using the class DatabaseManager(db_path). It contains only one table.
CREATE TABLE IF NOT EXISTS peering_links (
id INTEGER PRIMARY KEY CHECK(id BETWEEN 1 AND 65535),
as_num INTEGER UNIQUE NOT NULL,
wg_pub_key TEXT NOT NULL,
wg_endpoint_addr TEXT NOT NULL,
wg_endpoint_port INTEGER NOT NULL CHECK(wg_endpoint_port BETWEEN 1 AND 65535)
- Ansible role for deployment
- PythonSSHServerTutorial (Used as a starting point)