GRE over IPSec with IKEv2 between IOS and OpenBSD

Although the enterprise WAN connectivity market is increasingly focused on SD-WAN nowadays, there are still plenty of scenarios which call for more standards-based VPN technologies. In particular, inter-organisational connectivity (such as between partner companies).

Not only does the proprietary nature of the SD-WAN solutions available today mean they are unsuitable for such environments, but their architecture generally involves a single central controller / point of management. Admittedly I don’t have much direct experience of SD-WAN at this point, but I would be very surprised if the permissions model in such a design is capable of catering for environments in which no single person can have admin rights to the entire deployment, making them unsuitable for spanning multiple autonomous systems – where each AS wants full control over their own network.

For as long as I can remember, the de-facto standard VPN technology has been IPSec. In particular, for a site-to-site scenario that means IPSec tunnel mode in conjunction with IKEv1. As such, it is the only technology that can be relied upon to be supported by both parties in an inter-AS VPN.

In recent years IKEv2 has superseded IKEv1, and become sufficiently commonplace to be usable in the vast majority of scenarios. Having said that, OpenBSD does have a frustrating limitation whereby you can only run one or other of isakmpd (IKEv1) or iked (IKEv2) at a time, making it essentially impossible to migrate an OpenBSD which handles numerous IPSec VPNs to other organisations from IKEv1 to IKEv2.

This post will cover establishing an IKEv2 VPN between OpenBSD and Cisco IOS. But rather than IPSec tunnel mode, this example shows GRE-over-IPSec. Although it can’t be relied upon to the same extent to be supported by any arbitrary VPN router, GRE-over-IPSec does have fairly broad support, and offers significantly more flexibility in terms of the traffic that it can carry.

Scenario

