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:
- obtain a Kerberos ticket, connecting to and disconnecting from the VPN if needed,
- then connect to the target host, presenting the Kerberos ticket.
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 yesThe 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: '
fiIt then attempts to discover whether or not I am already inside the Lab network:
- Process each IPv4 address that I have (
ip -j -4 a | ...) [lines 2—7] - If my IP begins
128.232.then I’m in the Lab address space, (ie., “CUCL local”) [lines 4—6] - If I’m not inside the Lab network, raise the VPN [lines 9—11]
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
fiFinally, 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
fiI 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 $pidOne 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:
- Raising the VPN if needed [lines 2—8]
- Renewing an existing or obtaining a new Kerberos ticket by either
- Running the Lab’s
cl-krenewscript if available (typically only if on a Lab managed machine) [lines 14—16] - Running the standard
kinittool [lines 16—18]
- Running the Lab’s
- If needed, tearing down the VPN when done [lines 20—22]
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.clPutting 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
- ensures we have a valid ticket,
- pushes source changes live,
- rebuilds the pages themselves using the Lab
ucampastool to give them the correct formatting (invoked as the default target ofmake), - and pulls the updated pages back locally overriding the exclude directive in the
.rsync-filter.
# 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 8080Adjust for Message of the Day variation ↩