WSL

I have really been enjoying WSL lately, and felt it was worth writing a quick article about some of the opportunities it opens up.

Clearly, choice of operating system is very much a personal preference, and it will (should) also vary according to the use-case. And although I appreciate that many people will want to do things differently, there are probably also many others whose preferences are similar to mine.

I am most comfortable using Windows on my end-user PC. However, for many other tasks I find other platforms are either more suitable or even an explicit requirement – which could include various flavours of linux, OpenBSD, or proprietary networking platforms such as Cisco IOS-XE.

For production workloads, it clearly makes sense to run the most appropriate platform either natively or in a dedicated virtualization environment of some kind. But for development, testing, and quick proof-of-concepts, it can be handy to just run everything on your PC.

Not only is WSL a very convenient way to make use of a linux environment from within a Windows PC, but it can be combined with qemu-kvm and nested virtualization to run other platforms inside Windows too. Of course it has long been possible to use virtualization software such as VMware Workstation, Oracle Virtualbox, or Microsoft Hyper-V to achieve exactly that. However another personal preference of mine is that I would rather use qemu & libvirt, and that’s where WSL comes in – making it possible to use my choice of hypervisor. I now regularly spin up OpenBSD VMs or Cisco C8000v instances within Windows using qemu, and it works great (so long as you’re not too bothered about performance). Oh and not to mention the fact that it is not necessary to stick to the linux distros that provide official images for WSL – I use gentoo as my main WSL environment.

Am I crazy to be building PoC network designs using Cisco and OpenBSD appliances running under qemu-kvm inside gentoo linux via WSL on Windows? Maybe, but I’m really liking this new level of flexibility and choice!

Desktop PC Upgrade

It’s about time to upgrade my desktop PC – as the core components all date back to mid-2016. I think it has managed to last that long because I haven’t had the opportunity to do much gaming in the last 2 years.

I intend to take my usual approach of just upgrading a few components at a time (on this occasion it’ll be the CPU (and cooler), motherboard, and perhaps RAM, but nothing else), and carrying forward the existing Windows installation.

For the record, the last time I bought an entire new desktop PC was back in 1998… it was an Intel Pentium 2 350 Mhz, with 64MB RAM, 8GB hard disk, 8MB nVidia Riva 128 graphics card, and 17″ CRT. Ever since then I have just replaced a couple of components at a time – so the current system is effectively an evolution of that original one. In addition, I have not performed a fresh install of Windows on this PC since 2007 (which was when I migrated from 32-bit Windows XP Pro to 64-bit Windows Vista Ultimate) – I have just carried forward the same Windows install to each new iteration of the hardware, and upgraded to newer Windows versions as they came out. That included migrating the OS from BIOS & MBR to UEFI & GPT, amongst other things. As an aside – I’d be interested to hear if anyone else has kept the same Windows install on their primary PC for a similar length of time (or longer), whilst keeping it up-to-date with the latest Windows versions (as opposed to just storing an ancient PC in the loft which still runs Windows 95, and never touching it!).

Anyway the current specs are as follows:

  • OS: Windows 11 Pro [since 2021]
  • Chassis: Lian Li X900B [since 2010]
  • CPU: Intel Core i7-6700K [since 2016]
  • Motherboard: Asus Z170-WS [since 2016]
  • RAM: 64GB (4×16) Corsair DDR4 2133 [since 2016]
  • Graphics: Asus nVidia GeForce GTX 1080 8GB [since 2017]
  • Storage: Samsung 980 Pro 1TB [since 2022]
  • PSU: Enermax Revolution 87+ 850W [since 2013]
  • Optical drive: LG GGC-H20L [since 2008]
  • Display: HP ZR30w @ 2560×1600 [since 2010]
  • & various peripherals / accessories

At the moment I am considering two main options. Both involve sticking with Intel, in part because I would be concerned about the difficulty of carrying forward the same Windows installation to an AMD platform.

It mainly comes down to RAM. I currently have 64GB, and am happy sticking with that amount. What I am unsure about is whether my existing DDR4 2133 RAM will function in a Z690 DDR4-based board (or if it is simply too slow). If it will function (albeit with a performance penalty versus buying faster RAM), then I would definitely consider re-using my existing RAM, and just getting a mid-range Z690 motherboard (costing around £200), plus the Core i7-12700KF – for a total cost of around £550.

On the other hand if I have to buy new RAM (which seems to be expensive nowadays), then I’m going to be looking at spending at least £1000 in total, in which case I’d feel more inclined to also spend a bit more on the other items (in other words by going for DDR5, moving up to the Core i9-12900KF, and getting a higher-end motherboard as well).

As far as the motherboard is concerned, there seem to be a lot of options to choose from, and it isn’t easy figuring out what is the best fit. Historically I have always favoured high-end motherboards, and typically those that are slightly more workstation-oriented than simply being gaming boards. For example I no longer have any desire to overclock, nor do I have much interest in multi-coloured LEDs or similar gimmicks, but I do value reliability, stability, performance, flexibility, and potential for future upgrades.

In particular I have been extremely happy with my current motherboard since I got it in 2016 – the Asus Z170-WS, which was marketed as a workstation board, and packed with features. Admittedly it was overkill for my needs in some respects (such as its support for quad-SLI… considering that I have never had more than a single graphics card installed in it). But in other respects I have made full use of its capabilities, or at least appreciated them. And at the time I purchased it I couldn’t necessarily have predicted that I wouldn’t want to get a second graphics card later, so I did value having that option. Having said that, the average price of a (high-end) motherboard appears to have roughly doubled since 2016, so it is becoming harder to justify continuing with that approach.

It looks like the most natural Z690-generation successor to the Asus Z170-WS is probably the Asus ProArt Z690-Creator Wifi. It definitely looks like a nice motherboard, and I am sure I would be happy with it, but given it costs around £500 I really ought to consider whether it’s worth that much more than, for example the MSI PRO Z690-A or Asus PRIME Z690-P D4 which both cost around £200. I do realise that that isn’t an apples-to-apples comparison, as the latter two support DDR4 RAM whereas the ProArt board supports DDR5, but as mentioned before if I can re-use my existing DDR4 RAM I’d pair it with a mid-range DDR4-based Z690 board, and if not then I’d go for DDR5 and a higher-end board, which is why I’m looking at these models.

Connectivity-wise, I have no desire to use wifi from this PC (my house is wired with Cat6a), but it would be useful to have bluetooth (and it looks like the two are coupled together). I used to care about having multiple on-board NICs (the Z170-WS has dual 1 Gbps interfaces), but I no longer consider that to be important. My current network switches have 1 Gbps interfaces, but it’s certainly possible that I will upgrade those to some which support 2.5 Gbps (or perhaps even 10 Gbps) within the potential lifetime of this new motherboard. But as most of the boards which offer 10 Gbps interfaces use a model I have never heard of, it’s likely I’d favour the use of the Intel 2.5 Gbps NIC even if the board had both and my switches supported both speeds. I do also value high-quality on-board audio.

Any advice on whether the more expensive motherboards (such as the Asus ProArt Z690 Creator Wifi) are actually worth it would be much appreciated. And even more so if anyone can say for sure whether my old DDR4 2133 RAM will function at all in a DDR4 Z690 board, that would be awesome!

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.