Skip to content

Instantly share code, notes, and snippets.

@LucaFilipozzi
Last active May 2, 2019 21:39
Show Gist options
  • Save LucaFilipozzi/7dbfe4e2d7ca844eedc9d751ee490acc to your computer and use it in GitHub Desktop.
Save LucaFilipozzi/7dbfe4e2d7ca844eedc9d751ee490acc to your computer and use it in GitHub Desktop.
acme4bigip

acme4bigip

The purpose of these scripts is to help implement HTTPS Everywhere using acmetool and F5 BigIP LTM.

Usage

Set up acmetool as normal.

DNS

Allocate an «IP Address» for the virtual server.

F5

Create two BigIP virtual servers for that «IP Address», one on port 80 and the other on port 44.

The port 80 virtual server should have a default pool that points to the server where acmetool runs.

The port 443 virtual server should have a default pool that points to the servers that provide content.

Add the appropriate iRule to each virtual server.

acmetool

Install acmetool as normal (apt install acmetool).

Install the acmetool4bigip.py hook script into /usr/lib/acme/hooks.

Set up a webserver that makes /var/lib/acme/live available to the F5. Alternately, set up another acmetool hook script that moves the files to a place where a webserver can serve them. This hook script needs to run before the acmetool4bigip.py hook script, so name it accordingly.

Getting a Certificate

Create an A record for the domain: site1.example.com IN A «IP Address»

Invoke 'acmetool want site1.example.com'.

acmetool will use HTTP domain control validation, via the iRules, to obtain the certificate from Let's Encrypt.

Once obtained, acmetool will invoke the acmetool4bigip.py hook script to cause the F5 to pull the key, certificate and intermediate certificate chain files from the webserver.

Rinse and repeat for each subsequent domain.

License

This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

