Lately I blogged about how am I managing my DNS
entries via SaltStack. So far it was about
being a great time saver, but nothing that you
couldn’t do manually with considerably more effort. This time, let’s take a
look at something that would be in some setups almost impossible manually -
adding TLSA
records for your webs.
What is TLSA
TLSA
records specify SSL certificate used by specified service. The records
looks something like this:
_443._tcp.example.com. IN TLSA 3 1 1 1B66080B9C57281512A06A41314293F406687602A1C74642F388AD9984D8CAA9
All the details are specified in
rfc7671, but let’s take a look
at the basics. How is it typically constructed and what does it mean. Record
name specifies port (in this case 443) and protocol (in this case tcp).
Type is TLSA
and the value contains some restrictions for the certificate.
First number (in my case 3) specifies the type of the restriction. 3 means that
restrictions apply to the certificate used by the service directly, lower
numbers specify restrictions on CA or chain. Next number let’s you choose
between taking into account full certificate (0) or just the public key (1).
Using public key reduces the need to roll-over whenever our certificate is
resigned. The last number specifies the hash type. You can choose between 1
for SHA-256 and 2
for SHA-512.
Now let’s talk about how SaltStack can help us fill in all that.
Modules and mines using custom functions
As with the SSHFP
records, we would like to collect these records
automatically from our minions. But there is no single command to get it. We
can’t use cmd.run
as we did for SSHFP
entries. But luckily, SaltStack
can be extended. Unfortunately for me, using Python. So let’s write a
Python function, that gives us all the informations we need to fill the DNS
records. It can look something like the following one.
#!/usr/bin/env python3
import glob
import re
import os.path
import ssl
import hashlib
from cryptography.x509 import load_pem_x509_certificate
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
def _sha_digests(something):
sha256 = hashlib.sha256()
sha256.update(something)
sha512 = hashlib.sha512()
sha512.update(something)
return sha256.hexdigest(), sha512.hexdigest()
def get_tlsa(cert):
crt = False
with open(cert, "rb") as fl:
crt = load_pem_x509_certificate(fl.read(), default_backend())
if not crt:
return False
pk = crt.public_key().public_bytes(format=PublicFormat.SubjectPublicKeyInfo, encoding=Encoding.DER)
sha256, sha512 = _sha_digests(pk)
return [
f'3 1 1 {sha256}',
f'3 1 2 {sha512}'
]
Now that we have a function, what do we do with that? We need to somehow
distribute it to our minions and make them use it to mine our data. To do that,
we put it into Salt root directory in subdirectory
_modules/tlsa_dumper
into file __init__.py
. This way, Salt with distribute
it to all minions over the time. To speed things up we can call
salt '*' saltutil.sync_modules
. After that all the public functions - those
not starting with _
can be used for whatever we want. Syntax is
{directory_name}.{function_name}
. Simple call can look something like this
salt 'minion' tlsa_dumper.get_tlsa /etc/ssl/nginx/example.com.key;
This will get us data for one of the certificates we have on one minion if we
know where exactly it is. But that is not the end of the story. Lets protect
visitors of all our web services via TLSA
. Let’s expand our function a little
bit more…
...
def _get_webs(conf):
ret = {}
webs = []
tlsa = []
srv = re.compile(r'^\s*server_name\s+(.*);$')
cert = re.compile(r'^\s*ssl_certificate\s+(.*);$')
with open(conf) as file:
for ln in file:
m = cert.match(ln)
if m:
tlsa = _get_tlsa(m.group(1))
if os.path.isfile(m.group(1) + '.new'):
tlsa = tlsa + get_tlsa(m.group(1) + '.new')
m = srv.match(ln)
if srv.match(ln):
webs = webs + m.group(1).split(' ')
for web in webs:
ret[web] = tlsa
return ret
def get_nginx_webs():
ret = {}
for conf in glob.glob("/etc/nginx/vhosts.d/*.conf"):
ret.update(_get_webs(conf))
return ret
Now it will go through all the Nginx virtual hosts, get their server names
and corresponding TLSA
records. It will also get records for keys ending with
.new
to handle roll-over. All that is left is integrate our function into
SaltStack Mine like this:
mine_functions:
webs:
- mine_function: tlsa_dumper.get_nginx_webs
And to put the harvested data into use by putting them into our domain.
knot:
server:
...
zone
example.com:
...
records:
...
{%- set domain = 'example.com %}
{%- for srv, webs in salt.saltutil.runner('mine.get', tgt='*', fun='webs', tgt_type='compound').items() %}
{%- for web_full in webs %}
{%- if web_full and web_full is match(".*" + domain + "$") %}
{%- if web_full != grains['id'] %}
- name: {{ web_full.replace('.' + domain,'') }}
type: CNAME
content: {{ srv }}.
{%- endif %}
{%- if webs.get(web_full, False) %}
{%- for tlsa in webs[web_full] %}
- name: _443._tcp.{{ web_full.replace('.' + domain,'') }}
type: TLSA
content: {{ tlsa|yaml_dquote }}
{%- endfor %}
{%- endif %}
{%- endif %}
{%- endfor %}
{%- endfor %}
As you can see, not only do we get all TLSA
entries, but at the same time, we
get all the CNAME
records pointing to the right server for free. We can
easily extend the setup to be able to collect TLSA
records for smtp servers
and other services as well. Would be possible to do it manually? Yes, with
considerable effort and only if your ACME client supports keeping the same key.
But with this setup, you can regenerate keys whenever you like and everything
will fix itself.
Hope this series of blog post got you interested if not into SaltStack, then
at least into putting some more data into your domains. DNS can be really
useful when paired with DNSSEC and if you have a simple way how to feed all
relevant data into it ;-) Let’s all get SSHFP
, TLSA
, DANE
, SPF
,
DKIM
, DMARC
, CAA
and more and build a safer Internet with those!