The configurations below have proven to work well for me for over a year now (I use it for a VPN between my home network (the OpenBSD end) and my Mum’s home network (the Cisco end). In case you’re wondering, unsurprisingly I set up my Mum’s home network… she has no idea how any of this works, but FWIW the Cisco router she’s using is a C887VA-WD-E-K9, and I’m currently running OpenBSD 7.0.

OpenBSD

As always, OpenBSD’s documentation is excellent, so for the definitive information on how to configure iked I could simply say “RTFM” (or at least, “man iked.conf”). But I’ll provide my example configuration (/etc/iked.conf) below:

ikev2 quick active transport esp inet proto gre from <openbsd_ip> to <cisco_ip> local \
<openbsd_ip> peer <cisco_ip> ikesa enc aes-256-gcm prf hmac-sha2-384 \
group ecp384 childsa enc aes-256-gcm group ecp384 srcid <openbsd_ip> dstid <cisco_ip> \
ikelifetime 86400 lifetime 3600 bytes 524288000 psk "ThisShouldBeAStrongPassword"

In this scenario, both VPN routers have static IPs and either side can initiate the tunnel (thus the “active” keyword). The only traffic directly encapsulated by IPSec will be GRE between the two VPN endpoint IPs themselves, so the traffic selector explicitly specifies “gre” as the protocol. This is important because the configuration we use for Cisco IOS does the same thing implicitly, and it needs to match on OpenBSD in order for the VPN to function. I have selected some strong ciphers that are supported by both devices, and am using a pre-shared-key for authentication (other forms of authentication are available, but I selected PSK for simplicity).

With the configuration in place, enabling and starting iked is simple:

rcctl enable iked
rcctl start iked

Now we need to configure the GRE tunnel. That involves defining the interface via the /etc/hostname.gre0 configuration file:

inet <openbsd_tunnel_ip> 255.255.255.252 <cisco_tunnel_ip>
inet6 <openbsd_tunnel_ipv6> 127
tunnel <openbsd_ip> <cisco_ip>
mtu 1442

Assuming PF is enabled, the following rules in /etc/pf.conf permit establishment of the VPN and set the TCP MSS values appropriately given the tunnel interface’s MTU:

# TCP MSS
match on { gre0 } inet scrub (max-mss 1422)
match on { gre0 } inet6 scrub (max-mss 1402)
# Permit IKEv2 / IPSec
pass out quick on { egress } proto { udp } from { egress } to { $cisco_ip } port { \
    500, 4500 } user { _iked }
pass in quick on { egress } proto { udp } from { $cisco_ip } to { egress } port { \
    500, 4500 }
pass out quick on { egress } proto { esp } from { egress } to { $cisco_ip }
pass in quick on { egress } proto { esp } from { $cisco_ip } to { egress }
pass out quick on { enc0 } proto { ipencap } from { egress } to { $cisco_ip } keep \
    state (if-bound)
pass in quick on { enc0 } proto { ipencap } from { $cisco_ip } to { egress } keep \
    state (if-bound)
pass out quick on { enc0 } proto { gre } from { egress } to { $cisco_ip }
pass in quick on { enc0 } proto { gre } from { $cisco_ip } to { egress }

PF rules to permit transit traffic through the VPN aren’t covered above, but they simply need to be defined such that they operate on the gre0 interface.

It is also necessary to enable IP forwarding, and GRE via sysctl, as those things are disabled by default. Here’s the appropriate configuration to be added to /etc/sysctl.conf:

net.inet.ip.forwarding=1
net.inet6.ip6.forwarding=1
net.inet.gre.allow=1

To make those changes live without a reboot, also run the following commands:

sysctl net.inet.ip.forwarding=1
sysctl net.inet6.ip6.forwarding=1
sysctl net.inet.gre.allow=1

The gre0 interface can then be brought up, and the PF ruleset reloaded, by running:

sh /etc/netstart gre0
pfctl -f /etc/pf.conf

Cisco IOS

In comparison, the equivalent IOS configuration is significantly lengthier:

crypto ikev2 proposal VPN-IKEv2-Proposal
 encryption aes-gcm-256
 prf sha384
 group 20
!
crypto ikev2 policy VPN-IKEv2-Policy
 proposal VPN-IKEv2-Proposal
!
crypto ikev2 keyring VPN-IKEv2-Keyring
 peer openbsd
  address <openbsd_ip>
  pre-shared-key local ThisShouldBeAStrongPassword
  pre-shared-key remote ThisShouldBeAStrongPassword
 !
!
crypto ikev2 profile VPN-IKEv2-Profile
 match identity remote address <openbsd_ip> 255.255.255.255
 authentication remote pre-share
 authentication local pre-share
 keyring local VPN-IKEv2-Keyring
 dpd 60 10 on-demand
!
crypto isakmp aggressive-mode disable
!
crypto ipsec security-association replay window-size 1024
!
crypto ipsec transform-set ESP-AESGCM-256 esp-gcm 256
 mode transport
crypto ipsec fragmentation after-encryption
!
crypto ipsec profile VPN-IKEv2-IPsec-Profile
 set security-association lifetime kilobytes 524288
 set transform-set ESP-AESGCM-256
 set pfs group20
 set ikev2-profile VPN-IKEv2-Profile
!
interface Tunnel0
 ip address <cisco_tunnel_ip> 255.255.255.252
 ip tcp adjust-mss 1402
 ipv6 address <cisco_tunnel_ip_ipv6>/127
 ipv6 tcp adjust-mss 1382
 tunnel source <cisco_ip>
 tunnel destination <openbsd_ip>
 tunnel path-mtu-discovery
 tunnel protection ipsec profile VPN-IKEv2-IPsec-Profile

A few things are worth pointing out:

  • Similarly IOS also supports having different PSKs in each direction, but OpenBSD does not, so we have to stick with just a single PSK
  • I recently discovered that aggressive mode is enabled by default in IOS, and needs to be explicitly disabled
  • I believe the various MTU and MSS values in the configurations above should be accurate for the chosen encapsulations (assuming a 1500 Byte MTU over the internet for the transport network)

As far as ACL entries to permit the VPN-related traffic are concerned, IOS doesn’t require rules to permit the GRE traffic through the IPSec tunnel, however the following are still needed for inbound & outbound ACLs on the router’s physical interface (if defined):

ip access-list extended Inbound
 permit udp host <openbsd_ip> host <cisco_ip> eq isakmp
 permit udp host <openbsd_ip> host <cisco_ip> eq non500-isakmp
 permit esp host <openbsd_ip> host <cisco_ip>
!
ip access-list extended Outbound
 permit udp host <cisco_ip> host <openbsd_ip> eq isakmp
 permit udp host <cisco_ip> host <openbsd_ip> eq non500-isakmp
 permit esp host <cisco_ip> host <openbsd_ip>

ACLs to restrict transit traffic flowing through VPN should be applied to the Tunnel0 interface. Unlike OpenBSD (where the default behaviour is for traffic to be blocked if PF is enabled), IOS will permit traffic over the VPN by default unless ACLs are defined and applied.

Initially I tried enabling GRE keepalives, but failed to get them to work. Later I discovered they are unsupported in conjunction with tunnel protection in IOS, as per https://www.cisco.com/c/en/us/support/docs/ip/generic-routing-encapsulation-gre/118370-technote-gre-00.html.

Routing

Now that the VPN itself is established, as it is a point-to-point GRE tunnel we can enable and configure any standard routing protocols (or just static routes) to direct traffic across the tunnel, rather than having to mess around with IPSec encryption domains as would be true for native IPSec tunnelling.

Easy-RSA as the basis for a PKI

This will be the first of a number of posts that I would have made in the past if I’d had a blog at the time. In other words the contents dates back a couple of years. But it should nevertheless (hopefully) still be relevant/interesting!

Why bother with a private PKI at all? Just use Let’s Encrypt…

For publicly-trusted X.509 certificates, Let’s Encrypt has undoubtedly become the go-to solution for most use-cases (pretty much anything that doesn’t require OV or EV certificates). Unsurprisingly, as you may have noticed, I’m using a Let’s Encrypt certificate for this website. But within an enterprise environment there are plenty of scenarios in which Let’s Encrypt is not an appropriate choice, and instead a locally-managed PKI is called for. I’ll provide one example…

I often find myself frustrated when trying to implement X.509 certificates for client authentication purposes due to the inexplicably limited number of ways in which many applications are able to restrict which certificates to allow. Quite often the only way to control which end-entity certificates are accepted as a valid proof of identity is to restrict which CA certificates to trust, which provides no value unless you only trust CAs that you operate yourself. Even in cases where the authenticating application uses an attribute in the certificate to map it to a user account in an LDAP directory service, and then leverages group memberships defined in that directory to determine what level of access to grant for each certificate (including potentially no access), the whole process remains dependent upon trusting that the value contained in the mapped attribute could never been included in a certificate issued to someone other than the owner of the user account to which it maps… which ultimately means completely trusting the security of your application to the CAs that you trust. While that may be the status-quo for many server certificates (such as those used for public websites), it is usually unacceptable for client certificates. Hence the continued need for private PKIs.

What PKI software to use?

PKIs are complex. That complexity is compounded by the fact that once you have implemented and begun to use it, you can’t easily make changes to the configuration without potentially impacting all already-issued and still-valid certificates.

There are plenty of commercial options available for use in an enterprise. Windows environments are likely to use Microsoft’s Active Directory Certificate Services, as it offers convenient integration with Active Directory and Windows operating systems.

For my home network (and for experimentation / learning purposes), I have looked at numerous open source options over the years. Some time ago I decided to go with EJBCA, as it appeared to be the most feature-rich solution out there. Although it certainly worked, and was a learning experience, in reality it was total overkill as I only needed a small fraction of the functionality, and keeping it in a healthy functioning state proved to be too time-consuming to be worth it. I also wasn’t a fan of it being Java-based.

More recently I looked at some modern solutions such as CFSSL and Vault, both of which have a lot going for them. However I ultimately concluded that I still wanted greater flexibility than they could offer (mainly to satisfy my desire for experimentation). Of course the greatest flexibility of all can be achieved by using OpenSSL directly. However doing so effectively was both beyond my level of expertise and once again likely to be too time-consuming to be worth it.

Then I remembered having used Easy-RSA in the past to issue and manage certificates for use with OpenVPN. It turns out that Easy-RSA is essentially a bunch of wrapper scripts built around OpenSSL, which together implement the basic functionality of a PKI. I liked the fact that I could read those wrapper scripts to understand what it was doing behind the scenes, and then extend its functionality myself without too much difficulty if I wanted. It seemed like the perfect starting point from which to build the PKI solution that I was looking for.

Designing a PKI

Due to the difficulty of changing the structure of a PKI once it is operational, careful planning is required at the outset, starting with the CA hierarchy.

The use of Easy-RSA makes it relatively straightforward to instantiate additional CAs, so I decided not only to have a dedicated non-issuing root CA, but also to have multiple issuing CAs, each issuing certificates for a different purpose (for example I have one issuing CA for server certificates, and a different issuing CA for client certificates).

Next up is certificate profiles, covering the important attributes for different types of certificate (you can’t issue your root CA certificate until you’ve decided what attributes it should have!), and selecting what cryptographic algorithms to use.

Having researched and planned all of this, I then realised that these details are supposed to be documented in a Certification Practice Statement (CPS). So I created such a document, and made it available over HTTP, so that it can itself be referenced by URL in the “Certificate Policy” attribute in the certificates. I made it available here as it doesn’t contain anything I’m not comfortable making public: lspeed.org Public Key Infrastructure Certification Practice Statement. It contains a lot of additional detail on my design choices, including the rationale behind them, so do read it alongside this post if you are interested in those things.

Customising Easy-RSA

Moving on to the implementation, I chose to host all my CAs on a single VM running Gentoo Linux, each in their own sub-directory under /etc/pki. I start by installing easy-rsa (v3.0.5), libfaketime (more on that later), and cfssl (used for its OCSP functionality), and then create a tweaked version of the OpenSSL configuration shipped with Easy-RSA and set up some tweaked x509-types to match the specs I came up with for the certificate profiles (as documented in the CPS):

emerge -av easy-rsa libfaketime cfssl
mkdir /etc/pki/x509-types
cp /usr/share/easy-rsa/openssl-1.0.cnf /etc/pki/
cp /usr/share/easy-rsa/x509-types/* /etc/pki/x509-types/

Here are diffs showing the changes I then applied to the openssl-1.0.cnf and x509-types/ca files:

--- /usr/share/easy-rsa/openssl-1.0.cnf 2018-02-15 16:03:28.023402027 +0000
+++ /etc/pki/openssl-1.0.cnf    2018-02-17 21:53:06.010101175 +0000
@@ -18,6 +18,7 @@
 certificate    = $dir/ca.crt           # The CA certificate
 serial         = $dir/serial           # The current serial number
 crl            = $dir/crl.pem          # The current CRL
+crlnumber      = $dir/crlnumber
 private_key    = $dir/private/ca.key   # The private key
 RANDFILE       = $dir/.rand            # private random number file

@@ -114,15 +115,14 @@
 # PKIX recommendations:

 subjectKeyIdentifier=hash
-authorityKeyIdentifier=keyid:always,issuer:always

 # This could be marked critical, but it's nice to support reading by any
 # broken clients who attempt to do so.
-basicConstraints = CA:true
+basicConstraints = critical, CA:true

 # Limit key usage to CA tasks. If you really want to use the generated pair as
 # a self-signed cert, comment this out.
-keyUsage = cRLSign, keyCertSign
+keyUsage = critical, cRLSign, keyCertSign

 # nsCertType omitted by default. Let's try to let the deprecated stuff die.
 # nsCertType = sslCA
@@ -133,5 +133,5 @@
 # Only issuerAltName and authorityKeyIdentifier make any sense in a CRL.

 # issuerAltName=issuer:copy
-authorityKeyIdentifier=keyid:always,issuer:always
+authorityKeyIdentifier=keyid:always
--- /usr/share/easy-rsa/x509-types/ca   2018-12-17 07:17:51.376734773 +0000
+++ /etc/pki/x509-types/ca      2018-02-25 15:55:57.442133868 +0000
@@ -6,8 +6,9 @@
 #
 # basicConstraints = CA:TRUE, pathlen:1

-basicConstraints = CA:TRUE
+basicConstraints = critical, CA:TRUE, pathlen:0
 subjectKeyIdentifier = hash
-authorityKeyIdentifier = keyid:always,issuer:always
-keyUsage = cRLSign, keyCertSign
-
+authorityKeyIdentifier = keyid:always
+keyUsage = critical, cRLSign, keyCertSign
+authorityInfoAccess = OCSP;URI:http://pki.lspeed.org/ocsp/$ENV::CA_NAME
+certificatePolicies = ia5org,@cpAll
--- /usr/share/easy-rsa/x509-types/COMMON       2018-02-16 20:04:29.189438645 +0000
+++ /etc/pki/x509-types/COMMON  2018-02-16 20:03:57.814304078 +0000
@@ -4,4 +4,5 @@
 # It could be used to add values every cert should have, such as a CDP as
 # demonstrated in the following example:

#crlDistributionPoints = URI:http://example.net/pki/my_ca.crl
+crlDistributionPoints = URI:http://pki.lspeed.org/lspeed_$ENV::CA_NAME.crl
+authorityInformationAccess = URI:http://pki.lspeed.org/lspeed_$ENV::CA_NAME.crt
--- /usr/share/easy-rsa/x509-types/client    2018-03-20 20:36:48.182453423 +0000
+++ /etc/pki/x509-types/client    2018-03-20 20:57:09.787860524 +0000
@@ -1,8 +1,9 @@
 # X509 extensions for a client

-basicConstraints = CA:FALSE
+basicConstraints = critical, CA:FALSE
 subjectKeyIdentifier = hash
-authorityKeyIdentifier = keyid,issuer:always
+authorityKeyIdentifier = keyid:always
+authorityInfoAccess = OCSP;URI:http://pki.lspeed.org/ocsp/$ENV::CA_NAME
 extendedKeyUsage = clientAuth
-keyUsage = digitalSignature
-
+keyUsage = critical, digitalSignature
+certificatePolicies = ia5org,@cpStandard
--- /usr/share/easy-rsa/x509-types/server       2018-12-17 07:17:51.377734792 +0000
+++ /etc/pki/x509-types/server  2018-02-25 12:39:32.679873646 +0000
@@ -1,8 +1,9 @@
 # X509 extensions for a server

-basicConstraints = CA:FALSE
+basicConstraints = critical, CA:FALSE
 subjectKeyIdentifier = hash
-authorityKeyIdentifier = keyid,issuer:always
+authorityKeyIdentifier = keyid:always
+authorityInfoAccess = OCSP;URI:http://pki.lspeed.org/ocsp/$ENV::CA_NAME
 extendedKeyUsage = serverAuth
-keyUsage = digitalSignature,keyEncipherment
-
+keyUsage = critical, digitalSignature,keyEncipherment
+certificatePolicies = ia5org,@cpStandard

Besides implementing the specs from the certificate profiles, you’ll notice I have additionally removed the issuer from the authorityKeyIdentifier attribute (see https://v13.gr/2013/04/11/x509v3-authority-key-identifier-authoritykeyidentifier/ for an explanation of why that is important), and added a crlnumber file to keep track of the CRL serial.

Initialising the root CA

Now it’s time to initialise the root CA. This is where libfaketime comes in… to achieve “nice round timestamps” in the validity start and end dates. I also opt for the longest possible validity time whilst avoiding the “Year 2038 Problem” (when 32-bit Unix epoch timestamps wrap), and specify my chosen crypto algorithms. Note that some of these commands are very long, so I have manually split them across multiple lines to avoid the need for excessive scrolling within the code block below:

# Root CA - Setup
CA_NAME=r1
mkdir /etc/pki/${CA_NAME}
EASYRSA=/usr/share/easy-rsa /bin/sh /usr/share/easy-rsa/easyrsa \
    --pki-dir=/etc/pki/${CA_NAME} init-pki
# Calculate the number of days between today and the day before the year 2038 bug hits
TZ=UTC CA_VALIDITY=`echo "( \`date -d 2038-01-18 +%s\` - \`date -d \\\`date +%F \\\` \
    +%s\` ) / (24*3600)" | bc -l | sed 's/\..*//'`
# Use faketime to get nice round timestamps (ie 00:00:00) in start and end dates
TZ=UTC EASYRSA_EXTRA_EXTS='basicConstraints = critical,CA:TRUE' \
    EASYRSA_SSL_CONF=/etc/pki/openssl-1.0.cnf EASYRSA=/usr/share/easy-rsa faketime -f \
    "`date --rfc-3339=seconds -d \`date +%F\` | sed s/\+.*//`" /bin/sh \
    /usr/share/easy-rsa/easyrsa --batch --pki-dir=/etc/pki/${CA_NAME} --use-algo=ec \
    --curve=secp384r1 --digest=sha384 --dn-mode=org --req-c=GB --req-st= --req-city= \
    --req-ou= --req-email= --req-org=lspeed --req-cn="lspeed ECC Root CA - R1" \
    --days=$CA_VALIDITY build-ca

Finally the root CA certificate is converted to binary DER format and placed in a separate directory intended to be made available to clients over HTTP:

mkdir /etc/pki/pub
openssl x509 -in /etc/pki/${CA_NAME}/ca.crt -outform DER > \
    /etc/pki/pub/lspeed_${CA_NAME}.crt

CRL for the root CA

Bearing in mind I would like to avoid any unnecessary human involvement in the day-to-day operation of the CA, my approach to CRLs is to pregenerate all CRLs in advance, with the same content but different (future) validity dates, forming a continuous set of CRLs spanning the entire remaining validity period of the CA certificate itself. That way, assuming no additional certificates are revoked, the CRLs can be generated once and then never again. This is important because the CRL is signed by the CA, and therefore manual entry of the password which protects the CA’s private key is required in order to generate a CRL.

If a certificate does need to be revoked (which in my environment I expect to happen extremely infrequently), then human involvement will be required to once again regenerate all future CRLs at that time. Even more so than during certificate issuance, faketime plays an essential role here.

I’ve opted to generate monthly CRLs, but for each one to be valid for 75 days, thereby giving a decent overlap period in which to switch from using one CRL to the next… hopefully sufficient time to spot and fix any issues with the process if they arise.

# Root CA - CRL
CA_NAME=r1
# Temporarily decrypt CA private key in order to issue numerous CRLs
cp /etc/pki/${CA_NAME}/private/ca.key /etc/pki/${CA_NAME}/private/ca.key.orig
EASYRSA_PKI=/etc/pki/${CA_NAME} /bin/sh /usr/share/easy-rsa/easyrsa set-ec-pass ca \
    nopass
# Clear existing pre-generated CRLs
rm -r /etc/pki/${CA_NAME}/crls
mkdir /etc/pki/${CA_NAME}/crls
# Create new pre-generated CRLs
crldate=`date +%Y-%m-01`
crlend="2038-03-01"
until [ "$crldate" == "$crlend" ]; do
  TZ=UTC faketime $crldate date -I | sed s/-//g > /etc/pki/${CA_NAME}/crlnumber
  TZ=UTC EASYRSA_CRL_DAYS=75 EASYRSA=/usr/share/easy-rsa \
      EASYRSA_SSL_CONF=/etc/pki/openssl-1.0.cnf faketime $crldate /bin/sh \
      /usr/share/easy-rsa/easyrsa --pki-dir=/etc/pki/${CA_NAME} --use-algo=ec \
      --curve=secp384r1 --digest=sha384 gen-crl
  openssl crl -in /etc/pki/${CA_NAME}/crl.pem -outform DER > \
      /etc/pki/${CA_NAME}/crls/$crldate.crl
  crldate=`date --date "$crldate 1 month" +%Y-%m-%d`
done
chmod -R go-rwx /etc/pki/${CA_NAME}/crls
# Create current CRL
date -I | sed s/-//g > /etc/pki/${CA_NAME}/crlnumber
EASYRSA_CRL_DAYS=75 EASYRSA=/usr/share/easy-rsa \
    EASYRSA_SSL_CONF=/etc/pki/openssl-1.0.cnf /bin/sh /usr/share/easy-rsa/easyrsa \
    --pki-dir=/etc/pki/${CA_NAME} --use-algo=ec --curve=secp384r1 --digest=sha384 \
    gen-crl
openssl crl -in /etc/pki/${CA_NAME}/crl.pem -outform DER > \
    /etc/pki/pub/lspeed_${CA_NAME}.crl
# Re-encrypt CA private key
mv /etc/pki/${CA_NAME}/private/ca.key.orig /etc/pki/${CA_NAME}/private/ca.key

OCSP for the root CA

I take a similar approach with OCSP, by pre-generating an OCSP response for every issued certificate (rather than having the OCSP responder do so dynamically whenever an OCSP request is received). Unlike with CRLs, the OCSP responses are signed by a special OCSP certificate. That means it is more reasonable to avoid having to enter the private key’s password during OCSP response pre-generation by simply not password-protecting the OCSP certificate’s private key in the first place), and therefore we can get away with pre-generating just a single OCSP response for each issued certificate, rather than pre-generating as many of them as would be required to last until the root CA’s certificate expires (as future responses can be generated automatically).