# F5 BigIP LTM iRule
# Copyright (C) 2017 Luca Filipozzi <luca.filipozzi@gmail.com>
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
#
# add this iRule to the port 80 virtual server
#
when HTTP_REQUEST {
if { ! ( [string tolower [HTTP::path]] starts_with "/.well-known/acme-challenge/") } {
#log local0. "no acme challenge detected, redirecting to https"
if {([string tolower [HTTP::host]] starts_with "www.")} {
HTTP::redirect "https://[string range [HTTP::host] 4 end][HTTP::uri]"
} else {
HTTP::redirect "https://[HTTP::host][HTTP::uri]"
}
return
}
#log local0. "using default pool to handle acme challenge"
HTTP::header remove X-Forwarded-For
HTTP::header insert X-Forwarded-For [IP::remote_addr]
HTTP::header remove X-Forwarded-Port
HTTP::header insert X-Forwarded-Port [TCP::local_port]
HTTP::header remove X-Forwarded-Proto
HTTP::header insert X-Forwarded-Proto "http"
}
# F5 BigIP LTM iRule
# Copyright (C) 2017 Luca Filipozzi <luca.filipozzi@gmail.com>
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
#
# add this iRule to the port 443 virtual server
#
when HTTP_REQUEST {
if {([string tolower [HTTP::host]] starts_with "www.")} {
HTTP::redirect "https://[string range [HTTP::host] 4 end][HTTP::uri]"
return
}
#log local0. "using default pool to deliver content"
HTTP::header remove X-Forwarded-For
HTTP::header insert X-Forwarded-For [IP::remote_addr]
HTTP::header remove X-Forwarded-Port
HTTP::header insert X-Forwarded-Port [TCP::local_port]
HTTP::header remove X-Forwarded-Proto
HTTP::header insert X-Forwarded-Proto "https"
}
#!/usr/bin/env python
# Copyright (C) 2017 Luca Filipozz <luca.filipozzi@gmail.com>
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
#
# use this deployment script with acmetool
import addict
import click
import f5.bigip
import f5.bigip.contexts
import requests
import yaml
def do_common(cfg, cn):
cfg.ssl.name = cfg.formats.ssl.name.format(cn)
cfg.ssl.defaultsFrom = cfg.formats.ssl.defaultsFrom.format(cn)
cfg.ssl.partition = cfg.formats.ssl.partition.format(cn)
cfg.ssl.exists = cfg.mr.tm.ltm.profile.client_ssls.client_ssl.exists(partition=cfg.ssl.partition, name=cfg.ssl.name)
cfg.key.name = cfg.formats.key.name.format(cn)
cfg.key.path = cfg.formats.key.path.format(cn)
cfg.key.exists = cfg.mr.tm.sys.file.ssl_keys.ssl_key.exists(name=cfg.key.name)
cfg.crt.name = cfg.formats.crt.name.format(cn)
cfg.crt.path = cfg.formats.crt.path.format(cn)
cfg.crt.exists = cfg.mr.tm.sys.file.ssl_certs.ssl_cert.exists(name=cfg.crt.name)
cfg.chn.name = cfg.formats.chn.name.format(cn)
cfg.chn.path = cfg.formats.chn.path.format(cn)
cfg.chn.exists = cfg.mr.tm.sys.file.ssl_certs.ssl_cert.exists(name=cfg.chn.name)
cfg.tx = cfg.mr.tm.transactions.transaction
def do_deploy(cfg):
with f5.bigip.contexts.TransactionContextManager(cfg.tx) as api:
if cfg.key.exists:
api.tm.sys.file.ssl_keys.ssl_key.load(name=cfg.key.name).update()
else:
api.tm.sys.file.ssl_keys.ssl_key.create(name=cfg.key.name, sourcePath=cfg.key.path)
if cfg.crt.exists:
api.tm.sys.file.ssl_certs.ssl_cert.load(name=cfg.crt.name).update()
else:
api.tm.sys.file.ssl_certs.ssl_cert.create(name=cfg.crt.name, sourcePath=cfg.crt.path)
if cfg.chn.exists:
api.tm.sys.file.ssl_certs.ssl_cert.load(name=cfg.chn.name).update()
else:
api.tm.sys.file.ssl_certs.ssl_cert.create(name=cfg.chn.name, sourcePath=cfg.chn.path)
if not cfg.ssl.exists:
cfg.mr.tm.ltm.profile.client_ssls.client_ssl.create(partition=cfg.ssl.partition, name=cfg.ssl.name,
key=cfg.key.name, cert=cfg.crt.name, chain=cfg.chn.name, defaultsFrom=cfg.ssl.defaultsFrom)
def do_remove(cfg):
if not cfg.ssl.exists and not cfg.key.exists and not cfg.crt.exists and not cfg.chn.exists:
return
with f5.bigip.contexts.TransactionContextManager(cfg.tx) as api:
if cfg.ssl.exists:
cfg.mr.tm.ltm.profile.client_ssls.client_ssl.load(partition=cfg.ssl.partition, name=cfg.ssl.name).delete()
if cfg.key.exists:
api.tm.sys.file.ssl_keys.ssl_key.load(name=cfg.key.name).delete()
if cfg.crt.exists:
api.tm.sys.file.ssl_certs.ssl_cert.load(name=cfg.crt.name).delete()
if cfg.chn.exists:
api.tm.sys.file.ssl_certs.ssl_cert.load(name=cfg.chn.name).delete()
@click.group()
@click.option('--insecure', is_flag=True,
help='Disable insecure request warning.')
@click.option('--cfgfile', metavar='<CFGFILE>', type=click.File(),
help='Specify configuration file.', default='config.yaml')
@click.pass_context
def cli(ctx, insecure, cfgfile):
""" hook script to deploy x509 certificates onto an F5 BIG-IP """
if insecure:
warning = requests.packages.urllib3.exceptions.InsecureRequestWarning
requests.packages.urllib3.disable_warnings(warning)
ctx.obj = addict.Dict(yaml.load(cfgfile))
@cli.command(name='live-updated')
@click.argument('cns', metavar='<CN [CN] [CN]>', nargs=-1)
@click.pass_obj
def live_updated(cfg, cns):
""" deploy multiple x509 certificates """
cfg.mr = f5.bigip.ManagementRoot(cfg.f5lb.hostname, cfg.f5lb.username, cfg.f5lb.password)
for cn in cns:
do_common(cfg, cn)
do_deploy(cfg)
@cli.command()
@click.argument('cn', metavar='<CN>', nargs=1)
@click.pass_obj
def deploy(cfg, cn):
""" deploy a single x509 certificate """
cfg.mr = f5.bigip.ManagementRoot(cfg.f5lb.hostname, cfg.f5lb.username, cfg.f5lb.password)
do_common(cfg, cn)
do_deploy(cfg)
@cli.command()
@click.argument('cn', metavar='<CN>', nargs=1)
@click.pass_obj
def remove(cfg, cn):
""" remove a single x509 certificate """
cfg.mr = f5.bigip.ManagementRoot(cfg.f5lb.hostname, cfg.f5lb.username, cfg.f5lb.password)
do_common(cfg, cn)
do_remove(cfg)
if __name__ == '__main__':
cli(obj=None)
---
f5lb:
hostname: f5.example.com
username: username
password: password
formats:
ssl:
name: "{0}_SSL"
defaultsFrom: "clientssl"
partition: "default"
key:
name: "{0}.key"
path: "http://acme.example.com/{0}/privkey"
crt:
name: "{0}.crt"
path: "http://acme.example.com/{0}/cert"
chn:
name: "{0}_chn.crt"
path: "http://acme.example.com/{0}/fullchain"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment