Skip to content

Commit 696e473

Browse files
committed
Add support for cerbot automation
1 parent e705461 commit 696e473

File tree

8 files changed

+240
-10
lines changed

8 files changed

+240
-10
lines changed

cookbooks/boxcutter_acme/README.md

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,73 @@ Configures ACME-based clients (Automated Certificate Management Environment)
44
that make it possible to automate the issuance and renewal of SSL certificates
55
without needing human interaction.
66

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

9-
- `boxcutter_acme::lego` - Let’s Encrypt client and ACME library written in Go.
11+
## Using certbot to automate the issuance and renewal of SSL certificates
12+
13+
Add `include_recipe 'boxcutter_acme::certbot'` to install the certbot Let's
14+
Encrypt client and ACME library. A Python virtual environment will be
15+
created in `/opt/certbot/venv`.
16+
17+
The certbot binary is installed in `/opt/certbot/venv/bin/certbot`.
18+
19+
You can specify SSL certificate configurations to be managed under
20+
`node['boxcutter_acme']['certbot']['config']`.
21+
22+
For example:
23+
24+
```
25+
node.default['boxcutter_acme']['certbot']['config'] = {
26+
'example' => {
27+
'domains' => 'server.example.com',
28+
'certbot_bin' => '/opt/certbot/venv/bin/certbot',
29+
'renew_script_path' => '/opt/certbot/bin/lego_renew.sh.erb',
30+
'email' => 'letsencrypt@example.com',
31+
'cloudflare_ini' => '/etc/chef/cloudflare.ini',
32+
'extra_args' => [
33+
'--dns-cloudflare',
34+
'--dns-cloudflare-credentials /etc/chef/cloudflare.ini',
35+
'--test-cert',
36+
].join(' '),
37+
},
38+
}
39+
```
40+
41+
### Fields
42+
43+
Required fields:
44+
45+
* `renew_script_path`: Full path where the automation should put the script
46+
that obtains and renews
47+
* `email`: Email used for registration and recovery contact.
48+
* `domains`: Array containing the list of domain values to be added to the SSL
49+
certificate
50+
* `certbot_bin`
51+
52+
Optional fields:
53+
54+
* `config_dir`: Specifies the directory where Certbot saves its configuration
55+
and certificates. Default: `/etc/letsencrypt`.
56+
* `logs_dir`: Specifies the directory where Certbot saves logs.
57+
Default: `/var/log/letsencrypt`.
58+
* `work_dir`: Specifies the working directory for temporary files.
59+
Default: `/var/lib/letsencrypt`.
60+
* `certbot_bin`
61+
62+
63+
* `renew_days`: The number of days left on a certificate to renew it. (default: 30)
64+
* `server`: Let's Encrypt ACME server to be used. If you'd like to test
65+
something without issuing real certificates, you can use the staging
66+
endpoint `https://acme-staging-v02.api.letsencrypt.org/directory`.
67+
* `extra_parameters`: Additional global options to be added to the command
68+
line, not covered by required fields (`--dns-resolvers value`). Default is `--http`.
69+
* `extra_environment`: Additional environment variables to be configured for
70+
the renew script. Usually environment variables required for the DNS
71+
tokens.
1072

11-
## Usage
73+
## Using lego to automate the issuance and renewal of SSL certificates
1274