Note that by pre-generating OCSP responses, the OCSP responder will not be capable of supporting the OCSP nonce extension. However that appears to be common-practice.

But first we need to issue the OCSP responder’s own certificate. This requires its own certificate profile (and thus x509-type file):

echo '# X509 extensions for an OCSP Responder
basicConstraints = critical, CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
extendedKeyUsage = critical, OCSPSigning
keyUsage = critical, digitalSignature
certificatePolicies = ia5org,@cpStandard
noCheck = ignored' > /etc/pki/x509-types/ocsp
# Root CA - Issue OCSP Cert
CA_NAME=r1
EASYRSA=/usr/share/easy-rsa /bin/sh /usr/share/easy-rsa/easyrsa --batch \
    --pki-dir=/etc/pki/${CA_NAME} --use-algo=ec --curve=secp384r1 --dn-mode=org \
    --req-c=GB --req-st= --req-city= --req-ou= --req-email= --req-org=lspeed \
    --req-cn="lspeed ECC OCSP Responder - R1" gen-req "lspeed ECC OCSP Responder \
    - R1" nopass
# Calculate the number of days between today and the day before the year 2038 bug hits
TZ=UTC CA_VALIDITY=`echo "( \`date -d 2038-01-18 +%s\` - \`date -d \\\`date +%F \\\` \
    +%s\` ) / (24*3600)" | bc -l | sed 's/\..*//'`
