Tuesday 14 January 2014

Hostname canonicalisation in OpenSSH

OpenSSH 6.5 will introduce some new options to allow the client to canonicalise unqualified domain names, allowing it (for example) to understand that I actually meant "bigserver.mydomain.com" when I typed "ssh bigserver". This turns out to be important because, even though your host's DNS resolver will connect you to the host that you intended, ssh doesn't know the full name for it.

If ssh doesn't know the full name for a host then it can't reliably match it with a host key. The problem is even worse when the server is offering a certificate host key - these (should) contain the fully-qualified domain name (FQDN) of the server, but this break when users type "ssh bigserver" without the remainder of the domain name. A common workaround is to add "Hostname" or "HostkeyAlias" directives to ssh_config, but that is messy and doesn't scale well to lots of hosts. The other workaround for certificates of adding the unqualified names to the list of certificate principals is also terrible.

One might be forgiven for thinking that the system resolver should be able to help us here; after all - it knows the FQDN for the destination host because it knows all the domain search paths the user configured and which one was actually taken. Unfortunately, it turns out not to be useful for two reasons:

  1. The resolver doesn't actually offer a way to figure out what the fully-qualified name is. Some platforms do, via the AI_FQDN option - but it isn't widely available (Windows and OpenBSD only AFAIK)
  2. Even if we could get the name, then we couldn't trust it for anything configured via DHCP anyway. On most systems, the set of domain search paths is configured by DHCP and a rogue DHCP server could supply a malicious set.

My solution has been to add explicit hostname canonicalisation options that allow the user to define their own optional DNS search paths in OpenSSH itself. These options are: CanonicalDomains, CanonicalizeFallbackLocal, CanonicalizeHostname, CanonicalizeMaxDots and CanonicalizePermittedCNAMEs. You may notice that they substantially duplicate the search path functionality you'd expect to find in resolv.conf.

CanonicalizeHostname turns canonicalisation off and on (it's off by default). CanonicalDomains specifies the list of domains to search for an unqualified hostname in. CanonicalizeMaxDots sets how many '.' characters must appear in a domain name before it is considered unqualified (e.g. if you want names like "ftp.dmz" to be subject to canonicalisation then you would set this to one or more). CanonicalizeFallbackLocal specifies whether the original, unqualified name should be passed to the system resolver if it wasn't found in any of the suffixes in CanonicalDomains. Finally CanonicalizePermittedCNAMEs specifies some rules for selectively following CNAMEs (DNS aliases) when canonicalising a name.

This should all be more clear with an example. This is what is at the top of my ~/.ssh/config:

CanonicalizeHostname yes
CanonicalDomains mindrot.org
CanonicalizeMaxDots 0
CanonicalizeFallbackLocal no

This enables canonicalisation with a single search path of mindrot.org. When I type "ssh mail", the hostname mail will be judged unqualified since it contains the no period characters (specifically, less than or equal to CanonicalizeMaxDots), so ssh will try to resolve it in one of the CanonicalDomains. If mail.mindrot.org did not exist, then ssh won't bother attempting to continue with the original hostname mail.

I haven't mentioned CanonicalizePermittedCNAMEs yet, since it is the most complex and most users won't need it. It's useful in cases where your organisation's DNS has a number of CNAME aliases that point to the same host(s). It allows the user to specify rules for when the alias should be allowed to replace the original host name. This option accepts multiple arguments, each of the form source_pattern:target_pattern. If the name (after canonicalisation and resolution) matches source_pattern and the destination of the CNAME matches target_pattern then the target of the CNAME will replace the original name The rules are pretty flexible; they accept the pattern syntax used widely in OpenSSH (with negation, '*' and '?' wildcards). Hopefully an example will make this clear too:

CanonicalizeHostname yes
CanonicalDomains example.com int.example.com
CanonicalizeMaxDots 1
CanonicalizeFallbackLocal yes
CanonicalizePermittedCNAMEs mail.*.example.com:anycast-mail.int.example.com dns*.example.com:dns*.dmz.example.com

This example enables canonicalisation with a couple of suffixes in the search path. It also turns CanonicalizeMaxDots up to 1, so a name like mail.dmz will be searched in each suffix. If a name does not resolve in any suffix then it will be passed to the system resolver as a fallback. Finally, some rules for following CNAMEs are specified: any CNAME matching mail.*.example.com will be followed so long as the ultimate destination is anycast-mail.int.example.com and any host name matching dns*.example.com will be followed so long as the destination matches dns*.dmz.example.com.

These options will be available in OpenSSH 6.5, which is due really soon (hopefully by the end of the month). I'd love to hear any feedback about them.

No comments: