Serving CA certificates on the cheap


Introduction

This is a third post in the High Altitude Water Aerosols series with the aim to get “cloud native” work for internal (web-)services.

We already have a container service running on Alpine and a somewhat reasonable reverse proxy config.

Problem statement

Previously I also explained how to run one’s own root Certificate Authority, but without a web server publishing the CA certificate(s) to the internal net.

Obviously running some kind of a behemoth for an extremely low-traffic endpoint is… dumb.

Let’s do the smart (resource-wise) thing, then.

Road to solution

While initially researching this topic, I came across Florin Lipan’s article titled The smallest Docker image to serve static websites. And indeed, a binary weighing a few KB is exactly the kind of thing I’m looking for.

So with a few tweaks, this stuff should be easy. For realsies this time.

Solution

To get an appropriate Docker image, I expropriated and customized Florian’s original template, bumping version as I went along:

# Taken from https://lipanski.com/posts/smallest-docker-image-static-website

FROM alpine:3.18 AS builder

# Install all dependencies required for compiling busybox
RUN apk add gcc musl-dev make perl

# Download busybox sources
RUN wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2 \
  && tar xf busybox-1.36.1.tar.bz2 \
  && mv /busybox-1.36.1 /busybox

WORKDIR /busybox

# Copy the busybox build config (limited to httpd)
COPY busybox.config ./.config

# Compile and install busybox
RUN make && make install

# Create a non-root user to own the files and run our server
RUN adduser -D static

# Switch to the scratch image
FROM scratch

EXPOSE 3000

# Copy over the user
COPY --from=builder /etc/passwd /etc/passwd

# Copy the busybox static binary
COPY --from=builder /busybox/_install/bin/busybox /

# Use our non-root user
USER static
WORKDIR /home/static

# Customized httpd.conf
# see https://git.busybox.net/busybox/tree/networking/httpd.c for syntax
COPY httpd.conf .

# Copy the static website
COPY web web

# Run busybox httpd
CMD ["/busybox", "httpd", "-f", "-v", "-p", "3000", "-c", "httpd.conf"]

Obviously of interest are the busybox.config, httpd.conf and web subdirs.

The BusyBox config

The delta between my final busybox.config and config from make allnoconfig is as follows:

diff -U0 .config busybox.config
--- .config	2024-01-15 18:44:57.107966359 +0100
+++ busybox.config	2023-09-09 20:12:34.000000000 +0200
@@ -4 +4 @@
-# Mon Jan 15 18:44:57 2024
+# Sat Sep  9 20:10:01 2023
@@ -29 +29 @@
-# CONFIG_INSTALL_NO_USR is not set
+CONFIG_INSTALL_NO_USR=y
@@ -43 +43 @@
-# CONFIG_STATIC is not set
+CONFIG_STATIC=y
@@ -129 +129 @@
-# CONFIG_LOOP_CONFIGURE is not set
+CONFIG_LOOP_CONFIGURE=y
@@ -131 +131 @@
-CONFIG_TRY_LOOP_CONFIGURE=y
+# CONFIG_TRY_LOOP_CONFIGURE is not set
@@ -863,2 +863,2 @@
-# CONFIG_HTTPD is not set
-CONFIG_FEATURE_HTTPD_PORT_DEFAULT=0
+CONFIG_HTTPD=y
+CONFIG_FEATURE_HTTPD_PORT_DEFAULT=80
@@ -867 +867 @@
-# CONFIG_FEATURE_HTTPD_BASIC_AUTH is not set
+CONFIG_FEATURE_HTTPD_BASIC_AUTH=n
@@ -873,7 +873,7 @@
-# CONFIG_FEATURE_HTTPD_ERROR_PAGES is not set
-# CONFIG_FEATURE_HTTPD_PROXY is not set
-# CONFIG_FEATURE_HTTPD_GZIP is not set
-# CONFIG_FEATURE_HTTPD_ETAG is not set
-# CONFIG_FEATURE_HTTPD_LAST_MODIFIED is not set
-# CONFIG_FEATURE_HTTPD_DATE is not set
-# CONFIG_FEATURE_HTTPD_ACL_IP is not set
+CONFIG_FEATURE_HTTPD_ERROR_PAGES=y
+CONFIG_FEATURE_HTTPD_PROXY=y
+CONFIG_FEATURE_HTTPD_GZIP=n
+CONFIG_FEATURE_HTTPD_ETAG=y
+CONFIG_FEATURE_HTTPD_LAST_MODIFIED=y
+CONFIG_FEATURE_HTTPD_DATE=y
+CONFIG_FEATURE_HTTPD_ACL_IP=n
@@ -1126 +1126 @@
-# CONFIG_ASH_SLEEP is not set
+CONFIG_ASH_SLEEP=y

