with apologies

ssh runes

· 4 min read · November 10, 2026 · #tech #linux #config #dotfiles

Having observed a colleague experiencing some similar pain accessing the Lab’s web storage I said I’d post my “simpler” way. So here it is. Though having now pulled it all into one place, I think I need to have a talk with myself about how I choose to define the word “simpler”…

Nevertheless, the overall flow here is something like:

If you don’t have a valid ticket then you may find you can login to the target host but you’ll be unable to access your home directory on the filer. The usual symptom of this is seeing something like the following when logging in after a delay of a few seconds:[1]

: mort@greyjay:~$; ssh slogin.cl.cam.ac.uk
Welcome to ...

...

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

Last login: Fri Oct 23 15:25:10 2026 from user-12fe-8c.svr-vpn-1.vpn.cl.cam.ac.uk
Could not chdir to home directory /home/rmm1002: Permission denied
-bash: /home/rmm1002/.bash_profile: Permission denied
rmm1002@svr-ssh-1:/$

When you have a valid Kerberos ticket (see below for details of the kinit shell function), then you will find your home directory is available so your dotfiles can be found:

: mort@greyjay:~$; kinit
which: no cl-krenew in (/home/mort/.local/bin:/run/wrappers/bin:/run/current-system/sw/bin:/home/mort/.nix-profile/bin:/etc/profiles/per-user/mort/bin:/nix/var/nix/profiles/default/bin:/usr/bin:/bin:)
kinit: Ticket expired while renewing credentials
Password for rmm1002@DC.CL.CAM.AC.UK:
: mort@greyjay:~$; ssh slogin.cl
Welcome to ...

...

Last login: Tue Nov 10 16:22:07 2026 from 2001:630:212:220:fb73:7a29:4235:8b8b
Linux svr-ssh-1.cl.cam.ac.uk 7.0.0-30-generic #30~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Aug  7 13:27:52 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
Tue 10 Nov 16:24:50 GMT 2026

: rmm1002@svr-ssh-1:~$;

You can only obtain a ticket while on the Lab network, hence the use of the VPN to do so. I leave it as an exercise for the reader to obtain a working VPN configuration—for NixOS, mine is here. Invoke to connect/disconnect via sudo ipsec [up|down] CUCL.

Connecting over SSH from wherever, whenever

My .ssh/config is setup so that when I try to connect to a target host ending .cl, e.g., the bounce server slogin.cl, it uses my CRSID rather than my local login, it presents the correct SSH key, and it invokes a proxy script to raise the VPN if needed:

Host *.cl
  User rmm1002
  HostName %h.cam.ac.uk
  IdentityFile ~/.ssh/cucl-key
  ProxyCommand ~/.ssh/ssh-cucl-proxy %h %p
  PubkeyAuthentication no
  GSSAPIAuthentication yes
  GSSAPIDelegateCredentials yes

The proxy script, ~/.ssh/ssh-cucl-proxy, takes two parameters: the target hostname (%h) and target port (%p). It also checks if debug output is required:

#!/usr/bin/env bash
#
# Copyright (C) 2017-2024 Richard Mortier <mort@cantab.net>
#
# Use bash `/dev/tcp` magic to setup a transparent tunnel.

if [[ $SSH_CUCL_PROXY_DEBUG == "true" ]]; then
  set -xv
  PS4='$LINENO: '
fi

It then attempts to discover whether or not I am already inside the Lab network:

cucl_local=false
while read IP; do
  echo "=== $IP"
  if [[ $IP =~ ^128.232. ]]; then
    cucl_local=true
  fi
done < <(ip -j -4 a | jq -r '.[].addr_info | .[].local')

if [[ $cucl_local == false ]]; then
  sudo ipsec up CUCL
fi

Finally, plumb the sockets so that input/output from the session are routed through the script, and then teardown the VPN once the session closes if it had been raised:

nc $1 $2

if [[ $cucl_local == false ]]; then
  sudo ipsec down CUCL
fi

I used to do the plumbing directly rather than have to remember to install netcat (nc) but it was unreliable for a time so I gave up and now just use nc, but for historical interest:

cat=/run/current-system/sw/bin/cat
exec 3<> /dev/tcp/$1/$2
$cat <&3 &
pid=$!
$cat >&3
kill $pid

One does not simply use a Kerberos ticket. One must first obtain a Kerberos ticket.

I have a shell function, defined here, that obtains the ticket by:

kinit() {
  kid=rmm1002@DC.CL.CAM.AC.UK
  cucl_local=false
  while read IP; do
    if [[ $IP =~ ^128.232. ]]; then
      cucl_local=true
    fi
  done < <(ip -j -4 a | jq -r '.[].addr_info | .[].local')

  if [[ $cucl_local != "true" ]]; then
    sudo ipsec up CUCL
  fi

  if which cl-krenew; then
    cl-krenew --ensuretgt --fresh --maxout
  else
    $(which kinit) -R $kid || $(which kinit) -f $kid
  fi

  if [[ $cucl_local != "true" ]]; then
    sudo ipsec down CUCL
  fi
}

Having now obtained a ticket, use it in the standard fashion:

ssh -k TARGET.cl

Putting it all together: Updating lecture pages

In the end then, the above gubbins was all in aid of the following: automating as far as possible getting myself connected so that I can push my course webpages live after writing them locally (often while offline). Being now a fan of the just tool over the more classical make, I have a Justfile that encodes the runes for, in that case my first year (Part IA) Operating Systems course.

First, some preamble: the standard “print all targets” as the default action, followed by setting up some variables so we hit the right place in the filesystem when pushing live, and the paramterized rsync command that we will use to sync local/remote over ssh using the ticket we obtained, after filtering out files that should not go live:

set default-list := true

YEAR := "2627"
COURSE := "OpSystems"

TARGET := "/anfs/www/html/teaching" / YEAR / COURSE / ""
CUCL := "slogin.cl"
ACCT := "rmm1002@DC.CL.CAM.AC.UK"
RSYNC := "rsync -v -Pavzt --rsh='ssh -K' --filter='.e .rsync-filter'"

Then the wrapper sync rule that

# build and sync everything
[group("remote")]
sync: ticket push rebuild pull

# obtain a kerberos ticket
[group("remote")]
ticket:
    source ~/rc-files/scripts/hosts.sh && kinit

# push local changes to the remote
[group("remote")]
push:
    {{RSYNC}} --exclude='/index*.html' --delete-excluded . {{CUCL}}:{{TARGET}}

# rebuild remote webpages
[group("remote")]
rebuild:
    ssh -K {{CUCL}} "cd {{TARGET}} && make"

# pull remote changes locally
[group("remote")]
pull:
    {{RSYNC}} --include='/index*.html' {{CUCL}}:{{TARGET}} .

Finally, for completeness, the Python one-liner I use to run things locally for quick testing, having first scraped enough of the static web resources that the pages render sensibly under the current Lab templates.

# run locally
serve:
    python3 -m http.server --cgi 8080
  1. Adjust for Message of the Day variation ↩