1375
Add `include_recipe 'boxcutter_acme::lego'` to install the Let's Encryt client
1476
and ACME library for Go. The LEGO binaries will be installed to `/opt/lego`
@@ -60,3 +122,9 @@ Optional fields:
60122
* `extra_environment`: Additional environment variables to be configured for
61123
the renew script. Usually environment variables required for the DNS
62124
tokens.
125+
126+
## Recipes
127+
128+
- `boxcutter_acme::lego` - Let’s Encrypt client and ACME library written in Go.
129+
130+
References: https://github.com/schubergphilis/chef-acme/blob/master/README.md
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1-
default['boxcutter_acme']['lego'] = {
2-
'config' => {},
1+
default['boxcutter_acme'] = {
2+
'certbot' => {
3+
'cloudflare_api_key' => nil,
4+
'config' => {},
5+
},
6+
'lego' => {
7+
'config' => {},
8+
},
39
}

cookbooks/boxcutter_acme/kitchen.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ suites:
7171
inspec_tests:
7272
- test/integration/default
7373
attributes:
74+
lifecycle:
75+
pre_converge:
76+
- remote: |
77+
bash -xc '
78+
set +x
79+
mkdir -p /etc/cinc
80+
ln -s /etc/cinc /etc/chef
81+
echo "<%= ENV['OP_SERVICE_ACCOUNT_TOKEN'] %>" > /etc/chef/op_service_account_token
82+
set -x
83+
'
7484
7585
- name: lego
7686
provisioner:
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module Boxcutter
2+
class Acme
3+
def self.to_bash_array(ruby_array)
4+
bash_array = ruby_array.map { |item| "\"#{item}\"" }.join(' ')
5+
"(#{bash_array})"
6+
end
7+
end
8+
end

cookbooks/boxcutter_acme/recipes/certbot.rb

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,78 @@
1818

1919
include_recipe 'boxcutter_python::system'
2020

21+
%w{
22+
/opt/certbot
23+
/opt/certbot/bin
24+
}.each do |dir|
25+
directory dir do
26+
owner 'root'
27+
group 'root'
28+
mode '0700'
29+
end
30+
end
31+
2132
boxcutter_python_virtualenv '/opt/certbot/venv'
2233

23-
%w{
24-
certbot
25-
certbot-dns-cloudflare
26-
}.each do |pkg|
27-
boxcutter_python_pip pkg do
34+
boxcutter_python_pip 'certbot' do
35+
virtualenv '/opt/certbot/venv'
36+
action :upgrade
37+
end
38+
39+
# Only bother configuring cloudflare plugins if we're provided an api token
40+
if node.exist?('boxcutter_acme', 'certbot', 'cloudflare_api_token') ||
41+
node.run_state.key?('boxcutter_acme') \
42+
&& node.run_state['boxcutter_acme'].key?('certbot') \
43+
&& node.run_state['boxcutter_acme']['certbot'].key?('cloudflare_api_token')
44+
45+
boxcutter_python_pip 'certbot-dns-cloudflare' do
2846
virtualenv '/opt/certbot/venv'
2947
action :upgrade
3048
end
49+
50+
cloudflare_api_token = node['boxcutter_acme']['certbot']['cloudflare_api_token']
51+
if node.run_state.key?('boxcutter_acme') \
52+
&& node.run_state['boxcutter_acme'].key?('certbot') \
53+
&& node.run_state['boxcutter_acme']['certbot'].key?('cloudflare_api_token')
54+
cloudflare_api_token = node.run_state['boxcutter_acme']['certbot']['cloudflare_api_token']
55+
end
56+
57+
template '/etc/chef/cloudflare.ini' do
58+
source 'cloudflare.ini.erb'
59+
owner 'root'
60+
group 'root'
61+
mode 0400
62+
variables(
63+
cloudflare_api_token: cloudflare_api_token,
64+
)
65+
end
66+
end
67+
68+
node.default['boxcutter_acme']['certbot']['config'].each do |name, config|
69+
execute "#{name} obtain certificate" do
70+
command config['renew_script_path']
71+
action :nothing
72+
end
73+
74+
template config['renew_script_path'] do
75+
source 'certbot_renew.sh.erb'
76+
owner 'root'
77+
group 'root'
78+
mode 0700
79+
variables(
80+
certbot_bin: config['certbot_bin'],
81+
domains: Boxcutter::Acme.to_bash_array(config['domains']),
82+
email: config['email'],
83+
cloudflare_ini: config['cloudflare_ini'],
84+
extra_args: config['extra_args'],
85+
)
86+
notifies :run, "execute[#{name} obtain certificate]", :immediately
87+
end
88+
89+
node.default['fb_timers']['jobs'][name] = {
90+
'calendar' => FB::Systemd::Calendar.every.weekday,
91+
'command' => config['renew_script_path'],
92+
'accuracy' => '1h',
93+
'splay' => '0.5h',
94+
}
3195
end
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/bin/bash
2+
3+
CERTBOT_BIN="<%= @certbot_bin %>"
4+
DOMAINS=<%= @domains %>
5+
EMAIL="<%= @email %>"
6+
CLOUDFLARE_INI="<%= @cloudflare_ini %>"
7+
8+
check_certificate() {
9+
for DOMAIN in "${DOMAINS[@]}"; do
10+
if ! "${CERTBOT_BIN}" certificates | grep -q "Domains:.*\b$DOMAIN\b"; then
11+
return 1 # If any domain is missing, return failure
12+
fi
13+
done
14+
return 0 # All domains are covered
15+
}
16+
17+
obtain_certificate() {
18+
if check_certificate; then
19+
echo "Certificate for all domains already exists. Skipping certificate creation."
20+
else
21+
echo "Creating a new certificate for domains: ${DOMAINS[*]}"
22+
DOMAIN_ARGS=()
23+
for DOMAIN in ${DOMAINS}; do
24+
DOMAIN_ARGS+=("-d $DOMAIN")
25+
done
26+
27+
"${CERTBOT_BIN}" certonly \
28+
--non-interactive \
29+
--agree-tos \
30+
--non-interactive \
31+
-m "${EMAIL}" \
32+
--no-eff-email \
33+
<%= @extra_args.nil? ? '' : "#{@extra_args} " -%>--preferred-challenges dns-01 \
34+
--expand \
35+
${DOMAIN_ARGS[@]}
36+
fi
37+
}
38+
39+
renew_certificate() {
40+
echo "Attempting to renew SSL certificate for domains: ${DOMAINS[*]}"
41+
"${CERTBOT_BIN}" renew
42+
}
43+
44+
certificate_info() {
45+
"${CERTBOT_BIN}" certificates
46+
}
47+
48+
obtain_certificate
49+
renew_certificate
50+
certificate_info
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Cloudflare API token used by Certbot
2+
dns_cloudflare_api_token = <%= @cloudflare_api_token %>

cookbooks/boxcutter_acme/test/cookbooks/boxcutter_acme_test/recipes/certbot.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,26 @@
33
# Recipe:: certbot
44
#
55

6+
# op item get 'Cloudflare API token amazing-sheila' --vault Automation-Org
7+
# op item get gk6bozl2ruh5v3knglpzsaml3u --vault Automation-Org --format json
8+
node.run_state['boxcutter_acme'] ||= {}
9+
node.run_state['boxcutter_acme']['certbot'] ||= {}
10+
node.run_state['boxcutter_acme']['certbot']['cloudflare_api_token'] = \
11+
Boxcutter::OnePassword.op_read('op://Automation-Org/Cloudflare API token amazing-sheila/credential')
12+
13+
node.default['boxcutter_acme']['certbot']['config'] = {
14+
'nexus' => {
15+
'renew_script_path' => '/opt/certbot/bin/certbot_renew.sh',
16+
'certbot_bin' => '/opt/certbot/venv/bin/certbot',
17+
'domains' => ['testy.boxcutter.net', '*.testy.boxcutter.net'],
18+
'email' => 'letsencrypt@boxcutter.dev',
19+
'cloudflare_ini' => '/etc/chef/cloudflare.ini',
20+
'extra_args' => [
21+
'--dns-cloudflare',
22+
'--dns-cloudflare-credentials /etc/chef/cloudflare.ini',
23+
'--test-cert',
24+
].join(' '),
25+
},
26+
}
27+
628
include_recipe 'boxcutter_acme::certbot'

0 commit comments

Comments
 (0)