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