# Use faketime to get nice round timestamps (ie 00:00:00) in start and end dates
TZ=UTC CA_NAME=${CA_NAME} EASYRSA_EXTRA_EXTS=$'[ cpStandard ]\npolicyIdentifier = \
    1.3.6.1.4.1.51546.1.1.1.1\nCPS = "https://pki.lspeed.org/";' \
    EASYRSA_EXT_DIR=/etc/pki/x509-types EASYRSA=/usr/share/easy-rsa faketime -f \
    "`date --rfc-3339=seconds -d \`date +%F\` | sed s/\+.*//`" /bin/sh \
    /usr/share/easy-rsa/easyrsa --batch --pki-dir=/etc/pki/${CA_NAME} --use-algo=ec \
    --curve=secp384r1 --digest=sha384 --days=$CA_VALIDITY sign-req ocsp "lspeed ECC \
    OCSP Responder - R1"

Next comes the OCSP response pre-generation. Here we build a temporary sqlite3 database containing the all the important attributes of every issued certificate, and then pass that to CFSSL to generate the OCSP responses:

# Root CA - OCSP Response Pre-generation
CA_NAME=r1
CA_NAME_UPPER=`echo $CA_NAME | tr '[:lower:]' '[:upper:]'`
rm -r /etc/pki/${CA_NAME}/ocsp 2>/dev/null
mkdir -p /etc/pki/${CA_NAME}/ocsp
mkdir -p /etc/pki/ocsp
echo '{"driver":"sqlite3","data_source":"/etc/pki/'${CA_NAME}'/ocsp/certs.db"}' > \
    /etc/pki/${CA_NAME}/ocsp/ocsp.json
