Skip to content

Commit

Permalink
Add support for cerbot automation
Browse files Browse the repository at this point in the history
  • Loading branch information
taylorific committed Jan 10, 2025
1 parent e705461 commit 696e473
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 10 deletions.
74 changes: 71 additions & 3 deletions cookbooks/boxcutter_acme/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,73 @@ Configures ACME-based clients (Automated Certificate Management Environment)
that make it possible to automate the issuance and renewal of SSL certificates
without needing human interaction.

## Recipes
Two different systems are supported:
- certbot - Python-based Let's Encrypt client and ACME library.
- lego - Go languaged-based Let's Encrypt client and ACME library.

- `boxcutter_acme::lego` - Let’s Encrypt client and ACME library written in Go.
## Using certbot to automate the issuance and renewal of SSL certificates

Add `include_recipe 'boxcutter_acme::certbot'` to install the certbot Let's
Encrypt client and ACME library. A Python virtual environment will be
created in `/opt/certbot/venv`.

The certbot binary is installed in `/opt/certbot/venv/bin/certbot`.

You can specify SSL certificate configurations to be managed under
`node['boxcutter_acme']['certbot']['config']`.

For example:

```
node.default['boxcutter_acme']['certbot']['config'] = {
'example' => {
'domains' => 'server.example.com',
'certbot_bin' => '/opt/certbot/venv/bin/certbot',
'renew_script_path' => '/opt/certbot/bin/lego_renew.sh.erb',
'email' => '[email protected]',
'cloudflare_ini' => '/etc/chef/cloudflare.ini',
'extra_args' => [
'--dns-cloudflare',
'--dns-cloudflare-credentials /etc/chef/cloudflare.ini',
'--test-cert',
].join(' '),
},
}
```

### Fields

Required fields:

* `renew_script_path`: Full path where the automation should put the script
that obtains and renews
* `email`: Email used for registration and recovery contact.
* `domains`: Array containing the list of domain values to be added to the SSL
certificate
* `certbot_bin`

Optional fields:

* `config_dir`: Specifies the directory where Certbot saves its configuration
and certificates. Default: `/etc/letsencrypt`.
* `logs_dir`: Specifies the directory where Certbot saves logs.
Default: `/var/log/letsencrypt`.
* `work_dir`: Specifies the working directory for temporary files.
Default: `/var/lib/letsencrypt`.
* `certbot_bin`


* `renew_days`: The number of days left on a certificate to renew it. (default: 30)
* `server`: Let's Encrypt ACME server to be used. If you'd like to test
something without issuing real certificates, you can use the staging
endpoint `https://acme-staging-v02.api.letsencrypt.org/directory`.
* `extra_parameters`: Additional global options to be added to the command
line, not covered by required fields (`--dns-resolvers value`). Default is `--http`.
* `extra_environment`: Additional environment variables to be configured for
the renew script. Usually environment variables required for the DNS
tokens.

## Usage
## Using lego to automate the issuance and renewal of SSL certificates

Add `include_recipe 'boxcutter_acme::lego'` to install the Let's Encryt client
and ACME library for Go. The LEGO binaries will be installed to `/opt/lego`
Expand Down Expand Up @@ -60,3 +122,9 @@ Optional fields:
* `extra_environment`: Additional environment variables to be configured for
the renew script. Usually environment variables required for the DNS
tokens.

## Recipes

- `boxcutter_acme::lego` - Let’s Encrypt client and ACME library written in Go.

