As I’ve previously mentioned, Gaige and I have an assortment of Polycom VVX and old Cisco 7960 phones floating around which we do VoIP on. The 7960s might not be long for the world though - I recently threatened a friend who had a line on some freebies that I’d toss him a tarball of firmware and sample configs and then stop answering his calls and texts if he went through with it.

It’s long been on my to-do list to get the Polycom provisioning server out of my basement and onto a public IP address, so that I can do configuration of phones at random locations (think parents’ house, etc).

The basic constraints were:

  • It has to be reasonably secure. Polycom’s first tongue seems to be ftp or tftp. Vonage, for instance, wants to use ftp. Tftp doesn’t go through NATs so well (Louie Mamakos wrote an open letter to hardware vendors about this back when he was CTO of Vonage).

  • It had to be relatively low drag. Maintaining IP whitelists is not my idea of a good time. Nor is manual configuration through the front panel of the phone.

  • In addition to tftp and ftp, the Polycoms also support http, or more accurately for our use case https since http is unencrypted just like ftp and tftp are. We (ClueTrust) have been using LetsEncrypt since it was pre-release, and about a year ago transitioned from using HTTP-01 handshakes to DNS-01 handshakes. Among other things this makes it possible to use to deploy wildcard certs, which is awfully powerful when combined with wildcard DNS records.

  • Now that we’ve got things encrypted, the next question becomes one of authentication. We’re trying to hit the sweet spot here of effort vs. security (see “low drag” above). This ain’t the Moscow hotline, and credentials for individual lines on prepaid VoIP doesn’t really need to be protected as if it is - there’s no more than $30 or so at stake if someone starts running up our phone bill. On the other hand, running an https server wide open to serve the files and counting on security in obscurity to keep people from downloading the config files that have predictable names that embed the phone’s MAC address seemed like a poor plan.

  • In ascending order of “pain in the butt to implement”, we have:
    • Auth_Basic and Auth_Digest, built into the http spec
    • client side authentication (sometimes called “mutual TLS”) using Polycom’s built-in-from-the-factory certs
    • client side authentication while running our own CA.
  • I deemed Auth_Basic to be “close enough for government work”, either with a different password per phone or a shared password for all phones under one administrative umbrella. Has the added advantage of being built into NGiNX with the build that I run. Difficulty: certain Microsoft Skype for Business http servers (no doubt IIS or something similar under the hood) sunset support for Auth_Basic, so naturally Polycom followed suit and got rid of Auth_Basic support in their software as well, several versions behind the 5.9.2 that I’m currently running.

  • Auth_Digest is marginally better than Auth_Basic, inasmuch as there are no unencrypted passwords flying around. It’s a challenge-response protocol not unlike SIP’s, wherein an initial authentication failure is returned with a nonce, which is then mixed into the hash for your password and sent upstream as the response and hopefully authenticating you. I say “marginally” because Auth_Digest is a relic of the days when md5 was a fine hash to use against decently skilled actors (it is still just as good as it ever was if your threat model is limited to cosmic rays, by the way), and AFAICT there is no way to run Auth_Digest with more modern hashes such as SHA-256. In any event this seemed to be a bit of a nonstarter since all I was able to find in the way of Auth_Digest for NGiNX comes with the disclaimer: “The module is feature-complete with respect to the RFC but is in need of broader testing before it can be considered secure enough for use in production.” Thankfully, it does not come compiled in to NGiNX.

  • That brings us to client side certs. I have no fear of running my own CA (I run several, and easyrsa3 makes it… easy?), but sussing out how to load certs onto a factory initialized phone on a secure network seemed to be a pain point that I didn’t want to address. PKI solves half of the fabled “key distribution problem”, but getting the secret part of the key pair onto the device in a secure manner is still a pain. I believe that the OS inside a Polycom is some kind of Unix-y thing, but for some reason it appears they decided to use DER formatted certs instead of PEM, so that added an additional step and unfamiliarity-related-complexity to my approach.

  • I decided to do a bit of a deep dive on whether Polycom’s factory certificates might be sufficiently useful. I’d have to trust Polycom’s root CA - disappointingly downloadable over http:// not https:// - for validating them. I don’t trust Polycom’s phones to have a hardened trust store in them or otherwise be hardened against an attack with a JTAG cable, so I would want to do a little more than just saying “any Polycom phone can talk to my server and I’ll trust it when it gives its MAC address”. Luckily for me, Polycom puts the MAC address (in all caps) in the CN of the X.509 client cert. So if I validate the cert against the Polycom root CA and check for a CN that contains a MAC address that I’m expecting, I’m trusting Polycom to not get coerced into issuing duplicate certs for my MAC addresses. I think I’m OK with this level of trust given how much easier it makes my life.