sqlite3 /etc/pki/${CA_NAME}/ocsp/certs.db <<EOF
  CREATE TABLE certificates (
    serial_number            blob NOT NULL,
    authority_key_identifier blob NOT NULL,
    ca_label                 blob,
    status                   blob NOT NULL,
    reason                   int,
    expiry                   timestamp,
    revoked_at               timestamp,
    pem                      blob NOT NULL,
    PRIMARY KEY(serial_number, authority_key_identifier)
  );
  CREATE TABLE ocsp_responses (
    serial_number            blob NOT NULL,
    authority_key_identifier blob NOT NULL,
    body                     blob NOT NULL,
    expiry                   timestamp,
    PRIMARY KEY(serial_number, authority_key_identifier),
    FOREIGN KEY(serial_number, authority_key_identifier) REFERENCES \
        certificates(serial_number, authority_key_identifier)
  );
EOF
OIFS="$IFS"
IFS=$'\n'
for cert in `ls -1 /etc/pki/${CA_NAME}/issued`; do
  serial_number=`echo "ibase=16;obase=A;\`openssl x509 -serial -noout -in \
      /etc/pki/${CA_NAME}/issued/${cert} | sed s/.*=//\`" | bc`
  authority_key_identifier=`openssl x509 -text -in /etc/pki/${CA_NAME}/issued/${cert} \
      | grep keyid: | sed s/.*keyid// | sed s/://g | tr [:upper:] [:lower:]`
  status="good"
  reason="0"
  expiry=`date --rfc-3339=seconds -d "\`openssl x509 -enddate -noout -in \
      /etc/pki/${CA_NAME}/issued/${cert} | sed s/.*=//\`"`
  revoked_at="0001-01-01 00:00:00+00:00"
  pem=`openssl x509 -in /etc/pki/${CA_NAME}/issued/${cert}`
  sqlite3 /etc/pki/${CA_NAME}/ocsp/certs.db "INSERT INTO \
      certificates(serial_number,authority_key_identifier,ca_label,status,reason,\
      expiry,revoked_at,pem) VALUES('$serial_number','$authority_key_identifier','',\
      '$status','$reason','$expiry','$revoked_at','$pem')"
done
IFS="$OIFS"
openssl pkey -in "/etc/pki/${CA_NAME}/private/lspeed ECC OCSP Responder - \
    ${CA_NAME_UPPER}.key" -out /etc/pki/${CA_NAME}/ocsp/ocsp.key
cp "/etc/pki/${CA_NAME}/issued/lspeed ECC OCSP Responder - ${CA_NAME_UPPER}.crt" \
    /etc/pki/${CA_NAME}/ocsp/ocsp.crt
TZ=UTC faketime `date +%Y-%m-01` cfssl ocsprefresh -db-config \
    /etc/pki/${CA_NAME}/ocsp/ocsp.json -responder /etc/pki/${CA_NAME}/ocsp/ocsp.crt \
    -responder-key /etc/pki/${CA_NAME}/ocsp/ocsp.key -ca /etc/pki/${CA_NAME}/ca.crt \
    -interval 1800h
cfssl ocspdump -db-config /etc/pki/${CA_NAME}/ocsp/ocsp.json > \
    /etc/pki/ocsp/ocsp_${CA_NAME}.dump
chmod 600 /etc/pki/ocsp/ocsp_${CA_NAME}.dump
rm -r /etc/pki/${CA_NAME}/ocsp

You might notice that I don’t actually populate the sqlite3 database with details of revoked certificates, so the OCSP responses are not pre-generated for them. That’s simply because I haven’t had to implement that yet (because I don’t have any revoked certificates), and because I’m lazy. But it shouldn’t be hard to extend the logic to encompass revoked certificates too. Having said that, it should already be the case that the OCSP responder does not provide positive “everything is fine” responses for revoked certificates. I would expect it to respond with some kind of “this certificate does not exist” message instead (although I’ve not tested that).

If/when I get round to implementing that, I’ll update this post. Or alternatively, feel free to do it for me and let me know what needs to be added šŸ™‚

Issuing CAs

The process is very similar for the issuing CAs. Here’s the setup for the first issuing CA (named “S1”)… initialising the CA first, then pre-generating CRLs, issuing an OCSP certificate, and pre-generating OCSP responses:

# Issuing CA - S1 - Setup
CA_NAME=r1
SUBCA_NAME=s1
mkdir -p /etc/pki/${CA_NAME}
EASYRSA=/usr/share/easy-rsa /bin/sh /usr/share/easy-rsa/easyrsa \
    --pki-dir=/etc/pki/${SUBCA_NAME} init-pki
