Get ACME certs on your LAN, like Glyph's lancer, but with foolscap extensions.
Do you have local fileservers or applicances that need TLS certificates? Tired of dismissing self-signed certificate warnings in your browser? Flancer might be for you.
Flancer comes in two halves. The server half runs on a publically-accessible machine, while the client half runs on a machine inside your LAN. The server responds to Let's Encrypt challenges on behalf of the client. At the end of the day, the client gets a valid TLS certificate for a machine that is only visible on the LAN.
Flancer has the additional ability to update a dynamic DNS record for the
external address of your LAN's internet connection. If you use
port-forwarding to expose specific services from this address, you can use a
stable name (e.g. gw.sf.example.com
) to connect to those forwarded ports,
despite your ISP changing this address (e.g. when your DHCP lease expires and
is renewed with a different address).
First, decide upon which DNS "Zone" or zones you'll be managing. For example,
if you control example.com
, and you have sites in San Francisco and New
York, you might use sf.example.com
for the first site, and ny.example.com
for the second. All the machines on the New York LAN will have names like
name1.ny.example.com
and name2.ny.example.com
. We'll be obtaining TLS
certifications from Let's Encrypt for name1.ny.example.com
machines.
Second, find a machine that has a public IP address and is up all the time.
This will run the flancer server half, which is a daemon that needs to be
launched at boot, and left running all the time. It will respond to DNS
queries from the internet at large (and most specifically from the Let's
Encrypt challenge machines). It will also accept connections from the flancer
client half. We'll need to know the public DNS name of this machine, e.g.
external.example.com
.
You must configure your example.com
nameserver to delegate authority over
the zone (sf.example.com
or ny.example.com
) to this server machine. This
allows the Let's Encrypt queries for name1.ny.example.com
to be sent to
(and answered by) this server.
Third, for each zone, choose a machine on the local LAN to run the flancer client half. It does not need to be up all of the time, but it can only obtain certificates while it's running, so try to let it run at least once a week. If you use the dynamic DNS "gateway" name feature, you may want to keep it running at all times, so the record can be updated promptly.
Run the server with Twisted's twist
utility, telling it the hostname of the
server machine:
server$ twist flancer-server --hostname=external.example.com
That will leave the process running in the current shell. You can use
twistd
instead of twist
to automatically daemonize it, or you can run it
in the background or under your favorite process-management tool (systemctl,
etc).
The server will store it's state in a "base directory", which defaults to
~/.flancer-server/
. The primary state goes into a config.json
in this
directory.
Once running, you'll interact with the server with a separate CLI tool. The base directory holds additional files which help the CLI tool talk to the running server.
For each zone you want to add, run the following command on the server machine:
server$ python -m flancer.server add-zone sf.example.com server.example.com
The first argument (sf.example.com
) is the zone being added. We're telling
the server to accept client certificate requests for names under this zone
(like name1.sf.example.com
).
The second argument must be the globally-known hostname of the machine which runs the flancer server. This will be used as the SOA ("source of authority") record, which means it must match the hostname to which the zone has been delegated by the parent zone.
On your selected LAN-side client machine, run the client daemon with:
client$ twist flancer-client --really
If you omit --really
, the certificates will come from the Let's Encrypt
staging server, which is appropriate for testing but won't help for real
certs.
As with the server, this leaves the process running in the current shell, so
you may wish to daemonize it somehow. The client daemon stores its state in
the ~/.flancer-client/
base directory by default. The state includes copies
of all the TLS certificates (and associated private keys) that are generated.
To get a TLS certificate for a new host on your LAN, start by going to the server and asking it for an invitation code:
server$ python -m flancer.server add-host name1.sf.example.com
This will display an "invitation code" (which uses Magic Wormhole under the hood), which takes the form of NUMBER-WORD-WORD.
Next, on your client machine, run this command:
client$ python -m flancer.client add-host [CODE]
You can either provide the full invitation code as the last argument, or you can omit it and then type in the code interactively. The interactive entry can do tab-completion of the words.
Once added, the invitation code is used to establish a secure connection with
the server. The client then chooses the private key, creates the certificate
signing request, contacts the Let's Encrypt verification servers, and
requests an ACME dns-01 (TXT
record) challenge. It receives the challenge
string, then contacts the flancer server and asks it to serve a new TXT
record for the new hostname. The Let's Encrypt verification server performs
the TXT
lookup, which is delegated to the flancer server (because the
entire sf.example.com
zone is delegated there), and retrieves the challenge
value, confirming that the client has control over the domain. With
verification complete, the Let's Encrypt server then returns the signed
certificate to the client directly. The flancer client then tells the flancer
server to remove the TXT
record.
The flancer client writes the TLS files into its basedir, in files that end
in .cert.pem
, .chain.pem
, .fullchain.pem
, .pem
, and .privkey.pem
.
For example, after adding name1.sf.example.com
, we will find:
~/.flancer-client/name1.sf.example.com.cert.pem
~/.flancer-client/name1.sf.example.com.chain.pem
~/.flancer-client/name1.sf.example.com.fullchain.pem
~/.flancer-client/name1.sf.example.com.pem
~/.flancer-client/name1.sf.example.com.privkey.pem
cert.pem
contains the leaf certificate, while chain.pem
contains the
Let's Encrypt intermediate cert. fullchain.pem
contains both of these
concatenated together.
privkey.pem
contains the private key, which must obviously be treated with
great care. The plain .pem
file contains both fullchain.pem
and
privkey.pem
concatenated together. For many TLS servers, this last .pem
file is what you need.
You must arrange for these .pem
files to be installed into your TLS/web
server.
(TODO): it is not implemented yet, but eventually the flancer client will pay
attention to a post-update hook file. When name1.sf.example.com
has a new
certificate available, the client will look for
~/.flancer-client/name1.sf.example.com.post-update-hook
. If this file is
present and executable, it will be run (with some arguments TBD).
The client will periodically check all configured certificates (perhaps daily). It will renew any that need updating. In practice this gets a new certificate every two months, and certificates are generally valid for three months.
(TODO) Once the post-update hook is implemented, the hook will be executed each time the certificate is renewed.
Note that many TLS/web servers only read the certificate file once, at startup, so you may need to restart or reload the server config after renewal.
You can configure the server to maintain a DNS name that points at the
client's external IP address. For example you might want gw.sf.example.com
to point to the sf
LAN's router's external address. The dynamic name must
be a member of the managed zone (so e.g. gw.other.sf.example.com
or
gw.other.com
would not work).
To do this, start on the flancer server machine, and run:
server$ python -m flancer.server add-dyndns gw.sf.example.com
This will create an invitation code, as before. Then, on your client machine, run this command:
client$ python -m flancer.client add-dyndns [CODE]
This will configure the client to initiate and maintain a connection to the server. The server will check the source address of this connection, and use it to create the dynamic DNS name record. Both sides will ping this connection every 60 seconds (to keep a NAT/firewall table entry in place), and will attempt to reestablish a connection if this ping is rejected or if roughly five minutes pass without a response.
The dynamic DNS name is not currently removed if/when the client connection is lost: the server will continue to advertise the most recently known IP address. However the address is not stored on disk, so when the server is restarted, it will not advertise the dynamic name at all until the client establishes the connection.
(TODO) In the future, the dynamic DNS name might be unregistered when the client connection is lost, under the theory that if the server can't reach it, the rest of the world won't be able to either. This is not implemented yet.