References: https://github.com/schubergphilis/chef-acme/blob/master/README.md
10 changes: 8 additions & 2 deletions cookbooks/boxcutter_acme/attributes/default.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
default['boxcutter_acme']['lego'] = {
'config' => {},
default['boxcutter_acme'] = {
'certbot' => {
'cloudflare_api_key' => nil,
'config' => {},
},
'lego' => {
'config' => {},
},
}
10 changes: 10 additions & 0 deletions cookbooks/boxcutter_acme/kitchen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ suites:
inspec_tests:
- test/integration/default
attributes:
lifecycle:
pre_converge:
- remote: |
bash -xc '
set +x
mkdir -p /etc/cinc
ln -s /etc/cinc /etc/chef
echo "<%= ENV['OP_SERVICE_ACCOUNT_TOKEN'] %>" > /etc/chef/op_service_account_token
set -x
'
- name: lego
provisioner:
Expand Down
8 changes: 8 additions & 0 deletions cookbooks/boxcutter_acme/libraries/default.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module Boxcutter
class Acme
def self.to_bash_array(ruby_array)
bash_array = ruby_array.map { |item| "\"#{item}\"" }.join(' ')
"(#{bash_array})"
end
end
end
74 changes: 69 additions & 5 deletions cookbooks/boxcutter_acme/recipes/certbot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,78 @@

include_recipe 'boxcutter_python::system'

%w{
/opt/certbot
/opt/certbot/bin
}.each do |dir|
directory dir do
owner 'root'
group 'root'
mode '0700'
end
end

boxcutter_python_virtualenv '/opt/certbot/venv'

%w{
certbot
certbot-dns-cloudflare
}.each do |pkg|
boxcutter_python_pip pkg do
boxcutter_python_pip 'certbot' do
virtualenv '/opt/certbot/venv'
action :upgrade
end

# Only bother configuring cloudflare plugins if we're provided an api token
if node.exist?('boxcutter_acme', 'certbot', 'cloudflare_api_token') ||
node.run_state.key?('boxcutter_acme') \
&& node.run_state['boxcutter_acme'].key?('certbot') \
&& node.run_state['boxcutter_acme']['certbot'].key?('cloudflare_api_token')

boxcutter_python_pip 'certbot-dns-cloudflare' do
virtualenv '/opt/certbot/venv'
action :upgrade
end

cloudflare_api_token = node['boxcutter_acme']['certbot']['cloudflare_api_token']
if node.run_state.key?('boxcutter_acme') \
&& node.run_state['boxcutter_acme'].key?('certbot') \
&& node.run_state['boxcutter_acme']['certbot'].key?('cloudflare_api_token')
cloudflare_api_token = node.run_state['boxcutter_acme']['certbot']['cloudflare_api_token']
end

template '/etc/chef/cloudflare.ini' do
source 'cloudflare.ini.erb'
owner 'root'
group 'root'
mode 0400
variables(
cloudflare_api_token: cloudflare_api_token,
)
end
end

node.default['boxcutter_acme']['certbot']['config'].each do |name, config|
execute "#{name} obtain certificate" do
command config['renew_script_path']
action :nothing
end

template config['renew_script_path'] do
source 'certbot_renew.sh.erb'
owner 'root'
group 'root'
mode 0700
variables(
certbot_bin: config['certbot_bin'],
domains: Boxcutter::Acme.to_bash_array(config['domains']),
email: config['email'],
cloudflare_ini: config['cloudflare_ini'],
extra_args: config['extra_args'],
)
notifies :run, "execute[#{name} obtain certificate]", :immediately
end

node.default['fb_timers']['jobs'][name] = {
'calendar' => FB::Systemd::Calendar.every.weekday,
'command' => config['renew_script_path'],
'accuracy' => '1h',
'splay' => '0.5h',
}
end
50 changes: 50 additions & 0 deletions cookbooks/boxcutter_acme/templates/certbot_renew.sh.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/bin/bash

CERTBOT_BIN="<%= @certbot_bin %>"
DOMAINS=<%= @domains %>
EMAIL="<%= @email %>"
CLOUDFLARE_INI="<%= @cloudflare_ini %>"

check_certificate() {
for DOMAIN in "${DOMAINS[@]}"; do
if ! "${CERTBOT_BIN}" certificates | grep -q "Domains:.*\b$DOMAIN\b"; then
return 1 # If any domain is missing, return failure
fi
done
return 0 # All domains are covered
}

obtain_certificate() {
if check_certificate; then
echo "Certificate for all domains already exists. Skipping certificate creation."
else
echo "Creating a new certificate for domains: ${DOMAINS[*]}"
DOMAIN_ARGS=()
for DOMAIN in ${DOMAINS}; do
DOMAIN_ARGS+=("-d $DOMAIN")
done

"${CERTBOT_BIN}" certonly \
--non-interactive \
--agree-tos \
--non-interactive \
-m "${EMAIL}" \
--no-eff-email \
<%= @extra_args.nil? ? '' : "#{@extra_args} " -%>--preferred-challenges dns-01 \
--expand \
${DOMAIN_ARGS[@]}
fi
}

renew_certificate() {
echo "Attempting to renew SSL certificate for domains: ${DOMAINS[*]}"
"${CERTBOT_BIN}" renew
}

certificate_info() {
"${CERTBOT_BIN}" certificates
}

obtain_certificate
renew_certificate
certificate_info
2 changes: 2 additions & 0 deletions cookbooks/boxcutter_acme/templates/cloudflare.ini.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Cloudflare API token used by Certbot
dns_cloudflare_api_token = <%= @cloudflare_api_token %>
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,26 @@
# Recipe:: certbot
#

# op item get 'Cloudflare API token amazing-sheila' --vault Automation-Org
# op item get gk6bozl2ruh5v3knglpzsaml3u --vault Automation-Org --format json
node.run_state['boxcutter_acme'] ||= {}
node.run_state['boxcutter_acme']['certbot'] ||= {}
node.run_state['boxcutter_acme']['certbot']['cloudflare_api_token'] = \
Boxcutter::OnePassword.op_read('op://Automation-Org/Cloudflare API token amazing-sheila/credential')

node.default['boxcutter_acme']['certbot']['config'] = {
'nexus' => {
'renew_script_path' => '/opt/certbot/bin/certbot_renew.sh',
'certbot_bin' => '/opt/certbot/venv/bin/certbot',
'domains' => ['testy.boxcutter.net', '*.testy.boxcutter.net'],
'email' => '[email protected]',
'cloudflare_ini' => '/etc/chef/cloudflare.ini',
'extra_args' => [
'--dns-cloudflare',
'--dns-cloudflare-credentials /etc/chef/cloudflare.ini',
'--test-cert',
].join(' '),
},
}

include_recipe 'boxcutter_acme::certbot'

0 comments on commit 696e473

Please sign in to comment.