Not a lot of features needed, eh? I’m sure it could be trimmed further.

Btw, diff against Florin’s .config ended up:

diff -U0 .config busybox.config 
--- .config     2024-01-15 18:49:01.146905810 +0100
+++ busybox.config 2023-09-09 20:12:34.000000000 +0200
@@ -4 +4 @@
-# Mon Nov  6 11:52:15 2023
+# Sat Sep  9 20:10:01 2023
@@ -62 +62 @@
-# CONFIG_INSTALL_APPLET_SYMLINKS is not set
+CONFIG_INSTALL_APPLET_SYMLINKS=y
@@ -867 +867 @@
-CONFIG_FEATURE_HTTPD_BASIC_AUTH=y
+CONFIG_FEATURE_HTTPD_BASIC_AUTH=n
@@ -875 +875 @@
-CONFIG_FEATURE_HTTPD_GZIP=y
+CONFIG_FEATURE_HTTPD_GZIP=n
@@ -879 +879 @@
-CONFIG_FEATURE_HTTPD_ACL_IP=y
+CONFIG_FEATURE_HTTPD_ACL_IP=n
@@ -1126 +1126 @@
-# CONFIG_ASH_SLEEP is not set
+CONFIG_ASH_SLEEP=y

The httpd config

Next up is the heart of the “serving stack”1, the httpd.conf:

H:/home/static/web
I:index.html
E404:404.txt
.crt:application/x-x509-ca-cert

I think the only interesting part there is the .crt:... line that sets the correct mime-type application/x-x509-ca-cert so the browsers know what’s up when you point them on the .crt file.

The web subdirectory

Oh goodness, this one is super complicated :-D

# Copy the cert from somewhere
ruby -pe 'next unless /BEGIN/../END/' /path/to/ca-cert.crt > ca.crt

# Create fancy 404 page
echo "That ain't here." > 404.txt

# Create fancy index.html page
cat - > index.html <<'EOF'
<!DOCTYPE html>
<html lang="en">
 <head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>Wejn's Awesome Certification Authority</title>
 </head>

 <body>
  <main>
   <h1>Wejn's Awesome Certification Authority</h1>
   <p>You might be looking for <a href="ca.crt">the certificate</a>.</p>
  </main>
 </body>
</html>
EOF

Composer stanza

With all that stuff in an appropriate places:

$ ls -1
ca
compose.yml
sp
traefik

$ find ca
ca
ca/web
ca/web/404.txt
ca/web/ca.crt
ca/web/index.html
ca/httpd.conf
ca/Dockerfile
ca/busybox.config

all that’s left is the compose.yml stanza that spins this badboy up:


services:
  sp:
    # [... see previous post(s) ...]
  traefik:
    # [... see previous post(s) ...]
  ca:
    build: ca
    stop_signal: SIGKILL
    labels:
      - "traefik.enable=true"
    restart: always

Notice that the ca service re-uses the default traefik config that just spins it up as ca.int.wejn.org on both HTTP and HTTPs (no redirects).

Stats

The image ends up being ridiculous 173 kB (that’s all in, assets and all):

$ # podman images |grep _ca
localhost/int_ca               latest      51d2b095d782  4 months ago  173 kB

And runtime is hardly any bigger:

ps -o vsz,rss,comm,args | grep [b]usy
 324  120 busybox          /busybox httpd -f -v -p 3000 -c httpd.conf

So, yeah, I’d say 324 kB VSZ and 120 kB RSS is agréable. For a mostly idle (but nice to have around) service2.

Closing words

See, I said this one was easy, mostly due to the groundwork laid down by the previous posts.

I think I have one more post to wrap this series – one where I go “zero to hero” with some brand new internal (web-)service.

  1. Talk about big words.

  2. Still nowhere near the QNX demo level of awesome, where the entire frickin’ 32bit OS fit on 1.44MB floppy. But not too shabby either.