How does one do that with naked NGiNX though? Plenty of recipes out there for setting headers and passing through various certificate bits to a back end, but there is no back end here - we’re using NGiNX as a web server that serves files, not a proxy.

The answer is to use “if” in the / location block (yeah, I know, I know but if you read the fine print my use here is actually OK) in conjunction with map in the “http” block.

Like so:

    map $ssl_client_s_dn $knownphone {
        default       0 ;
        "~*00005e00530c" 1 ; # vvx500 #1
        "~*00005E0053c7" 1 ; # vvx550 #2
        "~*00005E0053ee" 1 ; # vvx601
    }
...
...
    root /somelocation/configs ;
    location / {

             if ($knownphone = 0 ) {
                  return 403;
             }

This works nicely, with the disappointing caveat that the phone will try to upload its logs with an HTTP PUT with a Content-Range: header. This is explicitly prohibited in the penultimate paragraph of RFC 7231 section 4.3.4. NGiNX, unsurprisingly, is RFC-compliant. I’m going to guess that IIS allows it, and that Apache may or may not have a knob to allow it. If Polycom wants to do things this way they really ought to be using the HTTP PATCH command.

Might decide to write something in node.js to support this in my copious spare time (who am I kidding here?)

How does one get the initial provisioining server into the configuration? Through the keyboard on the front right and enter URLs with T9? Uh. no. If you do it right, you can configure a factory-reset phone by feeding it some extra DHCP options, which can then be made persistent so that future dhcp requests don’t have to have it (see “send phone to your parents” use case). Note that I have never tried provisioning a phone with a truly old OS release (and trust store) against a LetsEncrypt cert on an https server, so I might gently suggest bringing the software current over http:// in a trusted environment before trying to provision across the Internet. Note that it will use the first of the NTP servers you hand it, so maybe pool.ntp.org is your best choice if you’re going to be putting these out on random places on the Internet.

Here are the salient bits for isc-dhcpd:

option polycom-vlan             code 128 = text;
option polycom-server            code 160 = text;
option PCode code 100 = text;
option TCode code 101 = text;
option TCode "US/Eastern" ;

group {
        default-lease-time 172800 ;
        max-lease-time 604800 ;

# pick one
        option polycom-server "http://polycom-phones.lan.example.com/" ;
        #option polycom-server "https://polycom-phones.example.com/" ;

        option TCode "US/Eastern" ;

        host vvx500-00005e00530c {
                hardware ethernet 00:00:5e:00:53:0c ;
                fixed-address 192.0.2.30 ; }

        host vvx500-00005e0053c7 {
                hardware ethernet 00:00:5e:00:53:c7 ;
                fixed-address 192.0.2.31 ; }

        host vvx500-00005e0053ee {
                hardware ethernet 00:00:5e:00:53:ee ;
                fixed-address 192.0.2.32 ; }
}

Since I’ve got a wildcard cert under *.polycom-phones.example.com, I can have multiple constituencies (software versions, OT&E, etc) neatly templated out in the Ansible for the nginx.conf without having to make any DNS or certificate changes.

I hope this is useful to someone and avoids some of the slog through the Polycom documentation with pages and pages on how to configure IIS via the GUI but comparatively little in the way of technical details on what’s actually expected under the hood.