For your PKI needs, this is another Docker image to ease cfssl usage.
The image is Alpine based in order to reduce its footprint.
WARNING: All the following reflects my understanding of TLS and the cfssl API which might not be accurate as I am NOT a security expert. Do not hesitate to add issues or propose PRs to fix any inaccuracies.
First thing to do is to get the cfssl executable:
curl -LO https://github.com/gautaz/docker-alpine-cfssl/raw/master/bin/cfssl
chmod u+x cfssl
Then add the cfssl
bash script location to your PATH
.
You may use the Docker image directly if bash is not available on your platform by issuing:
docker run --rm -i -v "${PWD}:/home/cfssl" "${dockerargs[@]}" gautaz/alpine-cfssl "${cfsslargs[@]}"
Where dockerargs
and cfsslargs
are additional arguments to pass to Docker run and the instance entry point.
But operating this way can be rather daunting.
Simply issue cfssl
on the command line to get the general help page.
Basically cfssl
will give you access to a set of commands provided by cfssl.
The general syntax is cfssl <command> [option...]
.
In order to get help for a particular command simply issue cfssl <command> -h
where <command>
is the command you want help for.
The cfssl
script is designed to enable piping which is necessary to operate part of the flow needed to operate a PKI with cfssl.
In particular some cfssl commands may output JSON content that is to be used by other commands.
However this script will not behave well when the goal is to operate a daemon.
Once all of your PKI environment has been built up, you might want to use the image directly with Docker Compose without using the cfssl
shell script.
A few steps are generally needed in order to obtain a fully working PKI with cfssl:
- creating a root certificate authority (CA);
- optionally creating an intermediate CA;
- starting cfssl as a service;
- querying the cfssl API to manage certificates.
Having cfssl
available, operate the following commands in a test folder.
Create a certificate signing request (CSR) JSON configuration file:
cfssl print-defaults csr > ca-csr.json
This will create a default CSR configuration that you might want to modify in order to fit your needs. Once you have edited the file, you can generate everything needed to operate your own CA:
cfssl gencert -initca ca-csr.json | cfssl json -bare ca
If you did not want to modify the default values, you could have simply issued:
cfssl print-defaults csr | cfssl gencert -initca - | cfssl json -bare ca
This will create three additional files:
ca.csr
: a PEM formatted file containing the CSR for your CA;ca-key.pem
: a PEM formatted file containing the private key of your CA;ca.pem
: a PEM formatted file containing the (self-)signed certificate of your CA.
Then run the cfssl service which will answer your API calls:
cfssl serve -ca-key=ca-key.pem -ca=ca.pem -address=0.0.0.0 -- -p 8888:8888
In order to stop this instance, you will have to issue
docker stop <instance name>
(<ctrl-c>
will not work). Usedocker ps
to find the name of the running instance.
You can test the service by asking for a new certificate and saving data to PEM formatted files:
curl -X POST -d '{"request":{"CN":"","hosts":[""],"key":{"algo":"rsa","size":2048},"names":[{"C":"","ST":"","L":"","O":""}]}}' http://localhost:8888/api/v1/cfssl/newcert | cfssl json server
You can then launch an OpenSSL server using this certificate:
openssl s_server -key server-key.pem -cert server.pem -accept 4433
And check that an OpenSSL client will connect to this server by trusting the CA:
openssl s_client -connect localhost:4433 -CAfile ca.pem
The following uses the same CA for both client and server certificates but different CAs can be used.
Based on the previous section, you can also generate a client certificate:
curl -X POST -d '{"request":{"CN":"","hosts":[""],"key":{"algo":"rsa","size":2048},"names":[{"C":"","ST":"","L":"","O":""}]}}' http://localhost:8888/api/v1/cfssl/newcert | cfssl json client
You can then launch an OpenSSL server using this certificate and trusting client certificates emitted by the common CA:
openssl s_server -key server-key.pem -cert server.pem -accept 4433 -Verify 0 -CAfile ca.pem
Then check that an OpenSSL client will connect to this server by trusting the CA and using the previously created client certificate:
openssl s_client -connect localhost:4433 -CAfile ca.pem -key client-key.pem -cert client.pem
Depending on what the certificate is intended for, different signing profiles might be used.
This can be detailed in the cfssl serve
configuration file, a default configuration can easily be obtained:
cfssl print-defaults config > ca-config.json
Then you can start the API server by passing it this configuration file:
cfssl serve -config=ca-config.json -ca-key=ca-key.pem -ca=ca.pem -address=0.0.0.0 -- -p 8888:8888
Obtaining a new certificate now also means providing the signing profile to use in the request::
curl -X POST -d '{"request":{"CN":"","hosts":[""],"key":{"algo":"rsa","size":2048},"names":[{"C":"","ST":"","L":"","O":""}]},"profile":"client"}' http://localhost:8888/api/v1/cfssl/newcert
The main goal of an intermediate CA is to avoid exposing the private key of your root CA on a "less trusted" site. On this site, the intermediate CA will be used to deliver new certificates instead of using the root CA. Thus, the intermediate CA private key is used on this latter site and the intermediate CA can be revoked in case of a leakage.
The root CA keeps being the certificate that is trusted by clients, hence the use of certificate bundles when using a certificate delivered by the intermediate CA.
This bundle contains both the newly delivered certificate and the intermediate CA certificate.
When receiving such a bundle the client can authenticate it by using the following chain of trust (->
means "trusts" or "certifies"):
client -> root CA -> intermediate CA -> new certificate
For this to work, you first need to create an intermediate CA signed by the root CA.
Again, you need a root CA:
cfssl print-defaults csr | cfssl gencert -initca - | cfssl json -bare ca
In order to create an intermediate CA, you will need a CSR:
# this is a "it just works" CSR, do not use it for production purpose
echo '{"CN": "Intermediate CA"}' > ica-csr.json
You also need a specific signing profile to create intermediate CAs, save the following in ica-config.json
:
{
"signing": {
"profiles": {
"intermediate": {
"expiry": "8760h",
"usages": ["signing", "key encipherment", "cert sign", "crl sign"],
"ca_constraint": {"is_ca": true, "max_path_len":1}
}
}
}
}
Create the intermediate CA:
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ica-config.json -profile=intermediate ica-csr.json | cfssl json -bare ica
A warning will be emitted by
cfssl
due to the fact that the certificate of the intermediate CA lacks ahosts
field. You can simply ignore this warning, this certificate will not in fact be used to authentify a website server.
You can now move the ca-key.pem
file somewhere deep in a safe place secured by your own goblins and trolls.
Next step is to start a cfssl
server instance using this intermediate CA:
cfssl serve -ca-key=ica-key.pem -ca=ica.pem -ca-bundle=ca.pem -int-bundle=ica.pem -address=0.0.0.0 -- -p 8888:8888
Note that both options
-ca-bundle
and-int-bundle
are used and that in this particular example:
- only one root CA is known of and is part of the root CA bundle;
- only one intermediate CA is known of and is part of the intermediate CA bundle.
Thus
ca.pem
andica.pem
are directly used as certificate bundles both containing only one certificate.
This cfssl
instance is able to:
- deliver new certificates based on the intermediate CA due to the use of
-ca-key
and-ca
options; - bundle certificates by knowing all root and intermediate certificates available (through bundle options).
Root certificates will not be included in the bundles produced by
cfssl
as they are supposed to be trusted by the other party during the TLS handshake. Intermediate certificates will be added to the produced bundles in order to complete the certificate chain up to an intermediate certificate that is signed by a root certificate.
You can then generate a server certificate and its bundle with the following command:
curl -X POST -d "$(curl -X POST -d '{"request":{"CN":"localhost","hosts":[""],"key":{"algo":"rsa","size":2048},"names":[{"C":"","ST":"","L":"","O":""}]}}' http://localhost:8888/api/v1/cfssl/newcert | tee >(cfssl json server) | jq -M '.result.certificate | {certificate: .}')" http://localhost:8888/api/v1/cfssl/bundle | jq -M '.result.bundle | {certificate: .}' | cfssl json -bare server-bundle
This command is a bit intricate, let's break it down:
- the
newcert
API is used to generate a new certificate;- the output is forked with
tee
and passed tocfssl json
to write the certificate and its key respectively toserver.pem
andserver-key.pem
;- the same output is passed to the jq command to generate a
bundle
API request ({certificate: <new certificate>}
);- the
bundle
API ingests this request, its JSON result is again processed by jq and written in the fileserver-bundle.pem
.
Four files result from this command:
server.pem
: the new certificate;server-key.pem
: the private key associated with this new certificate;server.csr
: the CSR used to generate the new certificate;server-bundle.pem
: the new certificate bundle.
The server-bundle.pem
contains in fact two certificates:
- first the newly created certificate which is signed by the intermediate certificate;
- then the intermediate certificate which is signed by the root certificate.
The order matters in order to comply with the section 7.4.2 of RFC 5246.
The OpenSSL s_server
command seems unable to use a certificate bundle (or at least I did not find a way to do so).
Instead you can use this simple Python script (save it in https.py
):
import BaseHTTPServer, SimpleHTTPServer
import ssl
httpd = BaseHTTPServer.HTTPServer(('localhost', 4433), SimpleHTTPServer.SimpleHTTPRequestHandler)
httpd.socket = ssl.wrap_socket (httpd.socket, certfile='./server-bundle.pem', keyfile='./server-key.pem', server_side=True)
httpd.serve_forever()
You can then launch the Python HTTPS server using the certificate bundle:
python https.py
And finally check that an OpenSSL client will connect successfully to this server by trusting the CA:
openssl s_client -connect localhost:4433 -CAfile ca.pem