TLS Client Authentication for webservers

Instead of using basic auth for access restriction on websites with a predefined user base you can use TLS Client authentication.

This option requires the server to validate clients certificate while establishing a TLS connection. The usage of the known server-side TLS remains untouched, therefore you can use ACME provided certificates for your server and still validate client certificates you provided.

The procedure works like this: You create a CA, which is used to create certificates for your clients. The webserver needs your CA cert to validate any client connection.

Install cfssl

We will use cloudflares excellent tool cfssl to create our certificates.

go get -u github.com/cloudflare/cfssl/cmd/...

Create general config

This config encapsulates expiry dates and usages:

cat > ca-config.json <<EOF
{
  "signing": {
    "default": {
      "expiry": "168h"
    },
    "profiles": {
      "server": {
        "expiry": "26280h",
        "usages": [
          "signing",
          "key encipherment",
          "server auth"
        ]
      },
      "client": {
        "expiry": "26280h",
        "usages": [
          "signing",
          "key encipherment",
          "client auth"
        ]
      },
      "client-server": {
        "expiry": "26280h",
        "usages": [
          "signing",
          "key encipherment",
          "server auth",
          "client auth"
        ]
      }
    }
  }
}
EOF

1. Create CA

cat > ca-csr.json <<EOF
{
  "CN": "BLANG Root CA",
  "key": {
    "algo": "rsa",
    "size": 4096
  },
  "names": [
    {
      "C": "DE",
      "L": "Munich",
      "O": "BLANG",
      "OU": "CA",
      "ST": "Bavaria"
    }
  ]
}
EOF

cfssl gencert -initca ca-csr.json | cfssljson -bare ca

2. Create intermediate CA

cat > ca-intermediate-csr.json <<EOF
{
  "CN": "BLANG Intermediate CA",
  "key": {
    "algo": "rsa",
    "size": 4096
  },
  "names": [
    {
      "C": "DE",
      "L": "Munich",
      "O": "BLANG",
      "OU": "Intermediate CA",
      "ST": "Bavaria"
    }
  ]
}
EOF

cfssl gencert -initca ca-intermediate-csr.json | cfssljson -bare ca-intermediate

3. Sign intermediate CA with root CA

Create a signing config for intermediates which can only sign end-certificates (path_len=0):

cat > ca-root-to-intermediate.json <<EOF
{
	"signing": {
		"default": {
			"expiry": "43800h",
			"ca_constraint": {
				"is_ca": true,
				"max_path_len": 0,
				"max_path_len_zero": true
			},
			"usages": [
				"digital signature",
				"cert sign",
				"crl sign",
				"signing"
			]
		}
	}
}
EOF

Sign the intermediate using the CA:

cfssl sign \
    -ca ca.pem \
    -ca-key ca-key.pem \
    -config ca-root-to-intermediate.json \
    ca-intermediate.csr | cfssljson -bare ca-intermediate

4. Create CA Bundle

cat ca.pem ca-intermediate.pem > ca-bundle.pem

5. Client

cat > client1-csr.json <<EOF
{
  "CN": "client1",
  "hosts": [
    ""
  ],
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "DE",
      "L": "Munich",
      "O": "BLANG",
      "OU": "Client",
      "ST": "Bavaria"
    }
  ]
}
EOF


cfssl gencert -ca ./ca-intermediate.pem -ca-key ./ca-intermediate-key.pem -config ca-config.json -profile client client1-csr.json | cfssljson -bare client1

# Pack to pkcs12 format, the chain is provided by the server, this avoids warnings on android
openssl pkcs12 -export -out client1.p12 -inkey client1-key.pem -in client1.pem

# Alternative: Include the CAs
openssl pkcs12 -export -out client1.p12 -inkey client1-key.pem -in client1.pem -certfile ca-bundle.pem

6. Install the client PKCS12 cert

Chrome: Settings -> Manage certificates -> Import

7. Setup your webserver

Transfer the ca-bundle.pem to your server.

# Nginx
ssl_client_certificate /path/to/ca-bundle.pem;
ssl_verify_client on;

# Caddy with ACME (Let's encrypt)
tls [email protected] {
    clients /path/to/ca-bundle.pem
}

Optional: Check the certs

Check the certs extensions:

openssl x509 -in client1.pem -text -noout

For clients:

X509v3 extensions:
    X509v3 Key Usage: critical
        Digital Signature, Key Encipherment
    X509v3 Extended Key Usage:
        TLS Web Client Authentication

For CAs/Intermediate CAs:

X509v3 extensions:
    X509v3 Key Usage: critical
        Digital Signature, Certificate Sign, CRL Sign
    X509v3 Basic Constraints: critical
        CA:TRUE, pathlen:0

If pathlen:0 is set, this means this intermediate can only sign end-point certificates, not further intermediate CAs.

Optional: Server certificate

cat > server-csr.json <<EOF
{
  "CN": "iot.test.blang.io",
  "hosts": [
    "iot.test.blang.io"
  ],
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "DE",
      "L": "Munich",
      "O": "BLANG",
      "OU": "Server",
      "ST": "Bavaria"
    }
  ]
}
EOF

cfssl gencert -config ca-config.json -profile server -ca ./ca-intermediate.pem -ca-key ./ca-intermediate-key.pem server-csr.json | cfssljson -bare server