introduction
classically, revokation of certificates was accomplished with certificate revokation lists (crls). the idea was that browsers regularly download crls from the certificate authorities (cas) and check whether certificates they see are on the list. this doesn't scale well, though. nowadays, there are many cas trusted by browsers in their default configuration, and crls tend to get huge.
a better solution is the online certificate status protocol (ocsp): a browser, when encountering a new certificate, asks the ocsp server of the browser (the url for it is contained in the certificate) whether the certificate is still valid. this has several downsides as well: first, ocsp servers are not always reliable. if a browser cannot connect to it (or doesn't get a reply), what should it do? deny access to the site? besides that, there's another large downside: privacy. the ocsp server knows which page you are visiting, because the browser tells it by asking whether its certificate is valid.
ocsp stapling was invented to improve upon this: the idea is that the webserver itself asks the ocsp server for the status of its certificate, and delivers the answer together with the certificate to the connecting browser. as the ocsp response is signed by the cas certificate, the browser can verify that the response is valid. also, the expiration time of ocsp responses is much less than the one for certificates, so if a certificate is revoked, existing ocsp responses will only be valid for a couple of more days.
this is pretty good already, except that a malicious webserver could simply not send the ocsp response with its certificate. if a browser cannot contact the ocsp server itself, it has no way to know whether the certificate is revoked or not. to overcome this, ocsp must-staple was invented. this is a flag in the certificate itself which says that the certificate is only valid with a valid and good ocsp response. so if a browser encounters a certificate with this flag, and the webserver isn't ocsp stapling, the browser knows that something is fishy.
unfortunately, there are some downsides. first, most the most common webservers for linux, apache and nginx, while having ocsp stapling support, do in some situations send replies without ocsp stapling. if the certificate has the ocsp must-staple flag set, these answers result in error pages shown in browsers. and that's something you really want to avoid, that visitors of your page thing there's something bad happening.
fortunately, at least for nginx, you can specify a file containing an ocsp response directly with the ssl_stapling_file
directive. unfortunately, you have to make sure you always have a good and valid ocsp response at that place, and reload nginx in case the response is updated. other programs allow to specify an ocsp response in a similar way, such as exim with the tls_ocsp_file
directive, and thus have the same problem. to solve this problem, i've started creating ocsp bot:
ocsp bot
ocsp bot is a python script which should be called frequently (as in: once per hour or so), which checks a set of x.509 certificates to obtain up-to-date ocsp responses. in case the current ocsp responses will expire soon, or aren't there, it will try to get a new response. it will only copy the new response to the correct place if it is valid and good. calling it frequently will ensure that in case of problems getting a new response, it will retry every hour (or so) until a good and valid response could have been obtained. so a user response is only necessary if the process fails several times in a row.
ocsp bot will signal with its exit code whether responses have been updated, allowing to reload/restart the corresponding service to use the new response.
you can install ocsp bot with pip install ocsp
from pypi.
integration with ansible
i'm using ansible to configure my server. to copy certificates and obtain ocsp responses, i'm using a custom role.
the ansible tasks for the role are as follows. ocsp bot is installed in /var/www/ocsp
:
- name: Create OCSP log folder
file: dest=/var/www/ocsp/logs state=directory
- name: Create OCSP response folder
file: dest=/var/www/ocsp/responses state=directory
- name: Install pyyaml
pip: name=pyyaml
- name: Install OCSP response utility
copy: src=ocspbot.py dest=/var/www/ocsp/ocspbot.py mode=0755
# ocspbot.py is ocspbot/__main__.py from https://github.com/felixfontein/ocspbot/
- name: Install OCSP bash script
template: src=ocspbot.sh.j2 dest=/var/www/ocsp/ocspbot.sh mode=0755
- name: Install OCSP response utility configurations
template: src=ocspbot.yaml.j2 dest=/var/www/ocsp/ocspbot-{{ item.key }}.yaml
with_dict: "{{ certificates }}"
- name: Install OCSP response cronjob
cron: name="Update OCSP responses" hour=* minute=0 job=/var/www/ocsp/ocspbot.sh state=present
the variable certificates
is defined as follows:
certificates:
nginx:
domains:
- example.com
- example.net
reload:
- nginx
key_owner: root
key_group: root
key_mode: "0400"
mailserver:
domains:
- mail.example.com
reload:
- dovecot
- exim
key_owner: root
key_group: exim
key_mode: "0440"
The template for ocspbot.sh
:
#!/bin/bash
RC=0
{% for name, data in certificates|dictsort %}
# Renew OCSP responses for {{ name }}
/var/www/ocsp/ocspbot.py /home/ocsp/ocspbot-{{ name }}.yaml
RESULT=$?
if [ $RESULT -gt 0 ]; then
{% for service in data.reload %}
systemctl reload {{ service }}
{% endfor %}
elif [ $RESULT -lt 0 ]; then
RC=1
fi
{% endfor %}
exit $RC
the template for the configuration yaml files:
make_backups: True
minimum_validity: 3d
minimum_validity_percentage: 42.8
ocsp_folder: /var/www/ocsp/responses
output_log: /var/www/ocsp/logs/{{ item.key }}-{year}{month}{day}-{hour}{minute}{second}.log
domains:
{% for domain in item.value.domains|sort %}
{{ domain }}:
cert: /var/www/certs/{{ domain }}.pem
chain: /var/www/certs/{{ domain }}-chain.pem
rootchain: /var/www/certs/{{ domain }}-rootchain.pem
ocsp: {{ domain }}.ocsp-resp
{% endfor %}
the certificates are copied with the following ansible tasks:
- name: copy private keys
copy: src=keys/{{ item.1 }}.key dest=/var/www/keys/{{ item.1 }}.key owner={{ item.0.value.key_owner }} group={{ item.0.value.key_group }} mode={{ item.0.value.key_mode }}
with_dependent:
- "certificates"
- "item.0.value.domains"
notify: update OCSP responses
- name: copy certificates
copy: src=keys/{{ item.1 }}{{ item.2 }} dest=/var/www/certs/{{ item.1 }}{{ item.2 }} owner=root group=root mode=0444
with_dependent:
- "certificates"
- "item.0.value.domains"
- '["-rootchain.pem", "-fullchain.pem", "-chain.pem", ".pem"]'
notify: update OCSP responses
(here, the dependent loop lookup plugin is used.)
the handler update OCSP responses
is defined as follows:
- name: update OCSP responses
command: /var/www/ocsp/ocspbot.sh
register: result
failed_when: result.rc != 0
notify:
- reload nginx
- reload exim
- reload dovecot
i'm using this setup for some weeks now, and it seems to work fine. so far, i'm not using ocsp must-staple certificates (except for some test subdomains). if everything seems to be fine for some time, i'll switch to ocsp must-staple certificates.