Home desktop as an ssh server within same network

Setting up a desktop computer as an ssh server for remote access in an intranet.

If you spot mistakes in this blog post, please let me know.


I currently have access to a desktop computer and a laptop computer. Sometimes I need the files in one of these computers available in the other computer.

Then I found that one of my friends is using ssh to allow remote access to their desktop.

We are in our college's network. So that meant less effort to set up ssh for remote access.

Note: Whatever is mentioned here works only if both server and clients are on the same network.

Software installation

For ssh, the server and client software components components need to be installed.

In debian-based Linux distros, the package names are:

Wasn't sure if the server needed the client software as well, but I installed both components in both client and server.

(We can ssh into the server from the server itself, if the server has both server and client ssh software components installed.)

Find ip address of host

Now that we have the software part ready and the ssh daemon is running in the server, we can connect to the server. For that we need to know its IP address (this seems to work merely because both the server and the clients are within the same network).

For this, we could use ip

$ # Show protocol (IP or IPv6) addresses on a device
$ ip address
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp8s0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN group default qlen 1000
    link/ether 1c:69:7a:e3:5e:f0 brd ff:ff:ff:ff:ff:ff
3: wlp7s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 14:18:c3:02:be:08 brd ff:ff:ff:ff:ff:ff
    inet 18.52.3.125/21 brd 18.52.3.255 scope global dynamic noprefixroute wlp7s0
       valid_lft 78811sec preferred_lft 78811sec
    inet6 ee80::7bff:ad1:b469:815c/64 scope link noprefixroute
       valid_lft forever preferred_lft forever

(ip a seems to do the same thing as ip address.)

In my case, I got

See this for the naming scheme.

lo denotes loopback interface (localhost. Anything the computer sends over this connection is being sent to itself). Its IP address is usually 127.0.0.1 (ipv4) or ::1 (ipv6). localhost is its domain name.

I need to use the wireless connection. So the address that I need is the one corresponding to wlp7s0, which is 18.52.3.125.

We could also have used ifconfig command:

$ ifconfig
enp8s0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        ether 1c:69:7a:e3:5e:f0  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 1068  bytes 64584 (63.0 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1068  bytes 64584 (63.0 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

wlp7s0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 18.52.3.125  netmask 255.255.248.0  broadcast 18.52.3.255
        inet6 ee80::7bff:ad1:b469:815c  prefixlen 64  scopeid 0x20<link>
        ether 14:18:c3:02:be:08  txqueuelen 1000  (Ethernet)
        RX packets 11771740  bytes 2112625907 (1.9 GiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1043530  bytes 165447258 (157.7 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

(Looks like my wifi connection had been quite active…)

Although people well-versed in Linux seem to prefer ip over ifconfig. See this.

CIDR notation

In the output of ifconfig and ip, we can see ipv4 addresses followed by a slash and a number.

These are IP addresses written in the CIDR (Classless Inter Domain Routing) notation.

(For a small history behind the 'class' in the name of CIDR, see the addendum at the end of this blog post.)

The number after the slash indicates how many bits (known as prefix) of the ip address are used to address the network. The remaining bits are used to address the node inside that network.

For example, consider the ipv4 address 192.4.16.0/20.

11000000.00000100.0001 0000.00000000 
|                    | |           |
+--------------------+ +-----------+
     network part        host part
      (20 bits)          (12 bits)

So, the network denoted by 192.4.16.0/20 can have at most 2¹² hosts (as exactly 12 bits are available for addressing the hosts).

ie, range of addresses available for hosts of this network is: 192.4.16.0 to 192.4.255.255

A few more examples: ¹³.

Network Host addresses Subnet mask
10.39.25.151/24 10.39.25.0 to 10.39.25.255 255.255.255.0
192.0.2.0/29 192.0.2.0  to 192.0.2.7 255.255.255.248

CIDR notation can be used for ipv6 addresses as well. As in 2001:db8::/48 denoting addresses from 2001:db8:0:0:0:0:0:0 to 2001:db8:ffff:ffff:ffff:ffff:ffff:ffff. ¹⁴

2001:0db8:0000:0000:0000:0000:0000:0000
|                           | |       |
+---------------------------+ +-------+
        network part          host part
         (48 bits)            (16 bits)

(IPv6 addresses are 16 bytes, while IPv4 addresses are 4 bytes.)

Making ssh key pair

I prefer to use ssh keys instead of passwords to access remote machines (you should too!). For that I first need to make a key pair.

There are lots of guides to make ssh keys these days (like this, this and this), but I included the command and its output here nevertheless.

$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/myusername/.ssh/id_rsa): /home/myusername/.ssh/id_rsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/myusername/.ssh/id_rsa
Your public key has been saved in /home/myusername/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:5hrSi4l1XdkYZZkGBGn87nuG3SIxVjBiLxETRWh05kg myusername@host
The key's randomart image is:
+---[RSA 3072]----+
|        .+*B*=   |
|        ..*+*    |
|       o.=.= .   |
|   .  Fo* +=.    |
|     . =So+=..   |
|  =   .o..+ o .  |
|    ..o .+ o .   |
|   o .+   . E    |
|  . o.o. +oo     |
+----[SHA256]-----+

Add an ssh key to server

Right, now have got an ssh key pair with which we use to get authenticated to our remote server. But before using the key, we need to let the server know that this is the key that we are gonna use by 'installing' the key in the server.

We can use ssh-copy-id command for this.

I had got my ssh public key (id_rsa.pub) in my server and sort of installed the to the server itself by naming localhost as the 'target'.

$ ssh-copy-id -i ~/.ssh/id_rsa.pub localhost
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/myusername/.ssh/id_rsa.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
myusername@localhost's password:

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh 'localhost'"
and check to make sure that only the key(s) you wanted were added.

As advised by the ssh-copy-id's output, let's try if we can log in using this newly added key.

We use the private key (in my case, id_rsa and not id_rsa.pub) while logging into the remote server.

$ ssh -i ~/.ssh/id_rsa localhost
Enter passphrase for key '/home/myusername/.ssh/id_rsa':
Linux myhostname 5.10.0-10-amd64 #1 SMP Debian 5.10.84-1 (2021-12-08) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
$

Cool. It worked!

Changing ssh configuration

Now I make some changes to the way ssh is configured in the server.

(I referred to this nice blog post by Zach Duey for this part.)

We make changes to the configuration by editing the /etc/ssh/sshd_config file.

Note: Each time after editing this file, we got to 'reload' the ssh daemon.

For setups using systemd as init system, we can restart sshd with

sudo systemctl reload sshd

and the changes that we made should take effect.

Default port used by ssh is port 22. I didn't change that.

No password authentication

I am using ssh keys to log in as it is relative more secure, and don't want to allow logging in passwords. So I disable the authentication based on passwords with:

PasswordAuthentication no

Allow log in only as specific users

I may be having many user accounts in my server computer, but I don't want ssh login for most of them. So I restrict ssh login to some specific users.

# can log-in to server via ssh only as 'myusername'
AllowUsers myusername

Disallow root log in

I don't want to allow login as root via ssh. So I did:

PermitRootLogin no

(This may be redundant as I already used AllowUser.)

Limit concurrent connections

I guess it's a good idea to put a limit on the maximum number of concurrent unauthenticated connections to server:

# Set maximum number of concurrent connections to 3
MaxStartups 3

GUI

ssh has the option of sort of 'forwarding' the GUI of an application running on server to the client.

I didn't want to use GUI via ssh. So I'm disabled that option with:

X11Forwarding no

TCP forwarding

It's possible to forward traffic incident on server ports to ports in client.

I disabled this forwarding traffic.

AllowTcpForwarding no

Managing

Now that we got an ssh service to which we can log into, we could use ways to work with it.

For example, to see if the ssh service is active, we could do (if the init system is systemd):

$ systemctl status ssh
● ssh.service - OpenBSD Secure Shell server
     Loaded: loaded (/lib/systemd/system/ssh.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2022-04-04 12:02:26 UTC; 5h 19min ago
       Docs: man:sshd(8)
             man:sshd_config(5)
   Main PID: 109728 (sshd)
      Tasks: 1 (limit: 37689)
     Memory: 1.1M
        CPU: 422ms
     CGroup: /system.slice/ssh.service
             └─109728 sshd: /usr/sbin/sshd -D [listener] 0 of 2-2 startups

Log in history

I found ssh connect/disconnect history at /var/log/auth.log.

It had stuff like:

Apr  4 12:44:02 myhostname sshd[109728]: Server listening on 0.0.0.0 port 22.
Apr  4 12:44:02 myhostname sshd[109728]: Server listening on :: port 22.
Apr  4 12:48:50 myhostname sshd[111826]: Accepted publickey for myusername from 18.52.3.125 port 51760 ssh2: RSA SHA256:5Ben24j23SnbsjcjrMqfsa4T3FVkIwyvnqjNFNsnnxt
Apr  4 12:48:50 myhostname sshd[111826]: pam_unix(sshd:session): session opened for user myusername(uid=1000) by (uid=0)
Apr  4 12:48:55 myhostname sshd[111848]: Received disconnect from 18.52.3.125 port 51760:11: disconnected by user
Apr  4 12:48:55 myhostname sshd[111848]: Disconnected from user myusername 18.52.3.125 port 51760
Apr  4 12:48:55 myhostname sshd[111826]: pam_unix(sshd:session): session closed for user myusername

We can also see failed login attempts here:

Apr  4 12:37:43 myhostname sshd[111575]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=::1  user=myusername
Apr  4 12:37:46 myhostname sshd[111575]: Failed password for myusername from ::1 port 39414 ssh2

Tools like grep and awk could come handy in finding the things you need here.

Prevent process termination on disconnection

When the client disconnects from the server over an ssh connection, every process that the client had started at the host will be terminated.

That's a bit inconvenient if we have long-running jobs as we won't be able to disconnect till the end of the job execution, which might take days.

To get around this problem, we could use screen or tmux.

After logging into the server start screen/tmux and then start the process within it.

Now you can disconnect from the server and the process will keep executing as if the server didn't realize that we had left.

We can later come back and check on the job execution without breaking a sweat.

Found a few other ways as well here. I haven't (yet) tried any of these, though.

I had only known of tmux/screen. Had forgotten about nohup, didn't even know about disown.

There are fancier tools like byobu (which can work on top of screen or tmux) as well.

See this for a related discussion.

File transfer between remote and clients

Could use one of rsync and scp. I got the idea that rsync is usually better.

With rsync, we can copy from client to server with:

rsync -rve "ssh -i /path/to/private-key" /path/to/client-file remoteuser@remoteip:/path/to/remote-file

Similarly, copying from server to client can be accomplished with:

rsync -rve "ssh -i /path/to/private-key" remoteuser@remoteip:/path/to/remote-file /path/to/client-file

rsync needs to be installed on both client and server. This is true in the case of scp as well.

This is the error that I got when I tried using rsync when only client had it:

bash: line 1: rsync: command not found
rsync: connection unexpectedly closed (0 bytes received so far) [Receiver]
rsync error: remote command not found (code 127) at io.c(235) [Receiver=3.1.2]

Not exactly informative, but easy enough to figure out with an internet search, I guess.

Remove an ssh key from server

Adding an ssh key to a server was as easy as using ssh-copy-id, so I expected there would be an similar way to remove a previously added key from the server as well.

Well apparently, there's no such way.

One way that I found here is:

In my case, the key was in a file at ~/.ssh/authorized_keys.

This is essentially reversing what ssh-copy-id did.

From here:

ssh-copy-id uses the SSH protocol to connect to the target host and upload the SSH user key. The command edits the authorized_keys file on the server. The .ssh directory is created if it doesn't exist. It also creates the authorized_keys file if it doesn't exist.

References

(The ip addresses mentioned in output of some commands mentioned in this blog past are not real.)

Addendum: 'Class' in CIDR

The 'class' in the name 'Classless Inter-Domain Routing' has got to do with an addressing scheme that was in use before it.

Before CIDR, there were classful networks, where IP addresses were grouped into different classes.

In classful networks, IP addresses (32 bits) were grouped into 5 classes:

 1b     7b        24b
+---+-----------+------+
| 0 |  Network  | Host |     Class A
+---+-----------+------+



  2b      14b       16b
+----+-----------+------+
| 10 |  Network  | Host |    Class B
+----+-----------+------+



  3b       21b       8b
+-----+-----------+------+
| 110 |  Network  | Host |   Class C
+-----+-----------+------+
Class Networks# Hosts#
A 2⁷ 2²⁴
B 2¹⁴ 2¹⁶
C 2²¹ 2⁸

Maximum number of hosts is actually two less than what's shown in the above table as the address with all bits in the host part set is the broadcast address and that with all host bits set to zero is the address of the network itself. ²² (I guess that's why localhost is 127.0.0.1 and not 127.0.0.0).

But soon it became evident that classful addresses don't scale that well.

From Computer Networks: A systems approach:

The original idea was that the Internet would consist of a small number of wide area networks (these would be class A networks), a modest number of site-(campus-)sized networks (these would be class B networks), anda large number of LANs (these would be class C networks). However, it turned out not to be flexible enough

One problem was that classful addresses could lead to unused addresses.

For example, if a network needed 2⁸+1 host addresses, the addresses would be of class B. That means that 2¹⁶-(2⁸+1) = 65279 host addresses would remain unused. This would be even more if the number of host addresses required is at boundary of the number of hosts supported by class A and class B. That could mean huge wastage of address space. (Also see Wikipedia: IPv4 address exhaustion. Hadn't realized that we had already run out of IPv4 addresses…)

In contrast to this scenario, with CIDR, the number of bits to be used to address networks is totally flexible. CIDR IP addresses sort of comes with a subnet mask (represented as the slash prefix like /24) which can be used to distinguish between the network and host part.

For example, if an address of a network is 192.45.23.5/24, the subnet mask is 255.255.255.0 as the /24 means that the first 24 bits are used to identify the sub-network and the remaining bits are for addressing the hosts.

If an ip address of a host of this network is 192.45.23.64,

network part of address = address AND subnet-mask

    11000000.00101101.00010111.01000000 = 192.45.23.64
 &  11111111.11111111.11111111.00000000 = 255.255.255.0
------------------------------------------------------------
    11000000.00101101.00010111.00000000 = 192.45.23.0           

host part of address = address AND (one's complement of subnet-mask)

    11000000.00101101.00010111.01000000 = 192.45.23.64
 &  00000000.00000000.00000000.11111111 = one's complement(255.255.255.0)
------------------------------------------------------------
    00000000.00000000.00000000.01000000 = 0.0.0.64           

In the case of classful networks, the mask was implied the network class itself. CIDR just makes it explicit.

CIDR equivalent for the classes of addresses of classful networks would be:

Class CIDR
A /8
B /16
C /24

Update (13-Aug-2022): Explicitly mention that init system should be systemd to use the systemctl command.