EASYRSA=/usr/share/easy-rsa /bin/sh /usr/share/easy-rsa/easyrsa --batch \
    --pki-dir=/etc/pki/${SUBCA_NAME} --use-algo=ec --curve=secp384r1 --dn-mode=org \
    --req-c=GB --req-st= --req-city= --req-ou= --req-email= --req-org=lspeed \
    --req-cn="lspeed ECC Issuing CA - S1" build-ca subca
EASYRSA=/usr/share/easy-rsa /bin/sh /usr/share/easy-rsa/easyrsa \
    --pki-dir=/etc/pki/${CA_NAME} import-req /etc/pki/${SUBCA_NAME}/reqs/ca.req \
    "lspeed ECC Issuing CA - S1"
# Calculate the number of days between today and 31/12/37
TZ=UTC CA_VALIDITY=`echo "( \`date -d 2037-12-31 +%s\` - \`date -d \\\`date +%F \\\` \
    +%s\` ) / (24*3600)" | bc -l | sed 's/\..*//'`
# Use faketime to get nice round timestamps (ie 00:00:00) in start and end dates
CA_NAME=${CA_NAME} TZ=UTC EASYRSA_EXTRA_EXTS=$'[ cpAll ]\npolicyIdentifier = \
    2.5.29.32.0\nCPS = "https://pki.lspeed.org/";' EASYRSA=/usr/share/easy-rsa \
    EASYRSA_EXT_DIR=/etc/pki/x509-types faketime -f "`date --rfc-3339=seconds -d \
    \`date +%F\` | sed s/\+.*//`" /bin/sh /usr/share/easy-rsa/easyrsa \
    --pki-dir=/etc/pki/${CA_NAME} --digest=sha384 --days=$CA_VALIDITY sign-req ca \
    "lspeed ECC Issuing CA - S1"
cp "/etc/pki/${CA_NAME}/issued/lspeed ECC Issuing CA - S1.crt" \
    /etc/pki/${SUBCA_NAME}/ca.crt
openssl x509 -in /etc/pki/${SUBCA_NAME}/ca.crt -outform DER > \
    /etc/pki/pub/lspeed_${SUBCA_NAME}.crt
# Issuing CA - S1 - CRL
CA_NAME=s1
# Temporarily decrypt CA private key in order to issue numerous CRLs
cp /etc/pki/${CA_NAME}/private/ca.key /etc/pki/${CA_NAME}/private/ca.key.orig
EASYRSA_PKI=/etc/pki/${CA_NAME} /bin/sh /usr/share/easy-rsa/easyrsa set-ec-pass ca \
    nopass
# Clear existing pre-generated CRLs
rm -r /etc/pki/${CA_NAME}/crls
mkdir /etc/pki/${CA_NAME}/crls
# Create new pre-generated CRLs
crldate=`date +%Y-%m-01`
crlend="2038-02-01"
until [ "$crldate" == "$crlend" ]; do
  TZ=UTC faketime $crldate date -I | sed s/-//g > /etc/pki/${CA_NAME}/crlnumber
  TZ=UTC EASYRSA_CRL_DAYS=75 EASYRSA=/usr/share/easy-rsa \
      EASYRSA_SSL_CONF=/etc/pki/openssl-1.0.cnf faketime $crldate /bin/sh \
      /usr/share/easy-rsa/easyrsa --pki-dir=/etc/pki/${CA_NAME} --use-algo=ec \
      --curve=secp384r1 --digest=sha384 gen-crl
  openssl crl -in /etc/pki/${CA_NAME}/crl.pem -outform DER > \
      /etc/pki/${CA_NAME}/crls/$crldate.crl
  crldate=`date --date "$crldate 1 month" +%Y-%m-%d`
done
chmod -R go-rwx /etc/pki/${CA_NAME}/crls
# Create current CRL
date -I | sed s/-//g > /etc/pki/${CA_NAME}/crlnumber
EASYRSA_CRL_DAYS=75 EASYRSA=/usr/share/easy-rsa \
    EASYRSA_SSL_CONF=/etc/pki/openssl-1.0.cnf /bin/sh /usr/share/easy-rsa/easyrsa \
    --pki-dir=/etc/pki/${CA_NAME} --use-algo=ec --curve=secp384r1 --digest=sha384 \
    gen-crl
openssl crl -in /etc/pki/${CA_NAME}/crl.pem -outform DER > \
    /etc/pki/pub/lspeed_${CA_NAME}.crl
# Re-encrypt CA private key
mv /etc/pki/${CA_NAME}/private/ca.key.orig /etc/pki/${CA_NAME}/private/ca.key
# Issuing CA - S1 - Issue OCSP Cert
CA_NAME=s1
# OCSP signing certificate
EASYRSA=/usr/share/easy-rsa /bin/sh /usr/share/easy-rsa/easyrsa --batch \
    --pki-dir=/etc/pki/${CA_NAME} --use-algo=ec --curve=secp384r1 --dn-mode=org \
    --req-c=GB --req-st= --req-city= --req-ou= --req-email= --req-org=lspeed \
    --req-cn="lspeed ECC OCSP Responder - S1" gen-req "lspeed ECC OCSP Responder - \
    S1" nopass
# Calculate the number of days between today and 31/12/37
TZ=UTC CA_VALIDITY=`echo "( \`date -d 2037-12-31 +%s\` - \`date -d \\\`date +%F \\\` \
    +%s\` ) / (24*3600)" | bc -l | sed 's/\..*//'`
# Use faketime to get nice round timestamps (ie 00:00:00) in start and end dates
CA_NAME=${CA_NAME} TZ=UTC EASYRSA_EXTRA_EXTS=$'[ cpStandard ]\npolicyIdentifier = \
    1.3.6.1.4.1.51546.1.1.1.1\nCPS = "https://pki.lspeed.org/";' \
    EASYRSA_EXT_DIR=/etc/pki/x509-types EASYRSA=/usr/share/easy-rsa faketime -f \
    "`date --rfc-3339=seconds -d \`date +%F\` | sed s/\+.*//`" /bin/sh \
    /usr/share/easy-rsa/easyrsa --batch --pki-dir=/etc/pki/${CA_NAME} --use-algo=ec \
    --curve=secp384r1 --digest=sha384 --days=$CA_VALIDITY sign-req ocsp "lspeed ECC \
    OCSP Responder - S1"
