Roll-Your-Own X.509 CA


When I first encountered X.509 Certificates, they were big, scary, expensive things that only e-commerce sites, bank, and paranoid people wanted. They were so expensive, and so complicated, in fact, that architectures often centralized their usage, so you may have,, and for the regular traffic, but the secure traffic was forced through (because that's what you paid to get in the certificate!)

Nowadays, with things like Let's Encrypt, you can pretty much get a certificate for free, with only minimal effort expended to prove you are who the certificate says you are.

But this isn't a post about Let's Encrypt.

This is a post about how to handle all those pesky systems that don't natively implement ACME. Systems that need / want X.509 certificates, but expect you to do all the legwork in getting one.


Here's the thing: generating X.509 certificates is hard. Google around for a bit and you'll find a hundred different ways to invoke openssl, different ways of setting up a transient CA directory, and different ways of generating the signing key (some of which even use 1024-bits ferchrissakes!) Most of the time, these processes pop out a self-signed certificate that's good for a year. If you're lucky, the certificate has Subject Alt Names (SANs).

Judicious use of self-signed certificates leads to "browser exception fatigue" (a real medical malady, I assure you). Try as you might to only temporarily set an exception just for this one session each and every time, one day you're going to forget and not uncheck the bad little checkbox.

Ideally, you run your own Certificate Authority.

Yeah, you heard me. You can run your own Certificate Authority. It's not that hard, and if you bear with me here, I'll show you exactly what to do to keep it as safe and secure as I know how.

On Matters of Authority

Contrary to what you may have heard, you can run your own Certificate Authority. When you get right down to it, a CA is nothing but a key and a promise. The key is used to sign certificates that vouch for identities, and the promise is "I will vet the people and systems I sign certificates for."

The one thing you won't easily be able to do is get your CA certificate loaded into every browser and operating system in marginal use today. That's where Verisign, GeoTrust, DigiCert, Thawte, GoDaddy, Network Solutions, et al have you beat -- they are already trusted by the browsers and the OS you're using.

But the difference between logging security exceptions for self-signed certificates (that you generate) and running your own certificate authority is twofold. In the best case, you can just install your CA certificate in the browsers trusted CA store, or teach your OS how to trust new CAs. In the worst case, you're doing the exact same amount of work to ignore security warnings!

On Matters of Encryption


The key here is encryption. And yes, I did think about editing out that accidental pun. But I didn't. You're welcome.

Encryption, encryption, encryption.

What we need is a system that can keep our CA signing key secret, and only bring it out when we need to use it. Luckily, there's a freely available, rock-solid, open source solution that works on every major operating system I'd want to run on: Vault.


Into The Vault Of Secrets



From the man page:


This is important, because Vault is a long-running process, and it will be handling secrets directly. With mlock(2), we can rest assured that the memory management unit (a critical part of any modern OS) isn't going to barge in and copy sensitive memory to an unencrypted swap file somewhere.

(Note: Vault does have an option to turn off its mlock behaviors, but they don't recommend doing so and neither do I.)

So, Linux it is.


$ mkdir -p ~/.cavault/data
$ sudo chown -R jrhunt:root ~/.cavault


$ curl -Lo ~/.cavault/ \
$ (cd ~/bin && unzip && rm


$ sudo setcap cap_ipc_lock=+ep ~/bin/vault

Configuring Vault is pretty straightforward. I'm not going to enable too much functionality — Vault can do a lot more than what we're going to use it for. Here's a good starting configuration:

$ cat >~/.cavault/conf <<EOF
listener "tcp" {
  address         = ""
  tls_cert_file   = "$HOME/.cavault/tls.pem"
  tls_key_file    = "$HOME/.cavault/tls.key"
  tls_min_version = "tls12"
storage "file" {
  path = "$HOME/.cavault/data"


You may notice that I snuck another X.509 certificate and key in there. It's turtles, all the way down. For my specific setup, I reused a wildcard certificate that was valid for another domain. If you want (and if you're only running over loopback) you can specify tls_disable = 1 and run over HTTP until you can generate a certificate using safe+vault. So meta.

Next, let's write a small script to run the Vault, since remembering command-line arguments and paths is annoying.

$ cat ~/bin/ca <<EOF
exec vault server -config ~/.cavault/conf
$ chmod 755 ~/bin/ca

All that's left to do now is to fire it up.

$ ca
==> Vault server configuration:

        Cgo: disabled
  Log Level: info
      Mlock: supported: true, enabled: true
    Storage: file
    Version: Vault v0.9.0
Version Sha: bdac1854478538052ba5b7ec9a9ec688d35a3335

When you start Vault up the first time, it will be in an uninitialized state. You won't be able to read secrets from it or put secrets into it. To initialize the Vault, let's use safe:

$ safe target ca
Now targeting ca at

$ safe vault status
Error checking seal status: Error making API request.

Code: 400. Errors:

* server is not yet initialized
!! exit status 1

$ safe vault init
Unseal Key 1: wXTO7wdmF0TVfqivzw6OaJiVeC+QNacAanTsxQ1RlKad
Unseal Key 2: QQugZ0RmflcMzHTLQCt1StP3XXGiCMWOabhWkXvuDMUS
Unseal Key 3: 1jzeotmjwLahW7s8zhvJTs/npVgBVwjS5cAYgRjBJf9x
Unseal Key 4: cN+wF2qw3xliLwxzU6t+/DnYtBoW2FC1RYom/NEI4XNj
Unseal Key 5: E0Q/IRJr9RiWNW3kR6uvSSo1ehZ8AOO0+jvugQQgy1Q4

to unseal it again.

your vault will remain permanently sealed.

Your unseal keys and initial root token will be different.

Now the Vault is initialized, but it is still sealed. You'll need to run safe vault unseal three times (turning widdershins optional), supplying a different unseal key each time.

$ safe vault unseal
Key (will be hidden):

$ safe vault unseal
Key (will be hidden):

$ safe vault unseal
Key (will be hidden):

Finally, we can authenticate with our root token:

$ safe auth token
Authenticating against ca at
Token: <hidden>

The Joys Of Using safe

The first thing I do on any Vault I administer is to set a "canary" value:

$ safe set secret/handshake knock=knock
knock: knock

$ safe tree
└── secret
    └── handshake

safe tree is just one of the handy little commands that safe provides. The one we'll be most interested in for this project is safe x509 — there's a whole subsystem for generating CAs, issuing certificates, managing CRLs, etc!


$ safe x509 issue --ca \
                  --name \
                  --bits 4096 \
                  --subject / \
                  --ttl 10y \



The --bits flag is important — it sets the RSA private key strength, in bits. Valid values are 1024 (highly discouraged, if not outright broken), 2048 (okay for leaf certs) and 4096 (super-strong). I'll take the strongest my CPUs can handle, thank you very much!




The --ttl flag determines how long the CA certificate is good for. I'd like a decade or so before I have to rotate out my certs, but you are welcome to choose a shorter time-to-live.


If all goes well, your laptop will crank on the RSA key generation phase for a bit and then you'll be back at your command prompt.

$ safe tree
└── secret
    ├── handshake
        └── ca

You can use safe read to see what was generated:

$ safe read secret/
--- # secret/
certificate: |
combined: |
crl: |
key: |
serial: "4"

The following keys are stored:

  1. certificate - The PEM-encoded CA certificate. This is what you will need to import into your browser / OS.
  2. combined - The public certificate plus the private key. You won't need this.
  3. crl - A certificate revocation list, managed by safe x509 revoke
  4. key - The private CA signing key, PEM-encoded. You won't ever need to use this, and you definitely shouldn't export it to disk.

So let's pull that CA certificate out and put it on-disk:

$ safe read secret/ > ca.pem

Easy. You can now import that certificate into your browser or your OS. That's too broad a topic to cover here, but here are some helpful links:

Congratulations, you have a working certificate authority.

Issuing Some Certificates


$ safe x509 issue \
    --signed-by secret/ \
    --name \
    --name * \
    --name \
    --ttl 90d \

The chief difference here is that we specified --signed-by as the path to our CA in the Vault. That's all safe x509 needs to know in order to properly sign your new certificate.

Another new thing is that we specified --name multiple times. You can have as many subject alternative names as you need (within reason), and they can be regular names like, wildcards like *, IP addresses like, and even email addresses (not shown).


$ safe x509 show secret/
  issued by:

  expires in a 89 days
  for the following names:
    - (DNS)
    - * (DNS)
    - (IP)




Otherwise, the key and certificate are available separately in the key and certificate attributes, respectively.

Next Steps



Happy Hacking!



Currently working on Rook.