howtos:let_s_encrypt_-_how_to_issue_certificates_from_a_bigip
Differences
This shows you the differences between two versions of the page.
Next revision | Previous revision | ||
howtos:let_s_encrypt_-_how_to_issue_certificates_from_a_bigip [02/12/2018 21:34] – external edit 127.0.0.1 | howtos:let_s_encrypt_-_how_to_issue_certificates_from_a_bigip [23/01/2021 09:24] (current) – [hook.sh] domingo | ||
---|---|---|---|
Line 1: | Line 1: | ||
+ | ====== Let's Encrypt on a BigIP ====== | ||
+ | |||
+ | < | ||
+ | Let's Encrypt is no longer in beta (https:// | ||
+ | |||
+ | Let's Encrypt is a great project with a new approach to certificates and how to secure and manage them. This new approach however forces us to think a little bit differently when we work with them. Normally this was a task that took place once a year and could easily be handled by hand. This is not the case with Let's Encrypt. Each certificate only has a lifespan of 90 days and for the time being we do not have access to wildcard certificates, | ||
+ | |||
+ | So here comes certificate automation. The project has built a client making automation possible when running on a generic Linux server, and that is fine. However it does make it a little more problematic if you don't have the certificates on the backend servers but on an ADC, F5 Big-IP in this case. This got me thinking, how do we fix this? | ||
+ | Luckily I got some very talented colleagues (thank you Dindorp!) with an equal desire for Let's Encrypt, so here is one way of doing it! | ||
+ | ===== Requirements ===== | ||
+ | As this solution is based on pure bash scripts we have very few dependencies, | ||
+ | |||
+ | Besides the ability to control a keyboard and a SSH client LOL you of course needs to have access to a BigIP with admin rights and an advanced shell (F5 terminology for a Bash shell). | ||
+ | |||
+ | The idea is to use crontab for the automation, so you need to hack this for your requirements. The crontab on a BigIP is no different than on a generic Linux server, so nothing magical here. | ||
+ | |||
+ | On the BigIP you must have a virtual server listening on port 80/tcp that the domain resolves to. This VS is what we use as a reverse proxy for the challenge-response validation mechanism that Let's Encrypt is based upon. You probably have this already and you can just reuse it. As we have the logic tied to an iRule, you just have to make sure that the iRule is the first thing being executed so current logic doesn' | ||
+ | |||
+ | Another important (and obvious ^_^) requirement is when you have a HA pair, you must make sure that the scripts only run on the unit which is active for the traffic-group. Otherwise the changes wouldn' | ||
+ | ===== Limitations / Todo's ===== | ||
+ | |||
+ | Before you start to fire away with requests please be aware of these restrictions that is in place currently: https:// | ||
+ | |||
+ | I've been hit by them a couple of time now in my eagerness to test :-) They should be loosened over time but when and to what extent I do not know. | ||
+ | |||
+ | For now I haven' | ||
+ | |||
+ | The scripts are tested on TMOS version 12.1 but should work across other versions (Update: it also works with 13.0 and all in between). The limitation is gonna be the tmsh commands in the hook script, the rest is taken directly from GitHub. | ||
+ | |||
+ | ===== Data Group ===== | ||
+ | When you initiate the certificate requests the authentication is based on a challenge-response to prove you own or control the domain name. For this to work we utilize a data group to contain the challenge-response values that are generated through the script. This bridges the script values with the iRule and allows for easy and dynamic access to it. | ||
+ | |||
+ | Here is the tmsh command to create it: | ||
+ | < | ||
+ | tmsh create ltm data-group internal acme_responses type string | ||
+ | </ | ||
+ | ===== iRule ===== | ||
+ | The iRule works as the webserver/ | ||
+ | Copy the iRule below and attach it to the proper virtual server which hosts the domain(s). | ||
+ | |||
+ | < | ||
+ | when HTTP_REQUEST { | ||
+ | if { not ([HTTP:: | ||
+ | set token [lindex [split [HTTP:: | ||
+ | set response [class match -value -- $token equals acme_responses] | ||
+ | if { " | ||
+ | log local0. " | ||
+ | HTTP:: | ||
+ | } else { | ||
+ | log local0. " | ||
+ | HTTP:: | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | The virtual server you are using for this iRule probably also has other iRules or functions (like ASM or APM) attached to it. As I have found out this intervened with the challenge-response traffic. I had a bunch of redirect iRules which caused a multi-redirect error situation. So think about this when you attach this iRule. One simple and crude way of fixing this is to insert this in top of the HTTP_REQUEST event: | ||
+ | < | ||
+ | if { ([HTTP:: | ||
+ | </ | ||
+ | |||
+ | | ||
+ | ===== Client SSL Profiles ===== | ||
+ | When the certificate has been signed and returned the hook script will apply it to the F5 configuration through a set of tmsh commands. | ||
+ | These commands has some assumptions. First of all it has to exist beforehand and secondly it must have the name convention as this: " | ||
+ | |||
+ | An example. If you have the domain " | ||
+ | |||
+ | The hook script simply replaces the certificate and key files already in place. You can apply whatever settings to the profile you like. | ||
+ | |||
+ | If you have the " | ||
+ | < | ||
+ | #!/bin/bash | ||
+ | for i in $( cat domains.txt | awk '{ print $1}' ); do | ||
+ | tmsh create ltm profile client-ssl auto_$i | ||
+ | echo " | ||
+ | done | ||
+ | </ | ||
+ | |||
+ | |||
+ | ===== domains.txt ===== | ||
+ | For the domains you would like to register a certificate for you insert them into the domain.txt file. You can have as many as you like (see restrictions above!). The format is important though. | ||
+ | |||
+ | < | ||
+ | example.com www.example.com | ||
+ | example.dk wiki.example.dk | ||
+ | example.se upload.example.se download.example.se | ||
+ | </ | ||
+ | |||
+ | |||
+ | From the above example you can see that the " | ||
+ | The above domain example will generate three certificates. | ||
+ | ===== hook.sh ===== | ||
+ | Yet another updated version of the hook script. This one fits the " | ||
+ | |||
+ | Update 23-01-2021: Now the hook script cleans up the old certificate, | ||
+ | |||
+ | < | ||
+ | |||
+ | # | ||
+ | |||
+ | deploy_challenge() { | ||
+ | local DOMAIN=" | ||
+ | |||
+ | # This hook is called once for every domain that needs to be | ||
+ | # validated, including any alternative names you may have listed. | ||
+ | # | ||
+ | # Parameters: | ||
+ | # - DOMAIN | ||
+ | # The domain name (CN or subject alternative name) being | ||
+ | # | ||
+ | # - TOKEN_FILENAME | ||
+ | # The name of the file containing the token to be served for HTTP | ||
+ | # | ||
+ | # / | ||
+ | # - TOKEN_VALUE | ||
+ | # The token value that needs to be served for validation. For DNS | ||
+ | # | ||
+ | # TXT record. For HTTP validation it is the value that is expected | ||
+ | # be found in the $TOKEN_FILENAME file. | ||
+ | |||
+ | # Simple example: Use nsupdate with local named | ||
+ | # printf ' | ||
+ | response=$(cat $WELLKNOWN/ | ||
+ | cmd=' | ||
+ | $cmd | ||
+ | |||
+ | } | ||
+ | |||
+ | clean_challenge() { | ||
+ | local DOMAIN=" | ||
+ | |||
+ | # This hook is called after attempting to validate each domain, | ||
+ | # whether or not validation was successful. Here you can delete | ||
+ | # files or DNS records that are no longer needed. | ||
+ | # | ||
+ | # The parameters are the same as for deploy_challenge. | ||
+ | |||
+ | # Simple example: Use nsupdate with local named | ||
+ | # printf ' | ||
+ | | ||
+ | $cmd | ||
+ | } | ||
+ | |||
+ | deploy_cert() { | ||
+ | local DOMAIN=" | ||
+ | |||
+ | # This hook is called once for each certificate that has been | ||
+ | # produced. Here you might, for instance, copy your new certificates | ||
+ | # to service-specific locations and reload the service. | ||
+ | # | ||
+ | # Parameters: | ||
+ | # - DOMAIN | ||
+ | # The primary domain name, i.e. the certificate common | ||
+ | # name (CN). | ||
+ | # - KEYFILE | ||
+ | # The path of the file containing the private key. | ||
+ | # - CERTFILE | ||
+ | # The path of the file containing the signed certificate. | ||
+ | # - FULLCHAINFILE | ||
+ | # The path of the file containing the full certificate chain. | ||
+ | # - CHAINFILE | ||
+ | # The path of the file containing the intermediate certificate(s). | ||
+ | # - TIMESTAMP | ||
+ | # | ||
+ | |||
+ | # Simple example: Copy file to nginx config | ||
+ | # cp " | ||
+ | # systemctl reload nginx | ||
+ | now=$(date +%Y-%m-%d) | ||
+ | profile=auto_${DOMAIN} | ||
+ | name=${DOMAIN}_${now} | ||
+ | cert=${name}.crt | ||
+ | key=${name}.key | ||
+ | ocsp=" | ||
+ | # Keep current/old certificate/ | ||
+ | old_cert=$(tmsh list ltm profile client-ssl ${profile} cert |grep cert|awk ' | ||
+ | old_key=$(tmsh list ltm profile client-ssl ${profile} key |grep key|awk ' | ||
+ | |||
+ | tmsh install sys crypto key ${name} from-local-file ${KEYFILE} | ||
+ | tmsh install sys crypt cert ${name} from-local-file ${FULLCHAINFILE} | ||
+ | tmsh modify sys crypto cert ${name} cert-validation-options { ocsp } cert-validators replace-all-with { $ocsp } issuer-cert R3_Letsencrypt_issuer | ||
+ | tmsh modify ltm profile client-ssl ${profile} cert-key-chain replace-all-with { default { key ${name} cert ${name} } } | ||
+ | # Cleanup old certificate/ | ||
+ | still_in_use=$(tmsh list ltm profile client-ssl one-line|grep $old_cert|wc -l) | ||
+ | if [ $still_in_use -eq 0 ] | ||
+ | then | ||
+ | tmsh delete sys crypto cert $old_cert | ||
+ | tmsh delete sys crypto key $old_key | ||
+ | fi | ||
+ | |||
+ | |||
+ | } | ||
+ | |||
+ | deploy_ocsp() { | ||
+ | local DOMAIN=" | ||
+ | |||
+ | # This hook is called once for each updated ocsp stapling file that has | ||
+ | # been produced. Here you might, for instance, copy your new ocsp stapling | ||
+ | # files to service-specific locations and reload the service. | ||
+ | # | ||
+ | # Parameters: | ||
+ | # - DOMAIN | ||
+ | # The primary domain name, i.e. the certificate common | ||
+ | # name (CN). | ||
+ | # - OCSPFILE | ||
+ | # The path of the ocsp stapling file | ||
+ | # - TIMESTAMP | ||
+ | # | ||
+ | |||
+ | # Simple example: Copy file to nginx config | ||
+ | # cp " | ||
+ | # systemctl reload nginx | ||
+ | } | ||
+ | |||
+ | |||
+ | unchanged_cert() { | ||
+ | local DOMAIN=" | ||
+ | |||
+ | # This hook is called once for each certificate that is still | ||
+ | # valid and therefore wasn't reissued. | ||
+ | # | ||
+ | # Parameters: | ||
+ | # - DOMAIN | ||
+ | # The primary domain name, i.e. the certificate common | ||
+ | # name (CN). | ||
+ | # - KEYFILE | ||
+ | # The path of the file containing the private key. | ||
+ | # - CERTFILE | ||
+ | # The path of the file containing the signed certificate. | ||
+ | # - FULLCHAINFILE | ||
+ | # The path of the file containing the full certificate chain. | ||
+ | # - CHAINFILE | ||
+ | # The path of the file containing the intermediate certificate(s). | ||
+ | } | ||
+ | |||
+ | invalid_challenge() { | ||
+ | local DOMAIN=" | ||
+ | |||
+ | # This hook is called if the challenge response has failed, so domain | ||
+ | # owners can be aware and act accordingly. | ||
+ | # | ||
+ | # Parameters: | ||
+ | # - DOMAIN | ||
+ | # The primary domain name, i.e. the certificate common | ||
+ | # name (CN). | ||
+ | # - RESPONSE | ||
+ | # The response that the verification server returned | ||
+ | |||
+ | # Simple example: Send mail to root | ||
+ | # printf " | ||
+ | } | ||
+ | |||
+ | request_failure() { | ||
+ | local STATUSCODE=" | ||
+ | |||
+ | # This hook is called when an HTTP request fails (e.g., when the ACME | ||
+ | # server is busy, returns an error, etc). It will be called upon any | ||
+ | # response code that does not start with ' | ||
+ | # about problems with requests. | ||
+ | # | ||
+ | # Parameters: | ||
+ | # - STATUSCODE | ||
+ | # The HTML status code that originated the error. | ||
+ | # - REASON | ||
+ | # The specified reason for the error. | ||
+ | # - REQTYPE | ||
+ | # The kind of request that was made (GET, POST...) | ||
+ | # - HEADERS | ||
+ | # HTTP headers returned by the CA | ||
+ | |||
+ | # Simple example: Send mail to root | ||
+ | # printf " | ||
+ | } | ||
+ | |||
+ | generate_csr() { | ||
+ | local DOMAIN=" | ||
+ | |||
+ | # This hook is called before any certificate signing operation takes place. | ||
+ | # It can be used to generate or fetch a certificate signing request with external | ||
+ | # tools. | ||
+ | # The output should be just the cerificate signing request formatted as PEM. | ||
+ | # | ||
+ | # Parameters: | ||
+ | # - DOMAIN | ||
+ | # The primary domain as specified in domains.txt. This does not need to | ||
+ | # match with the domains in the CSR, it's basically just the directory name. | ||
+ | # - CERTDIR | ||
+ | # | ||
+ | # for storing additional files. | ||
+ | # - ALTNAMES | ||
+ | # All domain names for the current certificate as specified in domains.txt. | ||
+ | # | ||
+ | |||
+ | # Simple example: Look for pre-generated CSRs | ||
+ | # if [ -e " | ||
+ | # cat " | ||
+ | # fi | ||
+ | } | ||
+ | |||
+ | startup_hook() { | ||
+ | # This hook is called before the cron command to do some initial tasks | ||
+ | # (e.g. starting a webserver). | ||
+ | |||
+ | : | ||
+ | } | ||
+ | |||
+ | exit_hook() { | ||
+ | # This hook is called at the end of the cron command and can be used to | ||
+ | # do some final (cleanup or other) tasks. | ||
+ | |||
+ | : | ||
+ | } | ||
+ | |||
+ | HANDLER=" | ||
+ | if [[ " | ||
+ | " | ||
+ | fi | ||
+ | </ | ||
+ | |||
+ | If you plan make use of OCSP stapling you can change a part of the hook deploy script. Insert a snippet before this line - Note this will only work on v.13 as the syntax for OCSP has changed between v.12 and v.13: | ||
+ | |||
+ | |||
+ | < | ||
+ | tmsh modify ltm profile client-ssl ${profile} cert-key-chain replace-all-with { default { key $key cert $cert } } | ||
+ | </ | ||
+ | |||
+ | So it ends up like this: | ||
+ | |||
+ | < | ||
+ | ... | ||
+ | now=$(date +%Y-%m-%d) | ||
+ | profile=auto_${DOMAIN} | ||
+ | name=${DOMAIN}_${now} | ||
+ | cert=${name}.crt | ||
+ | key=${name}.key | ||
+ | ocsp=" | ||
+ | tmsh install sys crypto key ${name} from-local-file ${KEYFILE} | ||
+ | tmsh install sys crypt cert ${name} from-local-file ${FULLCHAINFILE} | ||
+ | tmsh modify sys crypto cert $cert cert-validation-options { ocsp } cert-validators replace-all-with { $ocsp } issuer-cert letsencrypt_full_chain.crt | ||
+ | tmsh modify ltm profile client-ssl ${profile} cert-key-chain replace-all-with { default { key $key cert | ||
+ | $cert } } | ||
+ | ... | ||
+ | </ | ||
+ | |||
+ | * " | ||
+ | < | ||
+ | sys crypto cert-validator ocsp letsencrypt-ocsp { | ||
+ | | ||
+ | | ||
+ | | ||
+ | | ||
+ | | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | " | ||
+ | | ||
+ | |||
+ | If you plan make use of OCSP stapling you can change a part of the hook deploy script. Insert a snippet before this line - Note this will only work on v.13 as the syntax for OCSP has changed between v.12 and v.13: | ||
+ | |||
+ | |||
+ | < | ||
+ | tmsh modify ltm profile client-ssl ${profile} cert-key-chain replace-all-with { default { key $key cert $cert } } | ||
+ | </ | ||
+ | |||
+ | So it ends up like this: | ||
+ | |||
+ | < | ||
+ | ... | ||
+ | now=$(date +%Y-%m-%d) | ||
+ | profile=auto_${DOMAIN} | ||
+ | name=${DOMAIN}_${now} | ||
+ | cert=${name}.crt | ||
+ | key=${name}.key | ||
+ | ocsp=" | ||
+ | tmsh install sys crypto key ${name} from-local-file ${KEYFILE} | ||
+ | tmsh install sys crypt cert ${name} from-local-file ${FULLCHAINFILE} | ||
+ | tmsh modify sys crypto cert $cert cert-validation-options { ocsp } cert-validators replace-all-with { $ocsp } issuer-cert letsencrypt_full_chain.crt | ||
+ | tmsh modify ltm profile client-ssl ${profile} cert-key-chain replace-all-with { default { key $key cert | ||
+ | $cert } } | ||
+ | ... | ||
+ | </ | ||
+ | |||
+ | * " | ||
+ | < | ||
+ | sys crypto cert-validator ocsp letsencrypt-ocsp { | ||
+ | | ||
+ | | ||
+ | | ||
+ | | ||
+ | | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | * {{: | ||
+ | |||
+ | Update 23-01-2021: The chain file used in the OCSP profile needs to be updated to match the current issuer. | ||
+ | |||
+ | * {{ : | ||
+ | |||
+ | ===== config ===== | ||
+ | Another change to the script is the config file. It has changed its name and gotten some new features. I dont' make use of them, so again it is only minor changes. I assume the hook file is in the same directory as the letsencrypt script. | ||
+ | |||
+ | < | ||
+ | ######################################################## | ||
+ | # This is the main config file for letsencrypt.sh | ||
+ | # # | ||
+ | # This file is looked for in the following locations: | ||
+ | # $SCRIPTDIR/ | ||
+ | # / | ||
+ | # / | ||
+ | # ${PWD}/ | ||
+ | # # | ||
+ | # Default values of this config are in comments | ||
+ | ######################################################## | ||
+ | |||
+ | # Path to certificate authority (default: https:// | ||
+ | # | ||
+ | |||
+ | # Path to license agreement (default: https:// | ||
+ | # | ||
+ | |||
+ | # Which challenge should be used? Currently http-01 and dns-01 are supported | ||
+ | CHALLENGETYPE=" | ||
+ | |||
+ | # Path to a directory containing additional config files, allowing to override | ||
+ | # the defaults found in the main configuration file. Additional config files | ||
+ | # in this directory needs to be named with a ' | ||
+ | # default: < | ||
+ | #CONFIG_D= | ||
+ | |||
+ | # Base directory for account key, generated certificates and list of domains (default: $SCRIPTDIR -- uses config directory if undefined) | ||
+ | # | ||
+ | |||
+ | # File containing the list of domains to request certificates for (default: $BASEDIR/ | ||
+ | # | ||
+ | |||
+ | # Output directory for generated certificates | ||
+ | # | ||
+ | |||
+ | # Output directory for challenge-tokens to be served by webserver or deployed in HOOK (default: $BASEDIR/ | ||
+ | # | ||
+ | |||
+ | # Location of private account key (default: $BASEDIR/ | ||
+ | # | ||
+ | |||
+ | # Location of private account registration information (default: $BASEDIR/ | ||
+ | # | ||
+ | |||
+ | # Default keysize for private keys (default: 4096) | ||
+ | # | ||
+ | |||
+ | # Path to openssl config file (default: < | ||
+ | # | ||
+ | |||
+ | # Program or function called in certain situations | ||
+ | # | ||
+ | # After generating the challenge-response, | ||
+ | # Given arguments: clean_challenge|deploy_challenge altname token-filename token-content | ||
+ | # | ||
+ | # After successfully signing certificate | ||
+ | # Given arguments: deploy_cert domain path/ | ||
+ | # | ||
+ | # BASEDIR and WELLKNOWN variables are exported and can be used in an external program | ||
+ | # default: < | ||
+ | HOOK=" | ||
+ | |||
+ | # Chain clean_challenge|deploy_challenge arguments together into one hook call per certificate (default: no) | ||
+ | # | ||
+ | |||
+ | # Minimum days before expiration to automatically renew certificate (default: 30) | ||
+ | # | ||
+ | |||
+ | # Regenerate private keys instead of just signing new certificates on renewal (default: yes) | ||
+ | # | ||
+ | |||
+ | # Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1 | ||
+ | # | ||
+ | |||
+ | # E-mail to use during the registration (default: < | ||
+ | # | ||
+ | |||
+ | # Lockfile location, to prevent concurrent access (default: $BASEDIR/ | ||
+ | # | ||
+ | |||
+ | # Option to add CSR-flag indicating OCSP stapling to be mandatory (default: no) | ||
+ | # | ||
+ | </ | ||
+ | |||
+ | |||
+ | ===== dehydrated ===== | ||
+ | This is now the main script. For now it is handled the same way as the old letsencrypt script. | ||
+ | |||
+ | You run it like this: " | ||
+ | |||
+ | ===== Wrapper ===== | ||
+ | As dehydrated only should run on the active unit I've made a wrapper which is making sure this is handled. Also I like to get an email whenever the script has run so I know the status of my certificates and if any errors had occurred. | ||
+ | Regarding the mail notification, | ||
+ | |||
+ | ==== wrapper.sh ==== | ||
+ | < | ||
+ | #!/bin/bash | ||
+ | |||
+ | MAILRCPT=" | ||
+ | MAILFROM=" | ||
+ | MAILSERVER=" | ||
+ | MAILSERVERPORT=" | ||
+ | LOGFILE="/ | ||
+ | DATE=$(date) | ||
+ | SENDMAIL="/ | ||
+ | MAILFILE="/ | ||
+ | date > | ||
+ | echo "" | ||
+ | |||
+ | send_status_mail () { | ||
+ | local message=$1 | ||
+ | cat << | ||
+ | From: $MAILFROM | ||
+ | To: $MAILRCPT | ||
+ | Date: $DATE | ||
+ | Subject: $message | ||
+ | EOF | ||
+ | cat $LOGFILE >> $MAILFILE | ||
+ | $SENDMAIL $MAILSERVER $MAILSERVERPORT $MAILFILE >/ | ||
+ | } | ||
+ | |||
+ | cd / | ||
+ | |||
+ | |||
+ | ME=`echo $HOSTNAME|awk -F. ' | ||
+ | ACTIVE=$(tmsh show cm failover-status | grep ACTIVE | wc -l) | ||
+ | |||
+ | if [[ " | ||
+ | echo "Unit is active - proceeding..." | ||
+ | exec >/ | ||
+ | ./ | ||
+ | send_status_mail "Lets Encrypt Report $ME" | ||
+ | |||
+ | |||
+ | fi | ||
+ | </ | ||
+ | |||
+ | ==== send_mail ==== | ||
+ | < | ||
+ | # | ||
+ | # | ||
+ | # sends a properly formatted file to smtp server | ||
+ | # usage: send_email | ||
+ | # | ||
+ | # blatantly copied from Peter Vibert’s expect script: | ||
+ | # http:// | ||
+ | # source: http:// | ||
+ | # | ||
+ | if {$argc< | ||
+ | | ||
+ | | ||
+ | exit | ||
+ | } | ||
+ | set mailserver [lrange $argv 0 0] | ||
+ | set portno [lrange $argv 1 1] | ||
+ | set cfile [lrange $argv 2 2] | ||
+ | send " | ||
+ | set fp [open " | ||
+ | set content [read $fp] | ||
+ | set hostname [exec hostname] | ||
+ | |||
+ | # extract the from address from message file | ||
+ | # must be in one of two forms: | ||
+ | # From: " | ||
+ | # or | ||
+ | # From: recipient@foo.com | ||
+ | |||
+ | set from [exec grep " | ||
+ | set quoted [string match "? | ||
+ | if [expr $quoted > 0 ] { | ||
+ | set from [exec echo " | ||
+ | } else { | ||
+ | set from [exec echo " | ||
+ | } | ||
+ | |||
+ | # extract the to address – same as from (see above) | ||
+ | set to [exec grep " | ||
+ | set quoted [string match "? | ||
+ | if [expr $quoted > 0 ] { | ||
+ | set to [exec echo " | ||
+ | } else { | ||
+ | set to [exec echo " | ||
+ | } | ||
+ | |||
+ | spawn telnet $mailserver $portno | ||
+ | expect " | ||
+ | send_user " | ||
+ | exit | ||
+ | } "2?? *" { | ||
+ | } "4?? *" { | ||
+ | exit | ||
+ | } " | ||
+ | send_user " | ||
+ | exit | ||
+ | } " | ||
+ | send_user " | ||
+ | exit | ||
+ | } timeout { | ||
+ | send_user " | ||
+ | exit | ||
+ | } | ||
+ | send "HELO $hostname\r" | ||
+ | expect "2?? *" { | ||
+ | } "5?? *" { | ||
+ | exit | ||
+ | } "4?? *" { | ||
+ | exit | ||
+ | } | ||
+ | send "MAIL FROM: < | ||
+ | expect "2?? *" { | ||
+ | } "5?? *" { | ||
+ | exit | ||
+ | } "4?? *" { | ||
+ | exit | ||
+ | } | ||
+ | send "RCPT TO: < | ||
+ | expect "2?? *" { | ||
+ | } "5?? *" { | ||
+ | exit | ||
+ | } "4?? *" { | ||
+ | exit | ||
+ | } | ||
+ | send " | ||
+ | expect "3?? *" { | ||
+ | } "5?? *" { | ||
+ | exit | ||
+ | } "4?? *" { | ||
+ | exit | ||
+ | } | ||
+ | log_user 0 | ||
+ | send " | ||
+ | set timeout 1 | ||
+ | expect " | ||
+ | close $fp | ||
+ | send " | ||
+ | expect " | ||
+ | expect "2?? *" { | ||
+ | } "5?? *" { | ||
+ | exit | ||
+ | } "4?? *" { | ||
+ | exit | ||
+ | } | ||
+ | send_user " | ||
+ | send " | ||
+ | exit | ||
+ | </ | ||
+ | ===== Install an iScript ===== | ||
+ | |||
+ | We now have all the scripts and profiles in place the Let's Encrypt certificates now we only need to automate the execution. | ||
+ | |||
+ | For this we make use of iScripts which is part of the iCall framework. This enable us to run the script periodically and without having to reset cronjobs whenever we do an upgrade, with this it rides along automatically as it is part of the configuration. Another great advantage is that is it synchronized in a cluster setup so you make it once and it gets installed on all the cluster members automatically. | ||
+ | |||
+ | The iCall framework is poorly documented (at the moment) but by guessing and reading through some iApps I came up with this solution. | ||
+ | It is basically an iScript which runs the wrapper script and a periodic handler running the script every week. | ||
+ | |||
+ | I assume the wrapper script i placed in / | ||
+ | |||
+ | < | ||
+ | tmsh create sys icall script letsencrypt | ||
+ | tmsh modify sys icall script letsencrypt definition { exec / | ||
+ | tmsh create sys icall handler periodic letsencrypt first-occurrence 2017-07-21: | ||
+ | tmsh save sys config | ||
+ | </ | ||
+ | |||
+ | This is what the lines do: | ||
+ | - Create the iscript | ||
+ | - Insert the only line needed, the execution of the wrapper script (remember to use the correct path for the script) | ||
+ | - Create the handler and make it execute the iscript once a week starting on Friday 21/7-17 00:00. I use " | ||
+ | - Save the configuration to disk | ||
+ | |||
+ | You should now have the following configuration: | ||
+ | |||
+ | < | ||
+ | # the iscript | ||
+ | > tmsh list sys icall script letsencrypt | ||
+ | sys icall script letsencrypt { | ||
+ | app-service none | ||
+ | definition { | ||
+ | exec / | ||
+ | } | ||
+ | description none | ||
+ | events none | ||
+ | } | ||
+ | # the event handler | ||
+ | > tmsh list sys icall handler periodic letsencrypt | ||
+ | sys icall handler periodic letsencrypt { | ||
+ | first-occurrence 2017-07-21: | ||
+ | interval 604800 | ||
+ | script letsencrypt | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | The iCall logic also has an event options which could make the script execute based on something coming out into the ltm logs like a certificate which is about to expire. You could also make it part of a cleanup procedure. So my example has room for enhancements :-P | ||
+ | ===== Sources, References and Files ===== | ||
+ | Given what ever skills I possess this Let's Encrypt automation scripting on a BigIP would never have been possible without the knowledge of Lukas Schauer and David Dindorp - Thank you guys, you are amazing!!! | ||
+ | |||
+ | |||
+ | You can find the scripts on GitHub here: < | ||
+ | |||