# Issuing CA - S1 - OCSP Response Pre-generation
CA_NAME=s1
CA_NAME_UPPER=`echo $CA_NAME | tr '[:lower:]' '[:upper:]'`
rm -r /etc/pki/${CA_NAME}/ocsp 2>/dev/null
mkdir -p /etc/pki/${CA_NAME}/ocsp
mkdir -p /etc/pki/ocsp
echo '{"driver":"sqlite3","data_source":"/etc/pki/'${CA_NAME}'/ocsp/certs.db"}' > \
    /etc/pki/${CA_NAME}/ocsp/ocsp.json
sqlite3 /etc/pki/${CA_NAME}/ocsp/certs.db <<EOF
  CREATE TABLE certificates (
    serial_number            blob NOT NULL,
    authority_key_identifier blob NOT NULL,
    ca_label                 blob,
    status                   blob NOT NULL,
    reason                   int,
    expiry                   timestamp,
    revoked_at               timestamp,
    pem                      blob NOT NULL,
    PRIMARY KEY(serial_number, authority_key_identifier)
  );
  CREATE TABLE ocsp_responses (
    serial_number            blob NOT NULL,
    authority_key_identifier blob NOT NULL,
    body                     blob NOT NULL,
    expiry                   timestamp,
    PRIMARY KEY(serial_number, authority_key_identifier),
    FOREIGN KEY(serial_number, authority_key_identifier) REFERENCES \
        certificates(serial_number, authority_key_identifier)
  );
EOF
OIFS="$IFS"
IFS=$'\n'
for cert in `ls -1 /etc/pki/${CA_NAME}/issued`; do
  serial_number=`echo "ibase=16;obase=A;\`openssl x509 -serial -noout -in \
      /etc/pki/${CA_NAME}/issued/${cert} | sed s/.*=//\`" | bc`
  authority_key_identifier=`openssl x509 -text -in /etc/pki/${CA_NAME}/issued/${cert} \
      | grep keyid: | sed s/.*keyid// | sed s/://g | tr [:upper:] [:lower:]`
  status="good"
  reason="0"
  expiry=`date --rfc-3339=seconds -d "\`openssl x509 -enddate -noout -in \
      /etc/pki/${CA_NAME}/issued/${cert} | sed s/.*=//\`"`
  revoked_at="0001-01-01 00:00:00+00:00"
  pem=`openssl x509 -in /etc/pki/${CA_NAME}/issued/${cert}`
  sqlite3 /etc/pki/${CA_NAME}/ocsp/certs.db "INSERT INTO \
      certificates(serial_number,authority_key_identifier,ca_label,status,reason,\
      expiry,revoked_at,pem) VALUES('$serial_number','$authority_key_identifier','',\
      '$status','$reason','$expiry','$revoked_at','$pem')"
done
IFS="$OIFS"
openssl pkey -in "/etc/pki/${CA_NAME}/private/lspeed ECC OCSP Responder - \
    ${CA_NAME_UPPER}.key" -out /etc/pki/${CA_NAME}/ocsp/ocsp.key
cp "/etc/pki/${CA_NAME}/issued/lspeed ECC OCSP Responder - ${CA_NAME_UPPER}.crt" \
    /etc/pki/${CA_NAME}/ocsp/ocsp.crt
TZ=UTC faketime `date +%Y-%m-01` cfssl ocsprefresh -db-config \
    /etc/pki/${CA_NAME}/ocsp/ocsp.json -responder /etc/pki/${CA_NAME}/ocsp/ocsp.crt \
    -responder-key /etc/pki/${CA_NAME}/ocsp/ocsp.key -ca /etc/pki/${CA_NAME}/ca.crt \
    -interval 1800h
cfssl ocspdump -db-config /etc/pki/${CA_NAME}/ocsp/ocsp.json > \
    /etc/pki/ocsp/ocsp_${CA_NAME}.dump
chmod 600 /etc/pki/ocsp/ocsp_${CA_NAME}.dump
rm -r /etc/pki/${CA_NAME}/ocsp

This process is easily repeatable for other issuing CAs.

Publishing CDP and AIA locations

During the setup of the CAs, we have been placing DER-encoded CA certificates and CRLs into the /etc/pki/pub/ directory. This needs to be made available over HTTP so that clients can access the CDP and AIA locations. In my case this is not made available over the internet, as I don’t use any certificates issued by this PKI for internet-facing services (that’s what I use Let’s Encrypt for). I also serve the CPS document from the same web server.

I opted to use NGINX to serve these static files, although any web server should work. I’ll not bother covering the web server configuration in detail here as it is trivial and off-topic. Except I will mention two things…

  • I configured the CRL files to expire in 1 day (ie from an HTTP content perspective), so that they won’t be cached for too long
  • I changed the MIME type used for serving certificates (.der, .pem, and .crt extensions) to be “application/x-x509-ca-cert”

OCSP responder

I have opted to use CFSSL’s OCSP responder. One instance is required for each CA. I am using the following init script to run the OCSP responder daemons (note that my Gentoo systems are using openrc rather than systemd):

#!/sbin/runscript

depend() {
    after net
}

start() {
    ebegin "Starting ocspserve"
    start-stop-daemon --start --background --exec /usr/bin/cfssl -- ocspserve -port \
        8000 -responses /etc/pki/ocsp/ocsp_r1.dump
    start-stop-daemon --start --background --exec /usr/bin/cfssl -- ocspserve -port \
        8001 -responses /etc/pki/ocsp/ocsp_s1.dump
    eend $?
}

stop() {
    start-stop-daemon --stop --name cfssl
    eend $?
}

Once the OCSP responder daemons are up and running, this additional config can be added to NGINX to pass OCSP requests through to the appropriate responder. I’m sure something similar can be achieved if you are using a different web server:

location /ocsp/r1 {
        proxy_pass http://127.0.0.1:8000;
}
location /ocsp/s1 {
        proxy_pass http://127.0.0.1:8001;
}

Issuing end-entity certificates

With all the initial setup complete, issuing new end-entity certificates is fairly straightforward. Here’s an example for a web server certificate (to allow HTTPS access to the CPS):

# Issuing CA - S1 - PKI Web Server Certificate
CA_NAME=s1
EASYRSA=/usr/share/easy-rsa /bin/sh /usr/share/easy-rsa/easyrsa --batch \
    --pki-dir=/etc/pki/${CA_NAME} --use-algo=ec --curve=secp384r1 --dn-mode=org \
    --req-c=GB --req-st= --req-city= --req-ou= --req-email= --req-org=lspeed \
    --req-cn="PKI Web Server" gen-req "PKI Web Server"
