Building a Lightweight Certificate Authority

A primary concern in every network is security and far to often encrypting internal network traffic is a task that falls by the wayside as other tasks take greater priority. Usually this is seen in lab or development environments but it is also prevalent in production environments due to the complexity of managing a certificate authority.

CFSSL (https://github.com/cloudflare/cfssl) is a collection of open source PKI and TLS tools created by CloudFlare (https://www.cloudflare.com/) which known primarily for their website acceleration and protection services.

A key feature of the cfssl utility is it’s api server functionality that allows certificates to be generated via a REST API call and will return the certificate data in a JSON response.

A quick and simple way to get started is by using the Docker image created by CloudFlare that can be found on the DockerHub (https://hub.docker.com/r/cfssl/cfssl/). We’ll tweak the image to initialize the certificate authority and start the API server using the Dockerfile below. The Dockerfile below initializes the CA with very generic settings which can be modified by using a JSON configuration file.

FROM cfssl/cfssl:latest

RUN cfssl print-defaults config > ca-config.json && cfssl print-defaults csr > ca-csr.json   
&& cfssl genkey -initca ca-csr.json | cfssljson -bare ca

EXPOSE 8888

ENTRYPOINT ["cfssl"]

CMD ["serve","-ca=ca.pem","-ca-key=ca-key.pem","-address=0.0.0.0"]  

We now need to be build a new image from the dockerfile listed above. We’ll name the image “cfssltest” and assume the dockerfile is in the current directory.

docker build -t cfssltest .  

We’ll start a container from the image we just created using the docker run command and use port 8888 for the API server.

docker run -d -p 8888:8888 cfssltest  

Now we’re going to request a new SSL certificate from the CA using the curl command and pass it some data about our host for the certificate. The fields were most concerned about are the common name, organization, city, state, and country. A JSON file that contains the CSR information can also be passed to the API server if desired.

hosts: The CN or common name listed on the certificate
C: The country listed on the certificate
ST: The state listed on the certificate
L: The city listed on the certificate

curl -d '{ "request": {"hosts":["$certname"],   
"names":[{"C":"US", "ST":"California", "L":"San Francisco", "O":"example.com"}]} }' 
http://$caaddress:8888/api/v1/cfssl/newcert  

To make things easier we’ll create a script to handle the steps of requesting the certificate from the CA server as well as parsing the JSON output into the respective files for the certificate, private key and certificate request.

#!/bin/bash

certname=$1  
caaddress=$2

# Generate Certificate
curl -d '{ "request": {"CN": '"$certname"',"hosts":['"$certname"'],  
"key": { "algo": "rsa","size": 2048 }, 
"names": [{"C":"US","ST":"California", "L":"San Francisco","O":"example.com"}]}} 
'http://10.0.0.100:8888/api/v1/cfssl/newcert

# Create Private Key
echo -e "$(cat tmpcert.json | python -m json.tool |   
grep private_key | cut -f4 -d '"')"   
> /opt/$certname.key

# Create Certificate
echo -e "$(cat tmpcert.json | python -m json.tool |   
grep -m 1 certificate | cut -f4 -d '"')"   
> /opt/$certname.cer

# Create Certificate Request
echo -e "$(cat tmpcert.json | python -m json.tool |   
grep certificate_request | cut -f4 -d '"')" > /opt/$certname.csr

# Remove JSON Data
rm -Rf tmpcert.json  

Now we can run the script we just created by passing the common name that will be used as well as the address of the certificate authority.

sh generatecert.sh servername.domain.local 192.168.15.5  

While just generating a cert via an API is nice it would be more helpful if we were to integrate it into our provisioning process and we’ll use a Jenkins docker container as an example. Below is the Dockerfile for a generic Jenkins image.

FROM centos:latest  
RUN yum -y update && yum -y install java && yum -y install git  
ADD http://mirrors.jenkins-ci.org/war/latest/jenkins.war /opt/jenkins.war  
RUN chmod 644 /opt/jenkins.war  
COPY generatecert.sh /opt/generatecert.sh  
COPY jenkins.sh /opt/jenkins.sh  
RUN chmod +x /opt/jenkins.sh && chmod +x /opt/generatecert.sh  
ENV JENKINS_HOME /jenkins  
EXPOSE 443  
ENTRYPOINT ["/opt/jenkins.sh"]  

We finally need to create a script to generate a new certificate and start Jenkins when the container starts, that script is below.

#!/bin/bash

hostname=$(hostname)

if [ ! -f /opt/$hostname.cer ];  
then  
  sh /opt/generatecert.sh $hostname $caaddress
fi

java -jar /opt/jenkins.war --httpsPort=443   
--httpsCertificate=/opt/$hostname.cer 
--httpsPrivateKey=/opt/$hostname.key

We now need to create a new image using the Dockerfile we just created. All three files (Dockerfile, generatecert.sh, and jenkins.sh) need to be in the same directory then we can run docker build to create the image.

docker build -t jenkinsssl .  

Now that the image has been created we can provision our Jenkins server that utilizes the SSL certificate from our CA.

docker run -d -p 443:443 -h jenkinsserver01.grt.local jenkinsssl  

We’ve just provisioned a container that has a dynamically generated SSL certificate. We can verify this by opening a web browser to https://dockerhostip and viewing the certificate which has our container name and is singed by our example.net CA.

This is just a quick example of the power that this utility offers for automating SSL certificate generation.

We’ve covered only a small number of the features offered by the cfssl utility but additional documentation can be found on the CFSSL github page.

References

CFSSL Github: https://github.com/cloudflare/cfssl