5. Remote access with SSH
Supported Versions: Ubuntu Server 24.04 LTS (primary), Rocky Linux 9 (alternate) Last Updated: September 15, 2026
Previous: DNS and connectivity | Course | Quiz | Next: Host security
SSH gives you an encrypted terminal on another computer. Encryption alone is insufficient: the client must identify the server, and the server must authenticate your account. We establish those two identities separately before changing login policy.
Prerequisites, outcomes, and recovery boundary
Complete lessons 1–4. Client 192.0.2.10/24 and server 192.0.2.20/24 use their MAC-verified internal-only NICs with no lab DHCP, gateway, or uplink. Separate NAT/DHCP management NICs remain unchanged. Use normal accounts and keep the server's hypervisor console open throughout. You need sudo on the server, its normal account password or console access to install a public key, and the existing management connection for package downloads.
You will install/check the SSH tools, verify a host fingerprint, create a passphrase-protected user key, install only its public part, prove a second independent key-only login, check effective server configuration, and undo your changes. All examples are illustrative, not live-tested VM transcripts. Every command below is for the specified guest, never the physical/documentation host. Stop at any unexpected error.
Take snapshots of both guests named before-ssh-lesson. Record existing SSH listeners and service/socket states before installation. A snapshot is the complete rollback for package/dependency changes; removing a package is not an exact inverse. Do not replace existing keys or configuration files to make a sample match.
1. Find the client and the server
The SSH client is the ssh command you run to connect. The SSH server is sshd, which waits for connections, normally TCP 22. A host key belongs to the server and proves its identity. A user key belongs to you and proves permission to use an account. A fingerprint is a short representation of a public key, suitable for comparison through a trusted channel.
Both guests, normal user; inspect first:
hostname
cat /etc/os-release
ip -br link
ip -4 -br address
command -v sshMatch NIC MACs with the hypervisor records and confirm the role addresses. On the client, ip -4 route get 192.0.2.20 should select the lab NIC and source .10; see lesson 2 for interpretation.
Ubuntu server, normal user; record state, then install only if missing:
dpkg-query -W openssh-server
systemctl status ssh.service ssh.socket --no-pagerIf absent, use sudo apt update followed by sudo apt install openssh-server. Ubuntu 24.04 installations can use ssh.socket socket activation; ssh.service is still the service name. Inspect both instead of disabling the socket. This lesson keeps the existing listen addresses/port, so it does not require changing socket bindings.
Rocky server, normal user; record state, then install only if missing:
rpm -q openssh-server
systemctl status sshd.service --no-pagerIf absent, use sudo dnf install openssh-server. On an Ubuntu client, the tools are supplied by openssh-client; on a Rocky client, by openssh-clients. If missing, install that named package using the distribution's package-manager procedure in lesson 1. Review transactions; do not add repositories for OpenSSH.
On the server, set one service variable after checking the distribution:
# Ubuntu server only:
SSH_UNIT=ssh.service# Rocky server only:
SSH_UNIT=sshd.serviceServer, normal user invoking sudo where shown:
sudo /usr/sbin/sshd -t
sudo systemctl start "$SSH_UNIT"
systemctl cat "$SSH_UNIT"
systemctl status "$SSH_UNIT" --no-pager
sudo ss -ltnp 'sport = :22'sshd -t checks configuration and key sanity; success normally prints nothing. Do not start/reload after a validation error. start affects current runtime, not boot enablement. systemctl cat reveals the unit and overrides: this procedure assumes the normal configuration path. If custom ExecStart/environment options supply -f or -o, mirror those options in later validation/effective checks rather than testing a different configuration. A listener on 0.0.0.0:22/[::]:22 covers more than the lab address; it is not source restriction. Do not change network bindings here, because that could remove management access. Lesson 6 restricts lab traffic by source and interface.
If a firewall currently blocks the lab connection, complete only the matching firewall branch in lesson 6 to allow .10 to .20 TCP 22, then return. Keep console access; do not turn off the firewall. “Connection refused” often means no listener/rejection; a timeout can indicate filtering or a path failure. Follow lesson 4's evidence sequence.
2. Verify the server's host key through its console
Server hypervisor console, normal user invoking sudo:
sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pubRecord the full SHA256:... fingerprint and that it is the ED25519 host key. This path is the public host key, not your future user key. Never print or copy /etc/ssh/ssh_host_ed25519_key, its private counterpart. This path assumes the normal non-FIPS guest baseline; a crypto policy that disallows Ed25519 needs an approved algorithm path, not a disabled security policy.
Client, normal user:
SSH_LAB_DIR=$(mktemp -d "$HOME/network-beginner-ssh.XXXXXX")
printf '%s\n' "$SSH_LAB_DIR"
read -r -p 'Normal account name on the server: ' SERVER_USERRecord the directory and account; stop if creation fails, the path is empty, or the account is root. This private directory holds a dedicated course key and host record, leaving existing ~/.ssh client files alone.
Client, normal user:
ssh -o HostKeyAlgorithms=ssh-ed25519 \
-o UserKnownHostsFile="$SSH_LAB_DIR/known_hosts" \
-o StrictHostKeyChecking=ask \
"$SERVER_USER@192.0.2.20"At the first host-key prompt, compare the entire fingerprint with the server console. Only an exact match justifies typing yes. If it differs, stop and check the VM/address; do not disable host-key checking or blindly delete a saved record. ssh-keyscan can collect a key but cannot by itself prove who supplied it.
If password login is allowed, enter the server account password and check hostname, whoami, and printf '%s\n' "$SSH_CONNECTION" remotely, then exit to the client. SSH_CONNECTION contains client IP/port and server IP/port; expect lab .10 and .20. Keep the independent hypervisor console available. If password login is already disabled, host-key verification can still finish before authentication fails; use the console installation alternative below without enabling passwords.
3. Create your user key and prepare server permissions
Client, normal user, same shell:
ssh-keygen -t ed25519 -a 64 -f "$SSH_LAB_DIR/id_ed25519" \
-C "network-beginner-$(basename "$SSH_LAB_DIR")"
ssh-keygen -lf "$SSH_LAB_DIR/id_ed25519.pub"
ls -l "$SSH_LAB_DIR"Enter a nonempty passphrase twice. It encrypts the private key on disk; it is not the server account password. -a 64 selects the key derivation work factor. id_ed25519 stays on the client; only id_ed25519.pub goes to the server. The unique directory prevents accidental replacement of an existing key. Stop rather than answering yes to an unexpected overwrite prompt.
Before installation, prepare a recovery record for the same normal server account used in SERVER_USER.
Server console, logged in as that normal account:
SERVER_NOTE=$(mktemp -d "$HOME/network-beginner-ssh-state.XXXXXX")
printf '%s\n' "$SERVER_NOTE"
stat -c '%a %U %n' "$HOME"
ls -ld "$HOME/.ssh" "$HOME/.ssh/authorized_keys"Stop if directory creation failed or the printed path is empty. Record the new path and original home mode. “No such file” for .ssh or authorized_keys is normal on a fresh account; record which was absent. If they exist, record their numeric modes with stat -c '%a %U %n' and inspect ownership. If either is a symlink, has another owner, has unexpected ACLs, or is managed by another system, stop and reconcile ownership instead of changing it recursively.
If authorized_keys already exists, server normal user: cp -p -- "$HOME/.ssh/authorized_keys" "$SERVER_NOTE/authorized_keys.before" makes a backup in the new directory. Also record .ssh's mode before changing it. Create .ssh only if absent with mkdir -m 700 -- "$HOME/.ssh". For this account, use chmod go-w "$HOME" and chmod 700 "$HOME/.ssh"; record original modes so they can be restored. OpenSSH's strict checks reject unsafe ownership or group/world-writable paths. Do not use recursive chmod or move someone else's keys.
4. Install the public key and prove key-only access
Client, normal user:
ssh-copy-id -i "$SSH_LAB_DIR/id_ed25519.pub" \
-o UserKnownHostsFile="$SSH_LAB_DIR/known_hosts" \
-o StrictHostKeyChecking=yes \
"$SERVER_USER@192.0.2.20"ssh-copy-id uses an existing authentication method to append the selected public key to the server account's ~/.ssh/authorized_keys; it does not copy your private key. Read the added-key count. It should add your one key or report it already installed. Do not use a forced mode to add duplicates.
Console alternative when password/bootstrap SSH is unavailable: on the client, display only cat "$SSH_LAB_DIR/id_ed25519.pub". On the server console as the target user, open nano "$HOME/.ssh/authorized_keys" and append that entire single public-key line, preserving existing lines. Save, then compare ssh-keygen -lf "$HOME/.ssh/authorized_keys" with the client user-key fingerprint. This is not the host-key fingerprint from step 2.
Server console, normal target user:
chmod 600 "$HOME/.ssh/authorized_keys"
ls -ld "$HOME" "$HOME/.ssh"
ls -l "$HOME/.ssh/authorized_keys"
ssh-keygen -lf "$HOME/.ssh/authorized_keys"Expect the target account as owner, private .ssh permissions, and the new public-key fingerprint among any existing entries. On Rocky with SELinux enforcing, also use sudo restorecon -v "$HOME/.ssh" "$HOME/.ssh/authorized_keys" when standard home-path labels are wrong; lesson 6 explains why Unix permissions alone are not the whole policy.
Client console A, normal user; open and keep this session:
ssh -i "$SSH_LAB_DIR/id_ed25519" -o IdentitiesOnly=yes \
-o PreferredAuthentications=publickey \
-o PasswordAuthentication=no -o KbdInteractiveAuthentication=no \
-o ControlMaster=no -o ControlPath=none \
-o UserKnownHostsFile="$SSH_LAB_DIR/known_hosts" \
-o StrictHostKeyChecking=yes "$SERVER_USER@192.0.2.20"The private-key passphrase prompt is expected. A prompt for the remote account password is not part of this key-only test. IdentitiesOnly limits keys offered, and disabling connection sharing ensures you test a fresh authentication rather than reuse a multiplexed session.
In that remote server session, run whoami and printf '%s\n' "$SSH_CONNECTION" and leave it open. Now open client console B, set SSH_LAB_DIR to the recorded exact client directory and SERVER_USER to the same account, and repeat the full key-only command above. Confirm account and .10→.20 connection again. Only a successful second independent login completes this gate.
If you already use SSH over the management NIC, repeat the key-only test for every management account/path that must remain usable before disabling server-wide password authentication. An existing session alone is insufficient. If you cannot verify a required path, keep the present authentication policy and finish preparation; the NIC's unchanged IP does not guarantee unchanged login access.
5. Inspect configuration precedence before hardening
Server console, normal user invoking sudo:
sudo cat /etc/ssh/sshd_config
sudo ls -la /etc/ssh/sshd_config.d
sudo grep -RnsE '^[[:space:]]*(Include|Match|PasswordAuthentication|KbdInteractiveAuthentication|PermitRootLogin|PubkeyAuthentication|AuthenticationMethods|AuthorizedKeysFile)' \
/etc/ssh/sshd_config /etc/ssh/sshd_config.dRead the actual included files, including other paths named by Include; grep is an aid, not a complete parser. OpenSSH normally uses the first obtained value for a keyword. Includes are processed where they occur and globbed filenames in lexical order. A matching Match block can override global values for that connection; among applicable Match assignments, ordering matters again. A late 99-...conf file is not a reliable override.
This exercise uses a new, global-only drop-in, requiring an active /etc/ssh/sshd_config.d/*.conf Include in the global part of the main file. Check that no earlier assignment defeats it. Do not put Match blocks in this course drop-in or edit unrelated include files to force precedence. If the installed layout cannot support the new file as intended, stop the hardening step and investigate the owner/layout while retaining working access.
Server console; set the target account again in this shell:
read -r -p 'Normal SSH account on this server: ' SERVER_USER
SSH_CASE="user=$SERVER_USER,addr=192.0.2.10,host=net-client,laddr=192.0.2.20,lport=22"
sudo /usr/sbin/sshd -T -C "$SSH_CASE"-T prints effective configuration; -C supplies a hypothetical connection so Match rules are evaluated. host describes the connecting host for hostname matches, not the server hostname. This course does not depend on hostname-based Match rules; if your configuration does, use the actual name sshd evaluates. Check pubkeyauthentication, passwordauthentication, kbdinteractiveauthentication, permitrootlogin, authenticationmethods, and authorizedkeysfile. A valid configuration can still reject your account because of AllowUsers, DenyUsers, group restrictions, PAM, or account state.
Save the effective baseline before adding anything:
sudo /usr/sbin/sshd -T -C "$SSH_CASE" > "$SERVER_NOTE/effective.before"
sudo /usr/sbin/sshd -T -C \
'user=root,addr=192.0.2.10,host=net-client,laddr=192.0.2.20,lport=22' \
> "$SERVER_NOTE/root-effective.before"Record equivalent baselines for any existing management SSH paths, using their real source/local addresses and accounts. No example address should replace an actual management endpoint.
6. Add one owned policy file, validate, reload, and retest
Proceed only after the second key-only login and required management-path tests succeeded. These settings change server-wide authentication, subject to effective Match rules; they do not change NIC configuration.
Server console, normal user invoking sudo; exclusive creation:
sudo sh -c 'umask 077; set -C; : > /etc/ssh/sshd_config.d/00-network-beginner.conf'
sudoedit /etc/ssh/sshd_config.d/00-network-beginner.confset -C refuses to overwrite an existing file. If it fails, stop: do not edit or delete an earlier attempt without its recovery record. Put only this configuration text into your newly created file:
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin noPasswordAuthentication no alone does not disable password-like keyboard-interactive/PAM authentication. Keeping normal-user key login plus sudo avoids direct root SSH login. Do not disable PAM globally.
Server console, normal user invoking sudo:
sudo /usr/sbin/sshd -t
sudo /usr/sbin/sshd -T -C "$SSH_CASE"
sudo /usr/sbin/sshd -T -C \
'user=root,addr=192.0.2.10,host=net-client,laddr=192.0.2.20,lport=22'Continue only if syntax passes and the intended connections show pubkeyauthentication yes, both password/keyboard-interactive no, and permitrootlogin no. Repeat effective checks for required management paths. If an earlier setting or Match wins, do not reload: remove only your new file, validate the restored configuration, and investigate precedence. Renaming files randomly is not a fix.
Server console, normal user invoking sudo:
sudo systemctl reload "$SSH_UNIT"
systemctl status "$SSH_UNIT" --no-pager
sudo journalctl -u "$SSH_UNIT" -b -n 30 --no-pagerReload asks the running daemon to reread its configuration. It is not a firewall reload or a socket-binding change. Keep the console and original sessions open, then create another fresh client key-only login using step 4 and recheck management SSH paths. The continued existence of an old authenticated session does not prove new logins work.
Completion and exact recovery
Success means verified host fingerprint, dedicated passphrase-protected user key, correct ownership/permissions, a second fresh login before hardening, expected -T -C results for relevant accounts/paths, and a fresh successful login after reload. Explain why authentication failure differs from a refused TCP connection.
Practice policy rollback from the server console:
sudo rm -i -- /etc/ssh/sshd_config.d/00-network-beginner.conf
sudo /usr/sbin/sshd -t
sudo /usr/sbin/sshd -T -C "$SSH_CASE" > "$SERVER_NOTE/effective.restored"
diff -u "$SERVER_NOTE/effective.before" "$SERVER_NOTE/effective.restored"
sudo /usr/sbin/sshd -T -C \
'user=root,addr=192.0.2.10,host=net-client,laddr=192.0.2.20,lport=22' \
> "$SERVER_NOTE/root-effective.restored"
diff -u "$SERVER_NOTE/root-effective.before" "$SERVER_NOTE/root-effective.restored"Confirm deletion only of the file you created. No diff output means the normal-account effective baseline matches; check the root and recorded management cases too. If other administrators/configuration managers changed state meanwhile, investigate the difference rather than restoring an entire old directory. Reload $SSH_UNIT only after syntax and intended restored values pass, then verify a new login. If new logins failed after hardening, this same console procedure is the recovery path.
You may reapply the owned drop-in with the same validation gates if retaining the hardened course setup. Otherwise, finish cleanup in this order:
- Server target account: open
~/.ssh/authorized_keysin nano and remove only the single key line identified by the course comment and recorded user-key fingerprint. Preserve all unrelated keys. Compare with"$SERVER_NOTE/authorized_keys.before"if it existed. Restore its recorded numeric mode. If it was originally absent and now empty, remove only this empty file; remove.sshwithrmdironly if you created it and it is empty. Restore recorded home/.sshmodes where those paths originally existed. On a shared account, reconcile concurrent changes instead of overwriting them with the backup. - Client: after closing course sessions and revoking the public key, remove only
id_ed25519,id_ed25519.pub, andknown_hostsinside the recordedSSH_LAB_DIRusingrm -i, thenrmdir "$SSH_LAB_DIR". If an extra file exists, inspect its exact name; do not use recursive deletion. Existing client SSH files were never replaced. - Server: remove the exact lesson-created baseline/backup files from
SERVER_NOTEonce verified, then remove that empty directory. Record any management baseline filenames you created so cleanup stays explicit. - Restore the recorded service/socket runtime state only if you changed it and no later lesson needs SSH. If Ubuntu socket activation was originally active, preserve that socket; do not disable it merely because the service was initially inactive. If you newly installed packages, keeping them for later lessons is reasonable; complete package rollback uses the snapshots, which also discard later guest work.
Primary references
Checked September 15, 2026:
- Ubuntu Server OpenSSH — installation,
ssh.service, and include precedence. - Ubuntu 24.04 release notes — OpenSSH socket activation and the socket configuration generator.
- Rocky Linux OpenSSH key setup — Rocky's OpenSSH workflow.
- OpenSSH
ssh,ssh-keygen,sshd, andsshd_config— identity, keys, validation, Include/Match, and effective settings. - Ubuntu 24.04
ssh-copy-id— selected-key installation and existing-key handling.