-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathbasic_config.pp
More file actions
419 lines (385 loc) · 15.4 KB
/
basic_config.pp
File metadata and controls
419 lines (385 loc) · 15.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# @summary Class for Haproxy Basic Config Setup
#
# @param domains The domains to be managed by haproxy.
#
# @param listens The listening configurations for haproxy. Defaults to an empty hash.
#
# @param ddos_protection Boolean to enable or disable DDoS protection. Defaults to false.
#
# @param https Boolean to enable or disable HTTPS. Defaults to true.
#
# @param http Boolean to enable or disable HTTP. Defaults to false.
#
# @param use_hsts Boolean to enable or disable HSTS. Defaults to true.
#
# @param use_lets_encrypt Boolean to enable or disable Let's Encrypt. Defaults to true.
#
# @param mode The mode of haproxy. Defaults to 'http'.
#
# @param listen_on The IP addresses for haproxy to listen on. Defaults to ['0.0.0.0'].
#
# @param encryption_ciphers The encryption ciphers to use. Defaults to 'Modern'.
#
# @param version The version of haproxy. Defaults to 'latest'.
#
# @param native_acme Internal switch for native ACME mode.
#
# @param acme_contact The contact email for Let's Encrypt ACME. Defaults to 'ops@enableit.dk'.
#
# @param acme_ca The ACME CA directory URL used internally.
#
# @groups security ddos_protection, https, use_hsts, use_lets_encrypt, encryption_ciphers, acme_contact
#
# @groups networking domains, listens, listen_on
#
# @groups configuration version
#
# @groups mode mode, http
#
class eit_haproxy::basic_config (
Eit_haproxy::Domains $domains,
Eit_haproxy::Listen $listens = {},
Boolean $ddos_protection = false,
Boolean $https = true,
Boolean $http = false,
Boolean $use_hsts = true,
Boolean $use_lets_encrypt = true,
Enum['http','tcp'] $mode = 'http',
Array[Stdlib::IP::Address,1] $listen_on = ['0.0.0.0'],
Enum['Modern','Intermediate'] $encryption_ciphers = 'Modern',
Eit_types::Version $version = 'latest',
Boolean $native_acme = false,
Eit_types::Email $acme_contact = $eit_haproxy::acme_contact,
String $acme_ca = 'https://acme-v02.api.letsencrypt.org/directory',
) {
$_use_native_acme = $native_acme
$_bootstrap_dir = '/etc/ssl/private/haproxy-bootstrap'
# https://wiki.mozilla.org/Security/Server_Side_TLS
# Strong == Intermediate
# Strict == Modern
$_ssl_options = case $encryption_ciphers {
'Intermediate': {
{
'ssl-default-bind-ciphers' => 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS', #lint:ignore:140chars
'ssl-default-bind-options' => 'no-sslv3 no-tls-tickets'
}
}
'Modern': {
{
'ssl-default-bind-ciphers' => 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256', #lint:ignore:140chars
'ssl-default-bind-options' => 'no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets'
}
}
default: {
fail("${encryption_ciphers} is not supported")
}
}
$_native_acme_global_options = if $_use_native_acme {
{
'expose-experimental-directives' => '',
'ssl-default-bind-ciphersuites' => 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256',
}
} else {
{}
}
# Default copied from haproxy::params
class { 'haproxy':
package_ensure => $version,
global_options => {
'log' => '127.0.0.1 local0',
'crt-base' => '/etc/ssl/private',
'chroot' => '/var/lib/haproxy',
'pidfile' => '/var/run/haproxy.pid',
'maxconn' => '4000',
'user' => 'haproxy',
'group' => 'haproxy',
'daemon' => '',
'stats' => 'socket /var/run/haproxy.sock mode 600 level admin',
'tune.ssl.default-dh-param' => '2048',
} + $_ssl_options + $_native_acme_global_options,
defaults_options => {
'log' => 'global',
'stats' => 'enable',
'option' => [
'redispatch',
'forwardfor',
],
'retries' => '3',
'timeout' => [
'http-request 10s',
'queue 1m',
'connect 10s',
'client 1m',
'server 1m',
'check 10s',
],
'maxconn' => '8000',
}
}
if $_use_native_acme {
concat::fragment { 'haproxy_acme_section':
target => $haproxy::config_file,
order => '12-acme',
content => epp('eit_haproxy/acme_section.epp', {
'contact' => $acme_contact,
'acme_ca' => $acme_ca,
}),
}
}
$bind_ports = [
if $https { 443 },
if $http { 80 },
].delete_undef_values
# Setup firewall rules
firewall_multi { '100 allow haproxy access':
dport => $bind_ports,
proto => 'tcp',
jump => 'accept',
}
$web_bind_ports = [
if $https { 443 },
if $http and !$_use_native_acme { 80 },
].delete_undef_values
# Setup binds and required haproxy rule
$binds = functions::array_to_hash($web_bind_ports.map |$port| {
$_ssl = if $port == 443 and $mode == 'http' {
if $_use_native_acme {
"ssl crt ${_bootstrap_dir}/haproxy-dummy.pem alpn h2,http/1.1 strict-sni"
} else {
[
'ssl',
if $use_lets_encrypt {
'crt /etc/ssl/private/letsencrypt'
},
'crt /etc/ssl/private/static-certs/combined',
].delete_undef_values.join(' ')
}
}
$listen_on.map |$listen| {
Hash([
"${listen}:${port}", $_ssl,
])
}
})
if $domains.size {
# Required Headers
# If any domains hash map has set force_https to false
# then we need to add allow_http acl to allow direct http hits
# as well otherwise haproxy will redirect all traffic to https
$_allow_http_acl = false in $domains.map |$x, $opts| {
$opts['force_https']
}.flatten.delete_undef_values
$frontend_headers = [
{ 'http-request add-header' => 'X-Forwarded-Proto https if { ssl_fc }' },
{ 'http-request set-header' => 'X-Forwarded-Port %[dst_port]' },
if $use_hsts {
{ 'http-response set-header' => 'Strict-Transport-Security include_subdomains;\ preload;\ max-age=31536000; if { ssl_fc }' }
},
{ 'http-request redirect scheme https if !{ ssl_fc }' => [
if $https and $use_lets_encrypt and !$_use_native_acme { '!is_letsencrypt' },
if $_allow_http_acl { '!allow_http' },
].delete_undef_values.join(' '),
}
].delete_undef_values
# Setup the Mapfile.
$domains_with_backend = $domains.map | $x, $opts | {
$all_domains = $opts['domains']
$domain_backend = regsubst($x, /\./, '_', 'G')
$all_domains.map |$domain| {
"${domain} ${domain_backend}"
}
}.flatten.delete_undef_values.sort
$alldomains = flatten($domains.map |$x, $opts| {
$opts['domains']
})
$public_ips = lookup('common::settings::publicips', Array, undef, [])
if $use_lets_encrypt and !$_use_native_acme {
sort_domains_on_tld($alldomains, $public_ips).each |$cn, $san| {
profile::system::certs::letsencrypt::domain { $cn:
domains => $san,
deploy_hook_command => '/opt/obmondo/bin/letsencrypt_deploy_hook.sh',
cert_host => '0.0.0.0',
}
}
}
if $_use_native_acme {
# Add Native ACME Monitoring which will loop and directly call monitor::domains
sort_domains_on_tld($alldomains, $public_ips).each |$cn, $san| {
if $cn == 'rejected_domains' {
if $san.size > 0 {
notify { "These domains got rejected because its not pointing to correct server = ${san}": }
file { '/etc/puppetlabs/facter/facts.d/obmondo_certs_rejected.json':
ensure => file,
mode => '0644',
content => stdlib::to_json({
'rejected_domains' => $san,
}),
noop => false,
}
}
} else {
# Monitor the CN, since SAN are part of same cert, so expiry would be same ofcourse :)
monitor::domains { $cn:
enable => true,
}
}
}
}
$http_only_domains_options = $domains.values.filter |$_domain_opts| {
# remove all entries that do not force HTTPS
!pick($_domain_opts.dig('force_https'), false)
}.map |$_domain_opts| {
$_domains = $_domain_opts['domains']
$_domains.map |$_domain| {
{ 'acl allow_http hdr(host)' => $_domain }
}
}.flatten
if $https and $use_lets_encrypt and !$_use_native_acme {
# letsencrypt backend
haproxy::backend { 'letsencrypt':
mode => 'http',
options => {
'server letsencrypt_0' => '127.0.0.1:63480',
},
}
}
if $mode == 'http' {
# Frontend
# NOTE: This class can only setup one frontend for all domains.
# A seperate backend is created for every domain.
$prioritized_ips = 'DK.txt'
$static_exts = '.woff2 .woff .ttf .mp4 .gif .ico .png .jpg .css .js .html .pdf'
file { '/etc/haproxy/iplists':
ensure => directory,
mode => '0755',
recurse => true,
source => 'puppet:///modules/eit_haproxy/iplists',
}
$ddos_protection_options = [
# Define a stick table with key = IPv6 address (IPv4 will be translated automatically)
# and val = number of HTTP requests made in 10s.
{ 'stick-table' => 'type ipv6 size 100k expire 10s store http_req_rate(10s)' },
# ACL which is true for URLs that point to static files.
{ 'acl' => "is_static_content path_end ${static_exts}" },
# Keep track of all requests in sticky counter 0 of the above table.
{ 'http-request' => 'track-sc0 src if !is_static_content' },
# ACL which is true if source IP belongs to the priority list.
{ 'acl' => "is_priority src -f /etc/haproxy/iplists/${prioritized_ips}" },
# ACLs which return true if source IP crosses their rate limit,
# measured against sticky counter 0 of above table.
{ 'acl' => 'priority_rl_reached sc_http_req_rate(0) gt 600' },
{ 'acl' => 'ww_rl_reached sc_http_req_rate(0) gt 250' },
# If source is in the prioritized ip range, then follow the prioritized rate limit.
{ 'http-request' => 'deny deny_status 429 if is_priority priority_rl_reached' },
# If source is not from a prioritized ip (rest of the world) then follow the worldwide rate limit.
{ 'http-request' => 'deny deny_status 429 if !is_priority ww_rl_reached' },
]
$_native_acme_frontend_options = if $_use_native_acme {
$domains.filter |$group_name, $opts| {
if $opts['force_https'] { true } else { false }
}.map |$group_name, $opts| {
$_sorted_domains_map = sort_domains_on_tld($opts['domains'], $public_ips)
$_sorted_domains = $_sorted_domains_map.map |$cn, $san| {
if $cn != 'rejected_domains' { $san }
}.flatten.delete_undef_values.uniq
$_all_domains_in_group = if $_sorted_domains.empty {
$opts['domains'].sort.uniq.join(',')
} else {
$_sorted_domains.join(',')
}
$_cert_filename = regsubst($group_name, /[^a-zA-Z0-9.-]/, '_', 'G')
Hash(['ssl-f-use', "crt ${_bootstrap_dir}/${_cert_filename}.pem acme LE domains ${_all_domains_in_group}"])
}
} else {
[]
}
haproxy::frontend { 'web':
mode => $mode,
bind => $binds,
options => [
{ 'option' => "${mode}log" },
if $https and $use_lets_encrypt and !$_use_native_acme {
{ 'acl is_letsencrypt' => 'path_beg /.well-known/acme-challenge/' }
},
$http_only_domains_options,
$frontend_headers,
if $ddos_protection {
$ddos_protection_options
},
if $https and $use_lets_encrypt and !$_use_native_acme {
[{ 'use_backend letsencrypt' => 'if is_letsencrypt' }]
},
$_native_acme_frontend_options,
[{ 'use_backend' => '%[req.hdr(host),lower,map(/etc/haproxy/domains-to-backends.map)]' }]
].delete_undef_values.flatten,
}
if $_use_native_acme {
haproxy::frontend { 'acme':
mode => 'http',
bind => functions::array_to_hash($listen_on.map |$listen| {
Hash(["${listen}:80", []])
}),
options => [
{ 'http-request' => 'return status 200 content-type text/plain lf-string "%[path,field(-1,/)].%[path,field(-1,/),map(virt@acme)]\n" if { path_beg \'/.well-known/acme-challenge/\' }' },
{ 'http-request redirect scheme https' => [
if $_allow_http_acl { '!allow_http' },
].delete_undef_values.join(' '),
},
$http_only_domains_options,
[{ 'use_backend' => '%[req.hdr(host),lower,map(/etc/haproxy/domains-to-backends.map)]' }]
].delete_undef_values.flatten,
}
}
haproxy::mapfile { 'domains-to-backends':
ensure => 'present',
mappings => $domains_with_backend,
}
$domains.each | $domain, $opts | {
$domain_backend = regsubst($domain, /\./, '_', 'G')
$extra_options = pick($opts['extra_opts'], 'check')
# Setup the Backend
haproxy::backend { $domain_backend:
mode => $mode,
options => $opts['servers'].map |$index, $endpoint| {
{ "server ${domain_backend}_${index}" => "${endpoint} ${extra_options}" }
},
}
}
}
}
# Haproxy Monitoring
contain eit_haproxy::monitoring
$listens.each |$key, $value| {
$_servers = $value['servers'].map |$server| {
"${key} ${server} check"
}
$_bind_options = $value['bind_options'].empty ? {
true => [],
false => $value['bind_options'],
}
$bind = $value['binds'].reduce({}) |$acc, $x| {
if $value['force_https'] {
$acc + {
$x => [],
} + {
"${x} ${value['bind_options'].join(' ')}" => [],
}
} else {
$acc + {
$x => $value['bind_options'],
}
}
}
haproxy::listen { $key:
mode => $mode,
bind => $bind,
options => {
'option' => 'tcplog',
'balance' => 'roundrobin',
'http-request redirect' => if $value['force_https'] { 'scheme https code 301 unless { ssl_fc }' },
'server' => $_servers,
'timeout' => 'server 10m',
},
}
}
}