Automate Let's Encrypt DNS-01 certificate renewal with:
- certbot (manual DNS mode)
- OpenStack Designate via Terraform for
_acme-challengeTXT records - Optionally: Ansible to distribute renewed certificates to remote hosts
This repo is intentionally generic: no company-specific values. Everything is driven by variables or placeholders.
.
├── terraform/
│ ├── _txt.tf
│ ├── provider.tf
│ ├── variables.tf
│ └── terraform.tfvars.example
├── hooks/
│ ├── create_txt.sh
│ ├── clean_txt.sh
│ └── ip_addr.sh
├── ansible/
│ ├── distribute_certs.yaml
│ └── inventory.yaml.example
└── cron/
└── cron_example.txt
On the host running certbot (the "control" host):
- certbot
- terraform
- openstack terraform provider
- ansible (only if you want distribution)
- OpenStack Designate access (DNS zone already created)
- SSH access from control host to target hosts where certs will be deployed (for Ansible mode)
Edit terraform/terraform.tfvars.example and copy it to terraform/terraform.tfvars:
zone_id = "YOUR_DESIGNATE_ZONE_ID"
# For a wildcard covering *.example.com and example.com itself:
# base_domain = "example.com"
#
# For a single host like test.example.com:
# base_domain = "test.example.com"
base_domain = "example.com"
# Optional overrides
record_name = "_acme-challenge"
record_ttl = 10Configure OpenStack credentials either via OS_* environment variables or by uncommenting provider attributes in terraform/provider.tf.
You can test the record creation manually before integrating with certbot:
cd terraform
terraform init
terraform apply -auto-approve -var="challenge=test-value"
terraform destroy -auto-approve -var="challenge=test-value"The hook scripts live in hooks/:
create_txt.sh– creates the TXT record for the current ACME challengeclean_txt.sh– deletes the TXT record afterwardip_addr.sh– small helper to output a JSON object with the IP of a given interface
By default, the scripts assume:
- they’re installed under
/opt/certbot_dns/hooks - the Terraform project is
/opt/certbot_dns/terraform
You can override the Terraform directory with CERTBOT_DNS_TERRAFORM_DIR:
export CERTBOT_DNS_TERRAFORM_DIR=/some/other/path/terraformip_addr.sh uses the interface from CERTBOT_LOCAL_IFACE or falls back to eth0:
export CERTBOT_LOCAL_IFACE=enp3s0
./hooks/ip_addr.shDon’t forget to make the scripts executable:
chmod +x hooks/*.shansible/distribute_certs.yaml copies fullchain.pem and privkey.pem from the control host to each target host.
-
Define your target hosts in
ansible/inventory.yaml.exampleand copy toinventory.yaml:all: hosts: web1.example.com: web2.example.com:
-
Run the playbook manually to test:
ansible-playbook -i ansible/inventory.yaml ansible/distribute_certs.yaml -u ubuntu --private-key /path/to/ansible_private_key -e "local_cert_dir=/opt/certificates" -e "remote_cert_dir=/opt/certificates"
local_cert_diris where the certbot-renewed certs live on the control host (default/opt/certificates).remote_cert_diris where you want them on the target hosts (also default/opt/certificates).
You can use this project in two main ways.
This is the "all-in" mode: certbot, Designate via Terraform, copy certs locally, then distribute via Ansible.
Example for a wildcard certificate (*.example.com):
CERTBOT_DNS_TERRAFORM_DIR=/opt/certbot_dns/terraform certbot certonly --manual --preferred-challenges dns --manual-auth-hook /opt/certbot_dns/hooks/create_txt.sh --manual-cleanup-hook /opt/certbot_dns/hooks/clean_txt.sh --agree-tos --email you@example.com -d "*.example.com" --force-renewalAfter certbot succeeds, you might copy and distribute:
cp -f /etc/letsencrypt/live/example.com/*.pem /opt/certificates/
ansible-playbook -i ansible/inventory.yaml ansible/distribute_certs.yaml -u ubuntu --private-key /path/to/ansible_private_key -e "local_cert_dir=/opt/certificates" -e "remote_cert_dir=/opt/certificates"The cron/cron_example.txt file includes a combined one-liner that chains all of this in a single cron job.
This mode is useful when you:
- Only care about getting the cert on the control host, or
- Want to handle distribution separately (systemd reload scripts, custom tooling, etc.).
Example for a single host certificate (test.example.com):
CERTBOT_DNS_TERRAFORM_DIR=/opt/certbot_dns/terraform certbot certonly --manual --preferred-challenges dns --manual-auth-hook /opt/certbot_dns/hooks/create_txt.sh --manual-cleanup-hook /opt/certbot_dns/hooks/clean_txt.sh --agree-tos --email you@example.com -d test.example.com --force-renewalIn this case, certbot will store the certificate under:
/etc/letsencrypt/live/test.example.com/
You can then:
- Symlink or copy those files into place for a local service, or
- Run the Ansible playbook later, if you wish, with
local_cert_dirpointing at the appropriate path.
See cron/cron_example.txt for sample cron lines for both modes.
A typical full-flow setup (run as root) looks like:
@daily certbot certonly --manual --preferred-challenges dns --manual-auth-hook /opt/certbot_dns/hooks/create_txt.sh --manual-cleanup-hook /opt/certbot_dns/hooks/clean_txt.sh --agree-tos --email you@example.com -d "*.example.com" --force-renewal && cp -f /etc/letsencrypt/live/example.com/*.pem /opt/certificates/ && ansible-playbook -i /opt/certbot_dns/ansible/inventory.yaml /opt/certbot_dns/ansible/distribute_certs.yaml -u ubuntu --private-key /path/to/ansible_private_key --ssh-common-args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'And a DNS-only setup (no distribution) might be:
@daily CERTBOT_DNS_TERRAFORM_DIR=/opt/certbot_dns/terraform certbot certonly --manual --preferred-challenges dns --manual-auth-hook /opt/certbot_dns/hooks/create_txt.sh --manual-cleanup-hook /opt/certbot_dns/hooks/clean_txt.sh --agree-tos --email you@example.com -d test.example.com --force-renewalAdjust paths, email, domains, and SSH user/key to your environment.
- Start by running each piece manually (Terraform apply/destroy, hooks, Ansible) before relying on cron.
- Use a staging ACME endpoint (e.g., Let’s Encrypt staging) while testing to avoid rate limits.
- Add logging around your hooks and Ansible run if you want more visibility from cron.
- If you don't use Ansible, you can replace the last part of the full-flow cron line with any script that restarts services or reloads load balancers, etc.