Skip to content

Arch steps checked 2026-08-08

VPN from off campus

Gets you onto the campus network from anywhere, using the same SSO login as everything else. Needed for network shares, some library resources, and anything that checks whether you are on campus.

Official documentation: THI VPN service. The official client is FortiClient; this page uses openfortivpn, which is open source, has no GUI and does not need to be installed with administrator rights beyond the tunnel itself.

The certificate problem, once

vpn.thi.de presents its own certificate but does not send the intermediate certificate that links it to a trusted root. A correctly configured client therefore cannot build a complete chain and refuses to connect, which looks like a client bug and is not one.

The fix is to give openfortivpn its own certificate bundle: the missing intermediate, followed by the root certificates the system already ships. --ca-file makes openfortivpn read that file instead of the system trust store, so one client learns about one gateway and nothing else on the machine changes what it trusts.

The intermediate on its own is not enough. It is not self-signed, so OpenSSL will not accept it as an anchor and stops at "unable to get issuer certificate". The roots belong in the same file.

bash
sudo pacman -S openfortivpn
curl -o /tmp/inter.cer http://crt.harica.gr/HARICA-GEANT-TLS-E1.cer
openssl x509 -inform DER -in /tmp/inter.cer -out /tmp/HARICA-GEANT-TLS-E1.pem
sudo install -d -m 755 /etc/openfortivpn
cat /tmp/HARICA-GEANT-TLS-E1.pem /etc/ssl/certs/ca-certificates.crt |
  sudo tee /etc/openfortivpn/ca-bundle.pem > /dev/null
bash
sudo apt install openfortivpn
curl -o /tmp/inter.cer http://crt.harica.gr/HARICA-GEANT-TLS-E1.cer
openssl x509 -inform DER -in /tmp/inter.cer -out /tmp/HARICA-GEANT-TLS-E1.pem
sudo install -d -m 755 /etc/openfortivpn
cat /tmp/HARICA-GEANT-TLS-E1.pem /etc/ssl/certs/ca-certificates.crt |
  sudo tee /etc/openfortivpn/ca-bundle.pem > /dev/null

macOS keeps its roots in a keychain rather than a file, so they are exported first.

bash
brew install openfortivpn
curl -o /tmp/inter.cer http://crt.harica.gr/HARICA-GEANT-TLS-E1.cer
openssl x509 -inform DER -in /tmp/inter.cer -out /tmp/HARICA-GEANT-TLS-E1.pem
security find-certificate -a -p /System/Library/Keychains/SystemRootCertificates.keychain > /tmp/roots.pem
sudo install -d -m 755 /etc/openfortivpn
cat /tmp/HARICA-GEANT-TLS-E1.pem /tmp/roots.pem |
  sudo tee /etc/openfortivpn/ca-bundle.pem > /dev/null

The URL above is not a third-party mirror: it is published inside the gateway's own certificate, in the Authority Information Access extension. The intermediate is GEANT TLS ECC 1, under HARICA TLS ECC Root CA 2021, which your system already trusts.

The bundle lives under /etc and is owned by root on purpose. openfortivpn runs with sudo, so a bundle in a home directory would be writable by the account it is meant to protect.

Connect

bash
sudo openfortivpn vpn.thi.de --saml-login --ca-file=/etc/openfortivpn/ca-bundle.pem

openfortivpn prints an SSO URL and waits. Open that URL in a browser yourself and log in as usual: nothing opens it for you, so a run that looks stuck is usually a run whose URL nobody clicked. The tunnel stays up as long as the command runs; Ctrl+C disconnects.

Or use the script, which checks the bundle first and explains the failure instead of printing a TLS error:

vpn-connect.sh Download

Connects, with a readable diagnosis if the bundle is missing or incomplete

curl -fsSLO https://hutzelmann.github.io/thi-linux-macos-setup/scripts/vpn-connect.sh
less vpn-connect.sh   # read it first
sh vpn-connect.sh --dry-run
sh vpn-connect.sh

Runs on its own. The documented values and helpers are built in, no clone needed. --dry-run prints what it would do and changes nothing.

Fallback: pin the certificate, for about a year

Where building a bundle is not an option, openfortivpn can be told to accept one specific gateway certificate:

bash
sudo openfortivpn vpn.thi.de --saml-login --trusted-cert=<sha256>

openfortivpn prints the exact digest to pass when validation fails, so one failed attempt is enough to read it. To work it out beforehand:

bash
echo | openssl s_client -connect vpn.thi.de:443 \
  -servername vpn.thi.de 2>/dev/null |
  openssl x509 -noout -fingerprint -sha256 | cut -d= -f2 | tr -d ':' | tr 'A-Z' 'a-z'

The limitation: this pins a single certificate rather than the chain, and the gateway certificate is replaced roughly once a year. On the next replacement the connection fails again and the digest has to be found and re-recorded by hand. The bundle above survives that, because the intermediate and the root outlive the leaf by years.

No digest is recorded on this page. A value read off the network is an observed value, not a verified one, and pinning trust material is a decision each reader makes for themselves.

Verify

vpn-verify.sh Download

Checks the gateway and whether the documented bundle is in place

curl -fsSLO https://hutzelmann.github.io/thi-linux-macos-setup/scripts/vpn-verify.sh
less vpn-verify.sh   # read it first
sh vpn-verify.sh --dry-run

Runs on its own. The documented values and helpers are built in, no clone needed. --dry-run prints what it would do and changes nothing.

Nothing is connected and no credentials are used; it observes the TLS handshake only. By hand:

bash
echo | openssl s_client -connect vpn.thi.de:443 \
  -servername vpn.thi.de 2>/dev/null | openssl x509 -noout -subject -dates

Known quirks

DNS breaks Avahi, and Avahi breaks printing. openfortivpn reconfigures DNS through resolvconf. On systems using systemd-resolved, this collides with the Avahi daemon that CUPS relies on for printer discovery, so connecting to the VPN can stop printing from working until you disconnect. Installing openresolv and letting it manage the handover is the usual fix. Discussion: Arch BBS thread.

Installing openresolv is only half of it, and the other half is quiet. The gateway pushes its nameservers and a search suffix, and openfortivpn hands them to resolvconf. If NetworkManager is still writing /etc/resolv.conf itself, two programs claim one file: openresolv finds a file it did not create and declines rather than overwrite it.

INFO:   Adding VPN nameservers...
resolvconf: signature mismatch: /etc/resolv.conf
resolvconf: run `resolvconf -u` to update

Traffic through the tunnel is unaffected, so this presents as a connected VPN in which every campus name has suddenly become unknown, and /etc/resolv.conf still lists whatever your local network gave you. The first line of that file names whichever program last wrote it, which is the quickest way to tell the two situations apart. The nameservers that should have been installed are printed by openfortivpn itself, one line above, in its Got addresses line.

This is a local collision rather than anything about campus. No fix is recorded here yet.

DNS breaks Avahi, and Avahi breaks printing. openfortivpn reconfigures DNS through resolvconf. On systems using systemd-resolved, this collides with the Avahi daemon that CUPS relies on for printer discovery, so connecting to the VPN can stop printing from working until you disconnect. Installing openresolv and letting it manage the handover is the usual fix. Discussion: Arch BBS thread.

Installing openresolv is only half of it, and the other half is quiet. The gateway pushes its nameservers and a search suffix, and openfortivpn hands them to resolvconf. If NetworkManager is still writing /etc/resolv.conf itself, two programs claim one file: openresolv finds a file it did not create and declines rather than overwrite it.

INFO:   Adding VPN nameservers...
resolvconf: signature mismatch: /etc/resolv.conf
resolvconf: run `resolvconf -u` to update

Traffic through the tunnel is unaffected, so this presents as a connected VPN in which every campus name has suddenly become unknown, and /etc/resolv.conf still lists whatever your local network gave you. The first line of that file names whichever program last wrote it, which is the quickest way to tell the two situations apart. The nameservers that should have been installed are printed by openfortivpn itself, one line above, in its Got addresses line.

This is a local collision rather than anything about campus. No fix is recorded here yet.

--saml-login needs a browser on the same machine. On a headless box, authenticate elsewhere and pass the resulting cookie, or use a different approach entirely.

How much traffic goes through the tunnel is the gateway's decision, not a setting here. On one account the gateway pushed routes for the campus networks only, and the default route stayed on the local interface, so traffic to anywhere else did not enter the tunnel. This is a policy on the gateway side and can differ per account, so read it off your own connection rather than assuming either way:

bash
ip route show dev ppp0     # what the gateway routed into the tunnel
ip route show default      # unchanged means everything else stays local

Expected, but worth knowing before a large download.