Using WireGuard Keys for SSH
The best practice for managing SSH keys is to use SSH CA host and user certificates. However, if you’re already using WireGuard to connect two machines in a point-to-point network, you can skip SSH key management entirely, and simply rely on WireGuard for authentication. This article will show you how.
This concept works because WireGuard provides an encrypted connection with mutual authentication, just like SSH. In a point-to-point configuration between two endpoints, like the following between Endpoint A and Endpoint B, each WireGuard endpoint has been configured with the other’s public key, and uses that key to cryptographically authenticate the other side of the connection:
Furthermore, WireGuard’s cryptokey routing automatically blocks traffic from flowing through the WireGuard tunnel unless you allow-list it by peer and IP address. Therefore, if we configure our WireGuard interface on Endpoint B to accept connections from Endpoint A with traffic from 10.0.0.1
, WireGuard guarantees us that any packet which emerges from that interface with a source address of 10.0.0.1
legitimately has been sent to us from Endpoint A (or from a host that controls Endpoint A’s private key):
So if we configure our WireGuard interface on Endpoint B to accept connections from Endpoint A that have a source address of 10.0.0.1
— and configure the firewall on Endpoint B to drop connections that have a source address of 10.0.0.1
from anywhere but this interface — each network service we run on Endpoint B can trust that any connection to it with a source address of 10.0.0.1
legitimately came from Endpoint A.
And if we trust that an individual user (like Alice) has sole control of Endpoint A’s WireGuard private key, then we can automatically log her into any network service (like SSH) on Endpoint B whenever the connection has a source address of 10.0.0.1
:
To do this for SSH, these are the components we need to configure on Endpoint B:
With this configuration in place on Endpoint B, we can then Test It Out from Endpoint A (and do any Troubleshooting necessary). As a bonus, at the end of this article we’ll also show how we could ditch SSH entirely, replacing it with Telnet and FTP (or RSH) — encrypted and authenticated by WireGuard.
WireGuard Configuration
First, we’ll set up WireGuard on Endpoint A and Endpoint B just like the WireGuard Point to Point Configuration article (refer to it for a detailed explanation of each config setting).
On Endpoint A, we’ll use the following WireGuard config:
# /etc/wireguard/wg0.conf
# local settings for Endpoint A
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.0.1/32
ListenPort = 51821
# remote settings for Endpoint B
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
Endpoint = 203.0.113.2:51822
AllowedIPs = 10.0.0.2/32
This configuration means that Endpoint A will use a source IP address of 10.0.0.1
for its connections to Endpoint B, and Endpoint A will accept only connections with a source IP address of 10.0.0.2
from Endpoint B.
On Endpoint B, we’ll use the following WireGuard config:
# /etc/wireguard/wg0.conf
# local settings for Endpoint B
[Interface]
PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
Address = 10.0.0.2/32
ListenPort = 51822
# remote settings for Endpoint A
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
AllowedIPs = 10.0.0.1/32
This configuration means that Endpoint B will use a source IP address of 10.0.0.2
for its connections to Endpoint A, and Endpoint B will accept only connections with a source IP address of 10.0.0.1
from Endpoint A.
SSHD Configuration
Since we’ll use PAM to accomplish authentication (covered a few sections below), we need to enable it in our sshd_config
file on Endpoint B. To enable PAM for authentication, we need to make sure the (Linux-specific) UsePAM
setting is yes
— plus either KbdInteractiveAuthentication
or PasswordAuthentication
is also set to yes
:
# /etc/ssh/sshd_config
...
UsePAM yes
KbdInteractiveAuthentication yes
# or PasswordAuthentication yes
Note
|
The |
Tip
|
If you already use PAM-based SSH authentication on Endpoint B, you will already have the above settings in your If you don’t use PAM for SSH authentication, and use only public-key authentication for SSH on Endpoint B, you can further harden your
This will ensure that:
|
We can double-check that our sshd_config
file is valid by running the sshd -t
command (no output means OK), and then reload it with our changes:
$ sudo sshd -t $ sudo systemctl reload sshd
Netgroup Configuration
Netgroups are an old-school way of associating a local user account with a remote host. They’re completely distinct from regular account groups. We’ll use netgroups to connect local user accounts with remote WireGuard IP addresses — such as to connect the alice
user account on Endpoint B with Endpoint A’s WireGuard IP address of 10.0.0.1
.
First, we’ll create an /etc/netgroup
file on Endpoint B, and add an entry for each local user account on Endpoint B that we want to associate with a remote WireGuard address; plus we’ll add a master wg-users
entry that combines each per-user netgroup into a larger group for all WireGuard users:
# /etc/netgroup
wg-alice (10.0.0.1,alice,)
wg-bob (10.0.0.3,bob,)
wg-users wg-alice wg-bob
Each entry in the /etc/netgroup
file specifies a separate netgroup; each entry consists of the netgroup name listed first (eg wg-alice
), followed the members the netgroup (eg the (10.0.0.1,alice,)
combination), separated by spaces. Members of a netgroup may be a (host,user,domain)
combination, or they may be other named netgroups.
We’ll leave the domain
field blank when we specify our netgroups. Our wg-alice
netgroup combines the WireGuard IP address 10.0.0.1
(Endpoint A) with the alice
user account; our wg-bob
netgroup combines the WireGuard IP address 10.0.0.3
(some other host) with the bob
user account; and our wg-users
netgroup includes both combinations ((10.0.0.1,alice,)
and (10.0.0.3,bob,)
).
After creating the /etc/netgroup
file as root, make sure to make it readable (but not writeable) to other users:
$ sudo chmod 644 /etc/netgroup
Once we’ve defined our netgroups in the /etc/netgroup
file, we’ll configure them to be used by editing the /etc/nsswitch.conf
file. Edit it, and change its netgroup
entry to include files
(if it isn’t already using files
as one of its netgroup
data sources):
# /etc/nsswitch.conf
...
netgroup: files
Note
|
Traditionally, netgroups were often configured via a NIS (Network Information Service) server; but in more modern times they may also be configured via LDAP. If you do actually use netgroups with NIS or LDAP, don’t edit your /etc/netgroup and /etc/nsswitch.conf files — use your existing NIS or LDAP servers to configure a new set of netgroups to connect WireGuard IP addresses with users, as above.
|
Once we make the above configuration changes, we can query the members of a netgroup via the getent
command. For example, we can query the contents of the wg-users
netgroup to see that it contains the following combinations of WireGuard IP addresses and user accounts:
$ getent netgroup wg-users wg-users (10.0.0.3,bob,) (10.0.0.1,alice,)
PAM Configuration
Now we can update our PAM configuration to automatically authenticate members of our wg-users
netgroup when they attempt to log in via SSH. We’ll add the following PAM rule to the top of the /etc/pam.d/sshd
file on Endpoint B:
# /etc/pam.d/sshd
auth sufficient pam_succeed_if.so user innetgr wg-users
...
This will authenticate any attempt to log into Endpoint B via SSH if the IP address of the remote host from which the attempt is being made, together with the user account being logged into, matches a host+user combination from our wg-users
netgroup. Because we’ve used the sufficient
PAM keyword in this rule, if a user doesn’t match a host+user combination, the other authentication methods defined in the /etc/pam.d/sshd
config file (such as SSH public-key or password authentication) may allow the user to log in; but if the host+user combination does match, no other authentication methods will be attempted or required.
With the netgroups we’ve defined above, any SSH connection coming from the 10.0.0.1
IP address that tries to log into the alice
user account will automatically succeed; and any SSH connection coming from the 10.0.0.3
IP address that tries to log into the bob
user account will also automatically succeed. (But note that in our WireGuard configuration for Endpoint B above, we did not define a [Peer]
section with an AllowedIPs = 10.0.0.3
setting — so no one will actually be able to log into Endpoint B as bob
through WireGuard.)
Firewall Configuration
With the Netgroup and PAM configuration above, we’ve enabled any host using a source IP address of 10.0.0.1
to SSH into Endpoint B as the alice
user; and with the WireGuard configuration at the beginning of the article, we’ve guaranteed that the initiator of any connection coming in the WireGuard interface on Endpoint B with a source IP address of 10.0.0.1
legitimately holds the WireGuard private key to Endpoint A.
The one remaining hole we have in this system is the other network interfaces on Endpoint B: If an adversary controls a host on the same LAN subnet as Endpoint B, she could establish an SSH connection to Endpoint B through its LAN network interface using a spoofed source IP address of her own choosing — such as 10.0.0.1
. Furthermore, if Endpoint B was also using other, non-WireGuard virtual network interfaces, those virtual interfaces might similarly allow for spoofed or dynamic source IP addresses.
We can block spoofed source IP addresses to other interfaces on Endpoint B in one of two ways:
Anti-Spoofing Kernel Parameters
If we’re only using IPv4, and we’re not doing any fancy routing or other virtual networking on Endpoint B, we can set the net.ipv4.conf.all.rp_filter
kernel parameter to 1
to configure the Linux kernel to automatically enforce strict RPF (Reverse Path Forwarding) SAV (Source Address Validation) on the IPv4 packets it receives. The best way to do this is to add the following lines to Endpoint B’s /etc/sysctl.conf
file:
# /etc/sysctl.conf
...
net.ipv4.conf.default.rp_filter=1
net.ipv4.conf.all.rp_filter=1
Then run the following command on Endpoint B to re-apply all the kernel parameters set in your sysctl config files:
$ sudo sysctl --system
Note
|
These kernel parameters are available only for IPv4; for IPv6 you have to use Anti-Spoofing Firewall Rules. |
Anti-Spoofing Firewall Rules
Otherwise, we need to manually configure one or more firewall rules to drop any traffic with a source IP address in our WireGuard network’s range (10.0.0.0/24
) which Endpoint B did not receive through its WireGuard interface. There are a variety of different ways to do this; but with nftables, the most direct way is to add a rule early in the packet-filtering process that checks the interface and source address of incoming packets, and drops packets if they did not come from WireGuard but are nonetheless using a WireGuard source address:
table inet raw {
chain prerouting {
type filter hook prerouting priority raw; policy accept;
# drop spoofed packets pretending to come through WireGuard tunnel
iifname != "wg0" ip saddr 10.0.0.0/24 drop
}
}
The iptables equivalent would be a rule like the following:
iptables -t raw -I PREROUTING ! -i wg0 -s 10.0.0.0/24 -j DROP
Alternatively, we could add an nftables rule that generically checks the interface and source address of incoming packets against Endpoint B’s routing table, and if the interface on which a packet was received is not the interface that reply packets to it will be sent back out, drop the packet:
table inet raw {
chain prerouting {
type filter hook prerouting priority raw; policy accept;
# enforce strict RPF
fib saddr . mark . iif oif missing drop
}
}
The iptables equivalent would be a rule like the following:
iptables -t raw -I PREROUTING -m rpfilter --validmark --invert -j DROP
This will work nicely to block most address spoofing attempts in general (although it can cause problems if you’re doing fancy routing on Endpoint B where you actually need to accept legitimate incoming traffic on a network interface that doesn’t match the best route out).
For this article’s scenario, the following nftables configuration is ideal for Endpoint B. It combines the above generic prerouting hook, to protect against source address spoofing; with the recommended nftables base configuration from the How to Use WireGuard With Nftables guide, limiting external access to the host to ICMP, SSH, and WireGuard connections; plus it adds an additional rule to allow full access to all of Endpoint B’s network services through the WireGuard tunnel:
#!/usr/sbin/nft -f
flush ruleset
define pub_iface = "eth0"
define wg_iface = "wg0"
define wg_port = 51822
table inet raw {
chain prerouting {
type filter hook prerouting priority raw; policy accept;
# enforce strict RPF
fib saddr . mark . iif oif missing drop
}
}
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# accept all loopback packets
iif "lo" accept
# accept all icmp/icmpv6 packets
meta l4proto { icmp, ipv6-icmp } accept
# accept all packets that are part of an already-established connection
ct state vmap { invalid : drop, established : accept, related : accept }
# drop new connections over rate limit
ct state new limit rate over 1/second burst 10 packets drop
# accept all DHCPv6 packets received at a link-local address
ip6 daddr fe80::/64 udp dport dhcpv6-client accept
# accept all SSH packets received on a public interface
# (remove to block SSH except through WireGuard)
iifname $pub_iface tcp dport ssh accept
# accept all WireGuard packets received on a public interface
iifname $pub_iface udp dport $wg_port accept
# accept all packets received through the WireGuard tunnel
iifname $wg_iface accept
# reject with polite "port unreachable" icmp response
reject
}
chain forward {
type filter hook forward priority 0; policy drop;
reject with icmpx type host-unreachable
}
}
To apply the above nftables configuration, replace Endpoint B’s /etc/nftables.conf
(or /etc/sysconfig/nftables.conf
) file, and then restart the nftables service (eg sudo systemctl restart nftables
).
Test It Out
Start up the WireGuard interfaces on both Endpoint A and Endpoint B (eg by running sudo wg-quick up wg0
on both machines).
Then on Endpoint A, add the following to the top of Alice’s SSH config file (~/.ssh/config
):
# ~/.ssh/config
Host wg-endpoint-b
User alice
Hostname 10.0.0.2
PubkeyAuthentication no
StrictHostKeyChecking no
UserKnownHostsFile /tmp/known_hosts
LocalCommand rm /tmp/known_hosts
PermitLocalCommand yes
This sets up a host alias for wg-endpoint-b
. The User alice
and Hostname 10.0.0.2
directives configure SSH to use wg-endpoint-b
as an alias for alice@10.0.0.2
. The PubkeyAuthentication no
will prevent SSH from trying SSH public-key authentication. The rest of the directives work together to ignore the SSH host key provided by Endpoint B (as we will instead be relying on Endpoint B’s WireGuard key to authenticate the connection to Endpoint B), and to avoid polluting Alice’s usual known_hosts
file with unused entries from Endpoint B.
Specifically, the StrictHostKeyChecking no
directive instructs SSH accept any new SSH host keys from Endpoint B, and automatically add them to Alice’s known_hosts
file. The UserKnownHostsFile /tmp/known_hosts
directive instructs SSH to use /tmp/known_hosts
instead of ~/.ssh/known_hosts
as Alice’s known_hosts
file for Endpoint B. The LocalCommand rm /tmp/known_hosts
directive instructs SSH to delete the /tmp/known_hosts
file after each successful authentication with Endpoint B. And the PermitLocalCommand yes
directive enables the use of LocalCommand
.
After updating Alice’s SSH config file, run the following command as Alice on Endpoint A:
$ ssh wg-endpoint-b Warning: Permanently added '10.0.0.2' (ED25519) to the list of known hosts. Last login: Sat Feb 3 12:25:32 2024 from 198.51.100.1 alice@bee:~$
This should connect to Endpoint B, and log in Alice automatically, without prompting for a password.
Troubleshooting
If this doesn’t work, check the SSH logs on Endpoint B. If Endpoint B uses systemd, you can use the journalctl -u sshd
command to view them. This is what a successful authentication should look like:
$ journalctl -u sshd -e ... Feb 03 23:15:15 bee sshd[47919]: pam_succeed_if(sshd:auth): requirement "user innetgr wg-users" was met by user "alice" Feb 03 23:15:15 bee sshd[47893]: Accepted keyboard-interactive/pam for alice from 10.0.0.1 port 58164 ssh2 Feb 03 23:15:15 bee sshd[47893]: pam_unix(sshd:session): session opened for user alice(uid=1001) by alice(uid=0)
You may also find more information in the full auth logs on Endpoint B. If Endpoint B uses systemd, you can use the journalctl --facility auth
command to view them. This is what a successful authentication should look like (on a system running SELinux):
$ journalctl --facility auth -e ... Feb 03 23:15:14 bee audit[47903]: CRYPTO_KEY_USER pid=47903 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=destroy kind=server fp=SHA256:18:92:11:a8:e5:d6:e0:f4:b3:52:37:69:5c:64:28:81:2e:2c:7e:a0:41:1a:43:24:98:0a:6b:6d:d9:22:66:29 direction=? spid=47903 suid=0 exe="/usr/sbin/sshd" hostname=? addr=10.0.0.1 terminal=? res=success' Feb 03 23:15:15 bee audit[47893]: CRYPTO_SESSION pid=47893 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=start direction=from-server cipher=chacha20-poly1305@openssh.com ksize=512 mac=<implicit> pfs=curve25519-sha256 spid=47903 suid=74 rport=58164 laddr=10.0.0.2 lport=22 exe="/usr/sbin/sshd" hostname=? addr=10.0.0.1 terminal=? res=success' Feb 03 23:15:15 bee audit[47893]: CRYPTO_SESSION pid=47893 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=start direction=from-client cipher=chacha20-poly1305@openssh.com ksize=512 mac=<implicit> pfs=curve25519-sha256 spid=47903 suid=74 rport=58164 laddr=10.0.0.2 lport=22 exe="/usr/sbin/sshd" hostname=? addr=10.0.0.1 terminal=? res=success' Feb 03 23:15:15 bee audit[47919]: USER_AUTH pid=47919 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=PAM:authentication grantors=pam_succeed_if acct="alice" exe="/usr/sbin/sshd" hostname=10.0.0.1 addr=10.0.0.1 terminal=ssh res=success' Feb 03 23:15:15 bee audit[47919]: USER_ACCT pid=47919 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=PAM:accounting grantors=pam_unix,pam_localuser acct="alice" exe="/usr/sbin/sshd" hostname=10.0.0.1 addr=10.0.0.1 terminal=ssh res=success' Feb 03 23:15:15 bee audit[47893]: CRYPTO_KEY_USER pid=47893 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=destroy kind=session fp=? direction=both spid=47903 suid=74 rport=58164 laddr=10.0.0.2 lport=22 exe="/usr/sbin/sshd" hostname=? addr=10.0.0.1 terminal=? res=success' Feb 03 23:15:15 bee audit[47893]: CRED_ACQ pid=47893 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=PAM:setcred grantors=pam_env,pam_localuser,pam_unix acct="alice" exe="/usr/sbin/sshd" hostname=10.0.0.1 addr=10.0.0.1 terminal=ssh res=success' Feb 03 23:15:15 bee audit[47893]: USER_ROLE_CHANGE pid=47893 uid=0 auid=1001 ses=5 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='pam: default-context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 selected-context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 exe="/usr/sbin/sshd" hostname=10.0.0.1 addr=10.0.0.1 terminal=ssh res=success' Feb 03 23:15:15 bee systemd-logind[741]: New session 5 of user alice. Feb 03 23:15:15 bee audit[1]: SERVICE_START pid=1 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:init_t:s0 msg='unit=user-runtime-dir@1001 comm="systemd" exe="/usr/lib/systemd/systemd" hostname=? addr=? terminal=? res=success' Feb 03 23:15:15 bee audit[47924]: USER_ACCT pid=47924 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:init_t:s0 msg='op=PAM:accounting grantors=pam_unix acct="alice" exe="/usr/lib/systemd/systemd" hostname=? addr=? terminal=? res=success' Feb 03 23:15:15 bee audit[47924]: CRED_ACQ pid=47924 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:init_t:s0 msg='op=PAM:setcred grantors=? acct="alice" exe="/usr/lib/systemd/systemd" hostname=? addr=? terminal=? res=failed' Feb 03 23:15:15 bee audit[47924]: USER_ROLE_CHANGE pid=47924 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:init_t:s0 msg='pam: default-context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 selected-context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 exe="/usr/lib/systemd/systemd" hostname=? addr=? terminal=? res=success' Feb 03 23:15:15 bee audit[47924]: USER_START pid=47924 uid=0 auid=1001 ses=6 subj=system_u:system_r:init_t:s0 msg='op=PAM:session_open grantors=pam_selinux,pam_selinux,pam_loginuid,pam_keyinit,pam_namespace,pam_systemd_home,pam_umask,pam_keyinit,pam_limits,pam_systemd,pam_unix acct="alice" exe="/usr/lib/systemd/systemd" hostname=? addr=? terminal=? res=success' Feb 03 23:15:15 bee audit[1]: SERVICE_START pid=1 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:init_t:s0 msg='unit=user@1001 comm="systemd" exe="/usr/lib/systemd/systemd" hostname=? addr=? terminal=? res=success' Feb 03 23:15:15 bee audit[47893]: USER_START pid=47893 uid=0 auid=1001 ses=5 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=PAM:session_open grantors=pam_selinux,pam_loginuid,pam_selinux,pam_namespace,pam_keyinit,pam_keyinit,pam_limits,pam_systemd,pam_unix,pam_umask,pam_lastlog acct="alice" exe="/usr/sbin/sshd" hostname=10.0.0.1 addr=10.0.0.1 terminal=ssh res=success' Feb 03 23:15:15 bee audit[47936]: CRYPTO_KEY_USER pid=47936 uid=0 auid=1001 ses=5 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=destroy kind=server fp=SHA256:18:92:11:a8:e5:d6:e0:f4:b3:52:37:69:5c:64:28:81:2e:2c:7e:a0:41:1a:43:24:98:0a:6b:6d:d9:22:66:29 direction=? spid=47936 suid=0 exe="/usr/sbin/sshd" hostname=? addr=10.0.0.1 terminal=? res=success' Feb 03 23:15:15 bee audit[47936]: CRED_ACQ pid=47936 uid=0 auid=1001 ses=5 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=PAM:setcred grantors=pam_env,pam_localuser,pam_unix acct="alice" exe="/usr/sbin/sshd" hostname=10.0.0.1 addr=10.0.0.1 terminal=ssh res=success' Feb 03 23:15:15 bee audit[47893]: USER_LOGIN pid=47893 uid=0 auid=1001 ses=5 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=login id=1001 exe="/usr/sbin/sshd" hostname=? addr=10.0.0.1 terminal=/dev/pts/3 res=success' Feb 03 23:15:15 bee audit[47893]: USER_START pid=47893 uid=0 auid=1001 ses=5 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=login id=1001 exe="/usr/sbin/sshd" hostname=? addr=10.0.0.1 terminal=/dev/pts/3 res=success' Feb 03 23:15:15 bee audit[47893]: CRYPTO_KEY_USER pid=47893 uid=0 auid=1001 ses=5 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=destroy kind=server fp=SHA256:18:92:11:a8:e5:d6:e0:f4:b3:52:37:69:5c:64:28:81:2e:2c:7e:a0:41:1a:43:24:98:0a:6b:6d:d9:22:66:29 direction=? spid=47937 suid=1001 exe="/usr/sbin/sshd" hostname=? addr=10.0.0.1 terminal=? res=success' Feb 03 23:15:15 bee audit: BPF prog-id=78 op=LOAD Feb 03 23:15:15 bee audit: BPF prog-id=79 op=LOAD Feb 03 23:15:15 bee audit: BPF prog-id=80 op=LOAD Feb 03 23:15:15 bee audit[1]: SERVICE_START pid=1 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:init_t:s0 msg='unit=systemd-hostnamed comm="systemd" exe="/usr/lib/systemd/systemd" hostname=? addr=? terminal=? res=success'
If nothing shows up in the auth logs on Endpoint B, try using the SSH -vvv
option on Endpoint A when connecting. A successful authentication should look like this:
$ ssh -vvv wg-endpoint-b OpenSSH_9.6p1, OpenSSL 3.1.4 24 Oct 2023 debug1: Reading configuration data /home/ally/.ssh/config debug1: /home/ally/.ssh/config line 1: Applying options for wg-endpoint-b debug1: Reading configuration data /etc/ssh/ssh_config debug1: /etc/ssh/ssh_config line 22: include /etc/ssh/ssh_config.d/*.conf matched no files debug2: resolve_canonicalize: hostname 10.0.0.2 is address debug3: channel_clear_timeouts: clearing debug3: ssh_connect_direct: entering debug1: Connecting to 10.0.0.2 [10.0.0.2] port 22. debug3: set_sock_tos: set socket 3 IP_TOS 0x48 debug1: Connection established. debug1: identity file /home/ally/.ssh/id_rsa type -1 debug1: identity file /home/ally/.ssh/id_rsa-cert type -1 debug1: identity file /home/ally/.ssh/id_ecdsa type -1 debug1: identity file /home/ally/.ssh/id_ecdsa-cert type -1 debug1: identity file /home/ally/.ssh/id_ecdsa_sk type -1 debug1: identity file /home/ally/.ssh/id_ecdsa_sk-cert type -1 debug1: identity file /home/ally/.ssh/id_ed25519 type -1 debug1: identity file /home/ally/.ssh/id_ed25519-cert type -1 debug1: identity file /home/ally/.ssh/id_ed25519_sk type -1 debug1: identity file /home/ally/.ssh/id_ed25519_sk-cert type -1 debug1: identity file /home/ally/.ssh/id_xmss type -1 debug1: identity file /home/ally/.ssh/id_xmss-cert type -1 debug1: identity file /home/ally/.ssh/id_dsa type -1 debug1: identity file /home/ally/.ssh/id_dsa-cert type -1 debug1: Local version string SSH-2.0-OpenSSH_9.6 debug1: Remote protocol version 2.0, remote software version OpenSSH_9.3 debug1: compat_banner: match: OpenSSH_9.3 pat OpenSSH* compat 0x04000000 debug2: fd 3 setting O_NONBLOCK debug1: Authenticating to 10.0.0.2:22 as 'alice' debug1: load_hostkeys: fopen /tmp/known_hosts: No such file or directory debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory debug3: order_hostkeyalgs: no algorithms matched; accept original debug3: send packet: type 20 debug1: SSH2_MSG_KEXINIT sent debug3: receive packet: type 20 debug1: SSH2_MSG_KEXINIT received debug2: local client KEXINIT proposal debug2: KEX algorithms: sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,ext-info-c,kex-strict-c-v00@openssh.com debug2: host key algorithms: ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,sk-ecdsa-sha2-nistp256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,sk-ssh-ed25519@openssh.com,sk-ecdsa-sha2-nistp256@openssh.com,rsa-sha2-512,rsa-sha2-256 debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com debug2: MACs ctos: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 debug2: MACs stoc: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1 debug2: compression ctos: none,zlib@openssh.com,zlib debug2: compression stoc: none,zlib@openssh.com,zlib debug2: languages ctos: debug2: languages stoc: debug2: first_kex_follows 0 debug2: reserved 0 debug2: peer server KEXINIT proposal debug2: KEX algorithms: curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,kex-strict-s-v00@openssh.com debug2: host key algorithms: rsa-sha2-512,rsa-sha2-256,ecdsa-sha2-nistp256,ssh-ed25519 debug2: ciphers ctos: aes256-gcm@openssh.com,chacha20-poly1305@openssh.com,aes256-ctr,aes128-gcm@openssh.com,aes128-ctr debug2: ciphers stoc: aes256-gcm@openssh.com,chacha20-poly1305@openssh.com,aes256-ctr,aes128-gcm@openssh.com,aes128-ctr debug2: MACs ctos: hmac-sha2-256-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256,hmac-sha1,umac-128@openssh.com,hmac-sha2-512 debug2: MACs stoc: hmac-sha2-256-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256,hmac-sha1,umac-128@openssh.com,hmac-sha2-512 debug2: compression ctos: none,zlib@openssh.com debug2: compression stoc: none,zlib@openssh.com debug2: languages ctos: debug2: languages stoc: debug2: first_kex_follows 0 debug2: reserved 0 debug3: kex_choose_conf: will use strict KEX ordering debug1: kex: algorithm: curve25519-sha256 debug1: kex: host key algorithm: ssh-ed25519 debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none debug3: send packet: type 30 debug1: expecting SSH2_MSG_KEX_ECDH_REPLY debug3: receive packet: type 31 debug1: SSH2_MSG_KEX_ECDH_REPLY received debug1: Server host key: ssh-ed25519 SHA256:GJIRqOXW4PSzUjdpXGQogS4sfqBBGkMkmAprbdkiZik debug1: load_hostkeys: fopen /tmp/known_hosts: No such file or directory debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory Warning: Permanently added '10.0.0.2' (ED25519) to the list of known hosts. debug3: send packet: type 21 debug1: ssh_packet_send2_wrapped: resetting send seqnr 3 debug2: ssh_set_newkeys: mode 1 debug1: rekey out after 134217728 blocks debug1: SSH2_MSG_NEWKEYS sent debug1: expecting SSH2_MSG_NEWKEYS debug3: receive packet: type 21 debug1: ssh_packet_read_poll2: resetting read seqnr 3 debug1: SSH2_MSG_NEWKEYS received debug2: ssh_set_newkeys: mode 0 debug1: rekey in after 134217728 blocks debug3: send packet: type 5 debug3: receive packet: type 7 debug1: SSH2_MSG_EXT_INFO received debug3: kex_input_ext_info: extension server-sig-algs debug1: kex_ext_info_client_parse: server-sig-algs=<ssh-ed25519,sk-ssh-ed25519@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,sk-ecdsa-sha2-nistp256@openssh.com,webauthn-sk-ecdsa-sha2-nistp256@openssh.com,ssh-dss,ssh-rsa,rsa-sha2-256,rsa-sha2-512> debug3: kex_input_ext_info: extension publickey-hostbound@openssh.com debug1: kex_ext_info_check_ver: publickey-hostbound@openssh.com=(0) debug3: receive packet: type 6 debug2: service_accept: ssh-userauth debug1: SSH2_MSG_SERVICE_ACCEPT received debug3: send packet: type 50 debug3: receive packet: type 51 debug1: Authentications that can continue: publickey,gssapi-keyex,gssapi-with-mic,keyboard-interactive debug3: start over, passed a different list publickey,gssapi-keyex,gssapi-with-mic,keyboard-interactive debug3: preferred keyboard-interactive,password debug3: authmethod_lookup keyboard-interactive debug3: remaining preferred: password debug3: authmethod_is_enabled keyboard-interactive debug1: Next authentication method: keyboard-interactive debug2: userauth_kbdint debug3: send packet: type 50 debug2: we sent a keyboard-interactive packet, wait for reply debug3: receive packet: type 60 debug2: input_userauth_info_req: entering debug2: input_userauth_info_req: num_prompts 0 debug3: send packet: type 61 debug3: receive packet: type 52 Authenticated to 10.0.0.2 ([10.0.0.2]:22) using "keyboard-interactive". debug3: expanding LocalCommand: rm /tmp/known_hosts debug3: expanded LocalCommand: rm /tmp/known_hosts debug1: channel 0: new session [client-session] (inactive timeout: 0) debug3: ssh_session2_open: channel_new: 0 debug2: channel 0: send open debug3: send packet: type 90 debug1: Requesting no-more-sessions@openssh.com debug3: send packet: type 80 debug3: Executing /bin/sh -c "rm /tmp/known_hosts" debug1: Entering interactive session. debug1: pledge: exec debug3: client_repledge: enter debug3: receive packet: type 80 debug1: client_input_global_request: rtype hostkeys-00@openssh.com want_reply 0 debug3: receive packet: type 91 debug2: channel_input_open_confirmation: channel 0: callback start debug2: fd 3 setting TCP_NODELAY debug3: set_sock_tos: set socket 3 IP_TOS 0x48 debug2: client_session2_setup: id 0 debug2: channel 0: request pty-req confirm 1 debug3: send packet: type 98 debug2: channel 0: request shell confirm 1 debug3: send packet: type 98 debug3: client_repledge: enter debug1: pledge: fork debug2: channel_input_open_confirmation: channel 0: callback done debug2: channel 0: open confirm rwindow 0 rmax 32768 debug3: receive packet: type 99 debug2: channel_input_status_confirm: type 99 id 0 debug2: PTY allocation request accepted on channel 0 debug2: channel 0: rcvd adjust 2097152 debug3: receive packet: type 99 debug2: channel_input_status_confirm: type 99 id 0 debug2: shell request accepted on channel 0 Last login: Sat Feb 3 22:38:59 2024 from 10.0.0.1
Bonus: Telnet
Before SSH, Telnet was the most common way to connect over the network to a computer. On most modern Linux systems you can still install a Telnet server via a package named telnetd
or telnet-server
, and a Telnet client via a package named telnet
or telnet-client
.
On most distributions, the Telnet server runs as a systemd socket unit — so after installing the Telnet server, you can start and stop it via the telnet.socket
unit. We can verify it’s running by checking the output of the ss -ptunl
command (look for TCP port 23
):
$ sudo dnf install -qy telnet-server Installed: telnet-server-1:0.17-90.fc39.x86_64 $ sudo systemctl start telnet.socket $ sudo ss -ptunl Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=689,fd=17)) udp UNCONN 0 0 0.0.0.0:51822 0.0.0.0:* udp UNCONN 0 0 127.0.0.1:323 0.0.0.0:* users:(("chronyd",pid=735,fd=5)) tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=967,fd=3)) tcp LISTEN 0 4096 0.0.0.0:23 0.0.0.0:* users:(("systemd",pid=1,fd=54)) tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=689,fd=18))
On other distros, the Telnet server may run under the generic inetd daemon — so after installing the Telnet server, you can start and stop it by starting and stopping the inetd
service (and configure it via the /etc/inetd.conf
file):
$ sudo apt-get -qq install telnetd $ sudo systemctl start inetd $ sudo ss -ptunl Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=2906,fd=11)) udp UNCONN 0 0 0.0.0.0:51822 0.0.0.0:* tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=123375,fd=3)) tcp LISTEN 0 128 0.0.0.0:23 0.0.0.0:* users:(("inetd",pid=176367,fd=4)) tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=2906,fd=12))
To enable WireGuard public-key authentication with Telnet, start with the same WireGuard Configuration, Netgroup Configuration, and Firewall Configuration on Endpoint B as we set up above — but we also need a few more config tweaks, shown below.
PAM Configuration
For Telnet, we’ll configure PAM similarly to what we did for SSH, but we need to configure a different file (not the /etc/pam.d/sshd
file). On some distros this may be the /etc/pam.d/remote
file; if your distro doesn’t have this file, try the /etc/pam.d/login
file instead. Add the following to the top of it:
# /etc/pam.d/remote
auth sufficient pam_succeed_if.so user innetgr wg-users
...
This is the same line we added to the /etc/pam.d/sshd
file in the SSH PAM Configuration section above, and it will work the same way: It will automatically authenticate members of the wg-users
netgroup when they connect to Endpoint B through WireGuard.
Hosts Configuration
The one thing that will work differently with Telnet is that the Telnet server will automatically try to do a reverse hostname lookup on the source IP address used to connect. So when we connect via Telnet through WireGuard from Endpoint A (with an IP address of 10.0.0.1
), Endpoint B will try to do a reverse hostname lookup on 10.0.0.1
, and use the results of that lookup to determine to what netgroup the connecting user belongs.
To avoid trusting this lookup to DNS, we’ll configure the /etc/hosts
file on Endpoint B to provide a private, conceptual hostname for each WireGuard IP address in our WireGuard network. Edit the /etc/hosts
file on Endpoint B to add the following to the bottom of the file:
# /etc/hosts
...
10.0.0.1 alice-laptop.private
10.0.0.3 bob-workstation.private
The above config will ensure that when Endpoint B looks up the hostname for 10.0.0.1
(Endpoint A’s WireGuard IP address), it will come up with alice-laptop.private
; and if it looks up the hostname for 10.0.0.3
(a WireGuard IP address we might use in the future for Bob’s workstation), it will resolve to bob-workstation.private
.
Netgroup Configuration
With those virtual hostnames added to /etc/hosts
, go back and edit the /etc/netgroup
file on Endpoint B to use them. Add an (alice-laptop.private,alice,)
combination to the wg-alice
netgroup, and a (bob-workstation.private,bob,)
entry to the wg-bob
netgroup:
# /etc/netgroup
wg-alice (10.0.0.1,alice,) (alice-laptop.private,alice,)
wg-bob (10.0.0.3,bob,) (bob-workstation.private,bob,)
wg-users wg-alice wg-bob
Now if we run the getent
command, we’ll see the new hosts we’ve associated with Alice and Bob — the alice-laptop.private
host with the alice
user, and the bob-workstation.private
host with the bob
user:
$ getent netgroup wg-users wg-users (10.0.0.3,bob,) (bob-workstation.private,bob,) (10.0.0.1,alice,) (alice-laptop.private,alice,)
When Alice connects via Telnet as the alice
user from Endpoint A through her WireGuard connection, the Telnet server on Endpoint B will do a reverse lookup on her WireGuard IP address of 10.0.0.1
, and produce the hostname alice-laptop.private
. When PAM checks the wg-users
netgroup, it will find the (alice-laptop.private,alice,)
combination in it, and authenticate this connection.
Test Telnet
To test this out, run the telnet -l alice 10.0.0.2
command from Endpoint A:
$ telnet -l alice 10.0.0.2 Connected to 10.0.0.2 Entering character mode Escape character is '^]'. Last login: Sat Feb 3 23:44:19 from 10.0.0.1 alice@bee:~$
We should automatically be logged in as Alice on Endpoint B, without prompting for a password.
If this doesn’t work, check the system logs on Endpoint B for the login
identifier. This is what a successful login should look like:
$ journalctl -t login -e ... Feb 04 00:38:57 bee login[118032]: pam_succeed_if(remote:auth): requirement "user innetgr wg-users" was met by user "alice" Feb 04 00:38:57 bee login[118032]: pam_unix(remote:session): session opened for user alice(uid=1001) by alice(uid=0) Feb 04 00:38:57 bee login[118032]: LOGIN ON pts/3 BY alice FROM alice-laptop.private
Bonus: FTP
Just like we set up a Telnet server on Endpoint B as a replacement for SSH terminal access, we can also set up an FTP server as a replacement for SCP, allowing files to be copied back and forth between Endpoint A and B. There are several relatively-modern FTP servers we could install on Endpoint B; we’ll use ProFTPD (as it supports PAM, and includes the correct remote host information for its auth checks).
On most distributions it can be installed via a package named proftpd
(or proftpd-core
), and will run as its own proftpd
daemon:
$ sudo dnf install -qy proftpd Installed: libmemcached-awesome-1.1.4-2.fc39.x86_64 proftpd-1.3.8b-1.fc39.x86_64 $ sudo systemctl start proftpd $ sudo ss -ptunl Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=689,fd=17)) udp UNCONN 0 0 0.0.0.0:51822 0.0.0.0:* udp UNCONN 0 0 127.0.0.1:323 0.0.0.0:* users:(("chronyd",pid=735,fd=5)) tcp LISTEN 0 9 0.0.0.0:21 0.0.0.0:* users:(("proftpd",pid=176118,fd=0)) tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=967,fd=3)) tcp LISTEN 0 4096 0.0.0.0:23 0.0.0.0:* users:(("systemd",pid=1,fd=54)) tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=689,fd=18))
To enable WireGuard public-key authentication with ProFTPD, we’ll use the same WireGuard Configuration, Netgroup Configuration, and Firewall Configuration on Endpoint B as we set up for SSH. We (probably) will only need to modify the PAM Configuration for FTP, as shown below.
FTP Configuration
ProFTPD’s main config file usually can be found at /etc/proftpd.conf
or /etc/proftpd/proftpd.conf
. Out-of-the-box it should be configured to use PAM authentication; and to not do reverse hostname lookups. If that’s not the case, however, add the following settings to ProFTPD’s configuration, and restart (sudo systemctl restart proftpd
):
# /etc/proftpd.conf
...
AuthPAM on
AuthPAMConfig proftpd
AuthOrder mod_auth_pam.c* mod_auth_unix.c
UseReverseDNS off
PAM Configuration
For ProFTPD, we’ll also use a similar PAM Configuration as with SSH, but we again need to configure a different file — in this case, the /etc/pam.d/proftpd
file. Add the following to the top of that file:
# /etc/pam.d/proftpd
auth sufficient pam_succeed_if.so user innetgr wg-users
...
Test FTP
To test this out, run the ftp alice@10.0.0.2
command from Endpoint A:
$ ftp alice@10.0.0.2 Connected to 10.0.0.2. 220 FTP Server ready. 331 Password required for alice Password: 230 User alice logged in Remote system type is UNIX. Using binary mode to transfer files. ftp>
We should automatically be logged in as Alice on Endpoint B; if prompted for a password, simply enter a blank password.
To avoid password prompts with the standard command-line FTP client, add the following to Alice’s ~/.netrc
file on Endpoint A:
# ~/.netrc
machine 10.0.0.2 login alice password none
We should then be able to connect to Endpoint B without prompts, simply by running ftp 10.0.0.2
from Endpoint A:
$ ftp 10.0.0.2 Connected to 10.0.0.2. 220 FTP Server ready. 331 Password required for alice 230 User alice logged in Remote system type is UNIX. Using binary mode to transfer files. ftp>
If you encounter issues, check the ProFTPD logs on Endpoint B. This is what a successful login should look like:
$ journalctl -u proftpd -e ... Feb 04 01:50:20 bee proftpd[177215]: session[177215] 203.0.113.2 (10.0.0.1[10.0.0.1]): FTP session opened. Feb 04 01:50:20 bee proftpd[177215]: pam_succeed_if(proftpd:auth): requirement "user innetgr wg-users" was met by user "alice" Feb 04 01:50:20 bee proftpd[177215]: pam_unix(proftpd:session): session opened for user alice(uid=1001) by alice(uid=0) Feb 04 01:50:20 bee proftpd[177215]: session[177215] 203.0.113.2 (10.0.0.1[10.0.0.1]): USER alice: Login successful.
Bonus: RSH
Rlogin (and its companion programs like RSH, RCP, and Rexec) were more “modern” versions of Telnet and FTP that were used in the 80s and 90s, before SSH supplanted all of them. Today, most of these “R commands” aren’t well maintained (as they have fundamental security issues built into their design), and should be avoided in preference to SSH (or Telnet & FTP through a secure connection, like WireGuard).
But you can still install the server and client programs on many current Linux distributions — and use WireGuard to run them securely. On most distributions you can install a dedicated rsh-server
package that will include the Rexec, Rlogin, and RSH servers; on other distros they may be included in a larger inetutils
package that contains most of the GNU Inetutils. (We’ll ignore the Rexec server, as it supports only password authentication — plus it doesn’t do anything that RSH itself can’t.)
On most distributions, these servers run as a systemd socket unit — so after installing them, we can start and stop the Rlogin server via the rlogin.socket
unit, and the RSH server via the rsh.socket
unit. We can verify they’re running by checking the output of the ss -ptunl
command (look for TCP ports 513
and 514
):
$ sudo dnf install -qy rsh-server Installed: rsh-server-0.17-106.fc39.x86_64 $ sudo systemctl start rlogin.socket $ sudo systemctl start rsh.socket $ sudo ss -ptunl Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=689,fd=17)) udp UNCONN 0 0 0.0.0.0:51822 0.0.0.0:* udp UNCONN 0 0 127.0.0.1:323 0.0.0.0:* users:(("chronyd",pid=735,fd=5)) tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=967,fd=3)) tcp LISTEN 0 4096 0.0.0.0:513 0.0.0.0:* users:(("systemd",pid=1,fd=54)) tcp LISTEN 0 4096 0.0.0.0:514 0.0.0.0:* users:(("systemd",pid=1,fd=55)) tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=689,fd=18))
On other distros, these servers may run under the generic inetd daemon — so after installing the servers, we can start and stop them by starting and stopping the inetd
service. The Rexec server listens on TCP port 512
, the Rlogin server listens on TCP port 513
, and the RSH server listens on TCP port 514
:
$ sudo apt-get -qq install rsh-server $ sudo systemctl start inetd $ sudo ss -ptunl Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=2906,fd=11)) udp UNCONN 0 0 0.0.0.0:51822 0.0.0.0:* tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=123375,fd=3)) tcp LISTEN 0 128 0.0.0.0:514 0.0.0.0:* users:(("inetd",pid=176367,fd=8)) tcp LISTEN 0 128 0.0.0.0:513 0.0.0.0:* users:(("inetd",pid=176367,fd=9)) tcp LISTEN 0 128 0.0.0.0:512 0.0.0.0:* users:(("inetd",pid=176367,fd=10)) tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=2906,fd=12))
To enable WireGuard public-key authentication with Rlogin and RSH, we’ll use the same WireGuard Configuration and Firewall Configuration on Endpoint B as we set up for SSH — but instead of using netgroups, we’ll use Rhosts Configuration to associate WireGuard IP addresses with users.
Rhosts Configuration
Traditional Rlogin authentication relies on the ~/.rhosts
file in each individual user’s home folder (conceptually similar to the ~/.ssh/authorized_keys
file used by SSH public-key authentication). To allow Alice to log in automatically with Rlogin over WireGuard from Endpoint A (with a WireGuard IP address of 10.0.0.1
), add the following entry to Alice’s ~/.rhosts
file on Endpoint B:
# /home/alice/.rhosts
10.0.0.1 +
The first part of the entry (10.0.0.1
) is the remote host; the second part of the entry is the user account on that remote host (`). The `
symbol is a wildcard — we’re using a wildcard for the user account, since it has no effect on security (anyone using rlogin can claim to be using any remote user account). What matters in our scenario is the host: the IP address of 10.0.0.1
is not spoofable, due to the WireGuard Configuration and Firewall Configuration we’ve set up above.
Note
|
The Rlogin server will also attempt reverse hostname lookups, just like Telnet. However, unlike Telnet, the Rlogin authentication mechanism will check for matches with both the result of the hostname lookup, and the original source IP address. Therefore, you may wish to set up the same Hosts Configuration with Rlogin as we used for Telnet (which would, for example, produce
(Also, even if you don’t use resolved hostnames in your |
Tip
|
If setting up
And if using SELinux, make sure the file has the right SELinux context:
|
Note
|
Instead of setting up and using traditional Rlogin authentication as shown above, we could alternatively have set up the same Netgroup Configuration and Hosts Configuration as we used for Telnet; and then edit the PAM configuration for Rlogin ( |
Test Rlogin
The Rlogin, RSH, and other “R command” client programs are part of a few different packages on different distributions — try either the rsh-client
, rsh
, or inetutils
packages. Install the appropriate package with the client programs on both Endpoint A and Endpoint B.
Then run the rlogin -l alice 10.0.0.2
command from Endpoint A:
$ rlogin -l alice 10.0.0.2 Last login: Sun Feb 4 05:02:32 from 10.0.0.1 alice@bee:~$
We should automatically be logged into a terminal session on Endpoint B as Alice (similar to Telnet), without prompting for a password.
If this doesn’t work, check the system logs on Endpoint B for the rlogind
identifier. This is what a successful login should look like:
$ journalctl -t rlogind -e Feb 04 05:07:24 bee rlogind[340100]: pam_rhosts(rlogin:auth): allowed access to ally@10.0.0.1 as alice
Note
|
In the above Endpoint B log entry, the first user listed ( |
Test RSH
While the rlogin
client command provides an interactive terminal (like the telnet
client, or the ssh
client with no remote command), the rsh
client command simply runs a remote command and prints the results.
Try running the following command from Endpoint A (which runs the hostname
command remotely on Endpoint B):
$ rsh -l alice 10.0.0.2 hostname bee
If this doesn’t work, check the system logs on Endpoint B for the rshd
identifier. This is what a successful RSH command should look like:
$ journalctl -t rshd -e Feb 04 06:09:34 bee rshd[392565]: pam_rhosts(rsh:auth): allowed access to ally@10.0.0.1 as alice Feb 04 06:09:35 bee rshd[392565]: pam_unix(rsh:session): session opened for user alice(uid=1001) by alice(uid=0) Feb 04 06:09:35 bee rshd[392565]: pam_unix(rsh:session): session closed for user alice
Note
|
Running
|
Test RCP
The rcp
client command also uses the RSH service. We can test it out by creating a foo.txt
file remotely on Endpoint B, and then copying that file over locally to Endpoint A:
$ rsh -l alice 10.0.0.2 'echo foo > foo.txt' $ rsh -l alice 10.0.0.2 cat foo.txt foo $ rcp alice@10.0.0.2:foo.txt . $ cat foo.txt foo
Tip
|
If you see the following error after running the
|
Test Rsync
Rsync is maintained separately from the other “R commands” (and usually it comes in its own rsync
package); and modern versions of Rsync use SSH as its default transport — but originally, Rsync used RSH. We can still direct Rsync to use RSH, however, via the -e
flag (eg rsync -e rsh
).
We can test this out by creating a bar
directory containing some files remotely on Endpoint B, and then syncing that directory locally to Endpoint A:
$ rsh -l alice 10.0.0.2 mkdir bar $ rsh -l alice 10.0.0.2 'echo baz > bar/baz.txt' $ rsh -l alice 10.0.0.2 'echo qux > bar/qux.txt' $ rsync -e rsh -av alice@10.0.0.2:bar/ bar/ receiving incremental file list created directory bar ./ baz.txt qux.txt sent 65 bytes received 202 bytes 178.00 bytes/sec total size is 8 speedup is 0.03 $ ls bar baz.txt qux.txt