Advertisement
Open Source Projects by Phil Schwartz

Building a Python Tool for Local X.509 Certificate Generation

Setting up HTTPS in a local development environment has been a recurring headache for years. Browsers refuse to trust self-signed certificates, curl floods the terminal with red warnings, and every new team member wastes an afternoon clicking through scary dialog boxes. After too many fights with OpenSSL one-liners I could never remember, I finally wrote a small Python utility to handle the whole job from scratch.

The script generates a private root authority, mints leaf certificates for local services, writes them out as PEM files, and optionally installs the root into the macOS or Linux trust store. The rest of this write-up walks through how I built it, the design choices behind it, and the gotchas that bit me along the way.

The Frustration of Self-Signed Certificates

Anyone who has tested a webhook against a locally running service knows the pain. Browsers display the "Your connection is not private" page and refuse to remember the exception, and new starters waste half a day before they can hit a single endpoint.

The deeper issue is that modern browsers ignore the commonName field and look only at the Subject Alternative Name extension. A certificate generated with openssl req -new -x509 is technically invalid for HTTPS, even when it sits in the trust store. I discovered this while debugging an integration with an Australian fintech API that rejected every call from my local environment, even though the certificate looked fine in OpenSSL's output.

Designing a Sensible Command-Line Interface

I wanted the tool to feel familiar, so I modelled the interface on git and kubectl, with subcommands like init, issue, and trust. Typing certutil init my-project creates a root, while certutil issue api.localhost mints a leaf signed by that root. Australian developer meetups in Sydney and Melbourne often point out that small CLI tools survive only when their flags stay predictable, so I kept the surface area tight: an output directory flag, a validity period flag measured in days, and an optional Common Name override.

Help text comes from Python's argparse with a thin wrapper that colours the usage line when the terminal supports it. I considered click or typer, but for a utility this small the standard library does the job without dragging in another dependency.

Flags Worth Knowing

Generating the Root Authority

The root certificate is the foundation of the whole system, so I spent most of my time here. The cryptography library exposes a clean set of builders, and I leaned on x509.CertificateBuilder() to construct the certificate in code rather than shelling out to OpenSSL. Each root gets a serial number drawn from secrets.randbits(159), a validity period of ten years, and basic constraints marked critical=True with CA=True.

Storing the private key matters more than the certificate itself. I write it to disk with mode 0600 inside a directory set to 0700, and I refuse to encrypt it with a passphrase because the point of a local development root is to be invisible.

Issuing Leaf Certificates

Once the root exists, issuing a leaf is mostly a matter of assembling the right extensions. Subject Alternative Name gets a DNS entry plus an IP entry for 127.0.0.1, and I throw in localhost for good measure. Extended Key Usage is set to serverAuth and clientAuth so the same certificate works for both inbound and outbound calls, which is handy when one microservice talks to another over mTLS inside a docker-compose stack.

The signing step uses the root's private key through root_key.sign(...) with SHA-256 and RSA-PSS padding, mirroring recommendations published by the Australian Cyber Security Centre's Information Security Manual. Validity defaults to 365 days: long enough that nobody is regenerating certs every Friday, short enough that nothing stays trusted forever.

Files Produced by a Typical Run

Installing the Root into the System Trust Store

A certificate nobody trusts is worthless, so the trust subcommand copies the root PEM into the operating system's trust store. On macOS it runs security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain, on Debian it writes to /usr/local/share/ca-certificates/ and triggers update-ca-certificates, and on Fedora it copies into /etc/pki/ca-trust/source/anchors/ and runs update-ca-trust.

I added a dry-run flag because nothing kills an arvo faster than accidentally pushing your development root onto a production machine. The dry-run mode prints the exact commands it would have run and exits non-zero if the operating system is not one of the supported families. Windows support is on the roadmap, though I have not needed it on my current Brisbane-based project.

Wiring It Into Docker and Continuous Integration

The real payoff came when I plugged the tool into the team's workflow. A Makefile target rebuilds the root and leaf certs whenever the hostname list changes, a docker-compose volume mounts the output directory into each container, and a GitHub Actions step regenerates everything before the test suite runs. Tests that once took forty seconds of certificate warnings now start quietly.

A flag that prints the SHA-256 fingerprint of the root lets the CI pipeline write to a .well-known/pinned-ca file so integration tests can assert they are talking to the certificate they expect. It catches the moment a teammate re-initialises their local root and forgets to tell anyone.

Lessons Learnt and Where It Goes Next

A few things caught me out. The first was that Python's cryptography library will happily let you build a certificate that browsers reject, because it does not enforce every rule a real CA must follow. Testing against a real Chrome instance early would have saved me evenings staring at ERR_CERT_COMMON_NAME_INVALID. The second was that rotating a root certificate is far more painful than rotating a leaf, so it pays to choose a sensible organisational unit name from the start.

I am looking at OCSP responder support and a small web interface so non-engineers can mint certificates without the terminal. Porting the trust store installer to Windows is also on the cards. The utility has already replaced the bundle of shell scripts we used to carry around, and mornings in front of the coffee machine are a little less chaotic.