CA_NAME=${CA_NAME} EASYRSA_EXTRA_EXTS=$'subjectAltName = DNS:pki.lspeed.org\n[ \
    cpStandard ]\npolicyIdentifier = 1.3.6.1.4.1.51546.1.1.1.1\nCPS = \
    "https://pki.lspeed.org/";' EASYRSA_EXT_DIR=/etc/pki/x509-types \
    EASYRSA=/usr/share/easy-rsa faketime -f "`date --rfc-3339=seconds -d \`date +%F\` \
    | sed s/\+.*//`" /bin/sh /usr/share/easy-rsa/easyrsa --batch \
    --pki-dir=/etc/pki/${CA_NAME} --use-algo=ec --curve=secp384r1 --digest=sha384 \
    --days=3650 sign-req server "PKI Web Server"

Note that at this point it is also necessary to regenerate the OCSP responses for the issuing CA that you used to issue the new certificate (otherwise no OCSP response would be available for that new certificate). Just re-run the same commands as were used to generate the initial set of OCSP responses for that same CA (shown above).

Certificate revocation

A certificate can be revoked as follows (I’m revoking the same end-entity certificate that was just issued above for this example):

CA_NAME=s1
EASYRSA=/usr/share/easy-rsa /bin/sh /usr/share/easy-rsa/easyrsa \
    --pki-dir=/etc/pki/${CA_NAME} revoke "PKI Web Server"

After revoking a certificate, it is necessary to regenerate CRLs and OCSP responses for the certificate’s issuing CA. Again, that’s just a case of re-running the same commands that were used to generate the initial CRLs and OCSP responses in the first place.

Automation

The CRLs need to be rotated once per month, and fresh OCSP responses need to be generated for each CA too. CRL rotation is simple… it just involves copying the new file into place at the right time:

#!/bin/sh
crldate=`date +%Y-%m-01`
crlnumber=`date -I | sed s/-//g`
for CA_NAME in `ls /etc/pki/*/ca.crt | sed s,/etc/pki/,, | sed s,/ca.crt,,`; do
  cp /etc/pki/${CA_NAME}/crls/${crldate}.crl /etc/pki/pub/lspeed_${CA_NAME}.crl
  openssl crl -inform DER -in /etc/pki/${CA_NAME}/crls/${crldate}.crl -outform PEM > \
      /etc/pki/${CA_NAME}/crl.pem
done

OCSP response regeneration is a case of re-running the same commands as shown above for generating the initial responses.

Both these things can be run from cron on a monthly basis.

High Availability

Although the CAs themselves don’t need to be highly-available (at least in my small environment), the CDP and OCSP responder are more important, and would benefit from increased resiliency.

This is easily achieved by replicating the contents of the /etc/pki/pub/ and /etc/pki/ocsp/ directories to some additional web servers, and then fronting them with a load-balancer of some kind (I have done this using HAproxy).

ioquake3 / Quake3e / OSP / CPMA / CNQ3

Since our little boy arrived 8 months ago, opportunities to spend time gaming have become extremely scarce. So I find myself gravitating back towards Quake 3 Arena, rather than the single-player FPS games I’ve mostly been playing over the past 15 years or so, as a quick game of Quake 3 is typically short enough that it’s actually possible to fit one in occasionally. I should perhaps add that I have no expectation of playing online – I just play against nightmare bots nowadays.

My Q3 install has remained essentially untouched for years… ioquake3 with the OSP mod, and a config that I spent ages tweaking a long time ago. However I have now realised that I am somewhat out of touch – it seems that CPMA superseded OSP in popularity not long after I stopped playing competitively, and there are various other improved engines available besides ioquake3 now. Specifically, CPMA’s own CNQ3 engine, and also Quake3e. So I am trying to figure out whether it makes sense to ā€œmoderniseā€ my own installation or not.

Having tried out the various options, I’m finding that Quake3e adds numerous enhancements whilst remaining faithful to the base game, so is an obvious ā€œwinā€ (except for one minor annoyance, mentioned later). However I am not yet convinced by CPMA (even when using VQ3 mode). It just feels ā€œtoo differentā€, not least in terms of the UI. Given my current lack of free time, I also don’t fancy spending hours re-tweaking my config (which looks like it would be necessary for CPMA, but not if I continue with OSP on the Quake3e engine). But perhaps those changes in CPMA are in fact worthwhile improvements, and I should take the time to adapt. I am undecided…

One minor annoyance that I noticed when switching from ioquake3 to Quake3e (at least on Windows) is that the latter expects to have write access to the directory in which the game is installed, in order to write per-user config files there. This forced me to relocate the entire game into my home directory. Whereas ioquake3 could be installed into a system-wide location (eg under ā€œProgram Filesā€), and yet read/write per-user files such as configs from each user’s home directory. That seems so much more sensible, so the backwards step when switching to Quake3e is frustrating.

Having said that, I have settled on a workaround… I used Process Monitor to determine that Quake3e attempts to load .pk3 files from the appropriate location for a Steam installation of Quake 3 Arena by default. Perhaps the Windows binaries are compiled with that as their default fs_basepath (I’ve not looked at the code). While I could try setting fs_basepath to some other value myself, I am happy to use Steam as the system-wide location for pak0.pk3 – pak8.pk3. Then the Quake3e binaries, mods, and config files all go in my user profile. Even though it is still writing changes to the same location as the binary, rather than beneath %appdata%, this feels like a good compromise as the large .pk3 files no longer need to be in my user profile.

I also have two installations of Quake 3 Arena on Gentoo linux: one client, and one dedicated server. Rather than use the pre-compiled binaries directly I opted to write an ebuild to compile Quake3e from source (based on the ebuild for ioquake3 before it was pulled from the portage tree for some reason a year or so ago). I encountered a similar issue as on Windows whereby the build was ignoring the value of DEFAULT_BASEDIR as passed to the Makefile, so it wasn’t picking up the correct location of the .pk3 files. My workaround was to write a wrapper script to launch the game which sets the value of fs_basepath:

$ cat /usr/bin/games/quake3
#!/bin/sh
exec quake3e.x64 +set fs_basepath "/usr/share/games/quake3" "$@"

Welcome!

So I finally decided that the time has come to reinstate my website, having taking the previous incarnation offline around 7 years ago or so. The previous site had been built using Joomla (originally Mambo), but this time around it seemed like an easy decision to go with WordPress, given its popularity.

For now I’m deliberately keeping the design very spartan, although I will no doubt customise it further in the future. But I would rather get it back online first than worry about making it look perfect.

For the most part I don’t plan to import any content from the previous site (to be honest there wasn’t a great deal on there anyway). Although it’s possible I will resurrect my Deus Ex walk-through at some point.