diff --git a/Puppetfile b/Puppetfile index dd723f0..ab6ee0c 100644 --- a/Puppetfile +++ b/Puppetfile @@ -15,6 +15,7 @@ mod 'puppetlabs-firewall', '6.0.0' mod 'puppetlabs-accounts', '8.1.0' mod 'puppetlabs-mysql', '15.0.0' mod 'puppetlabs-xinetd', '3.4.1' +mod 'puppetlabs-haproxy', '8.0.0' # puppet mod 'puppet-python', '7.0.0' @@ -30,6 +31,7 @@ mod 'puppet-grafana', '13.1.0' mod 'puppet-consul', '8.0.0' mod 'puppet-vault', '4.1.0' mod 'puppet-dhcp', '6.1.0' +mod 'puppet-keepalived', '3.6.0' # other mod 'ghoneycutt-puppet', '3.3.0' diff --git a/hieradata/common.yaml b/hieradata/common.yaml index 5cb9769..606c093 100644 --- a/hieradata/common.yaml +++ b/hieradata/common.yaml @@ -21,6 +21,15 @@ lookup_options: profiles::yum::global::managed_repos: merge: strategy: deep + profiles::haproxy::server::defaults: + merge: + strategy: deep + profiles::haproxy::server::globals: + merge: + strategy: deep + haproxy::backend: + merge: + strategy: deep facts_path: '/opt/puppetlabs/facter/facts.d' diff --git a/hieradata/country/au/region/drw1/infra/halb/haproxy.yaml b/hieradata/country/au/region/drw1/infra/halb/haproxy.yaml new file mode 100644 index 0000000..e8ba37a --- /dev/null +++ b/hieradata/country/au/region/drw1/infra/halb/haproxy.yaml @@ -0,0 +1,49 @@ +--- +haproxy::backend: + be_letsencrypt: + description: Backend for LetsEncrypt Verifications + collect_exported: true + options: + balance: roundrobin + be_default: + description: Backend for unmatched HTTP traffic + collect_exported: true + options: + balance: roundrobin + option: + - httpchk + - forwardfor + http-check: send meth GET uri / + cookie: SRVNAME insert + http-request: + - set-header X-Forwarded-Port %[dst_port] + - add-header X-Forwarded-Proto https if { dst_port 443 } + +# fe_http +profiles::haproxy::fe_http::bind_addr: 0.0.0.0 +profiles::haproxy::fe_http::bind_port: 80 +profiles::haproxy::fe_http::bind_opts: + - transparent +profiles::haproxy::fe_http::acls: + - 'acl-letsencrypt path_beg /.well-known/acme-challenge/' +profiles::haproxy::fe_http::http_request: + - 'set-header X-Forwarded-Proto https' + - 'set-header X-Real-IP %[src]' + +# fe_https +profiles::haproxy::fe_https::bind_addr: 0.0.0.0 +profiles::haproxy::fe_https::bind_port: 443 +profiles::haproxy::fe_https::bind_opts: + - ssl + - crt-list /etc/haproxy/certificate.list + - ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH + - force-tlsv12 +profiles::haproxy::fe_https::acls: + - 'acl-letsencrypt path_beg /.well-known/acme-challenge/' +profiles::haproxy::fe_https::http_request: + - 'set-header X-Forwarded-Proto https' + - 'set-header X-Real-IP %[src]' + +profiles::haproxy::certlist::enabled: true +profiles::haproxy::certlist::certificates: + - /etc/pki/tls/vault/certificate.pem diff --git a/hieradata/roles/infra/halb/haproxy.yaml b/hieradata/roles/infra/halb/haproxy.yaml new file mode 100644 index 0000000..f6e352d --- /dev/null +++ b/hieradata/roles/infra/halb/haproxy.yaml @@ -0,0 +1,40 @@ +--- +profiles::haproxy::ls_stats::port: 9090 +profiles::haproxy::ls_stats::user: 'admin' +profiles::selinux::setenforce::mode: permissive + +profiles::haproxy::selinux::ports: + - 9090 +profiles::haproxy::selinux::sebooleans: + - haproxy_connect_any + +profiles::haproxy::server::globals: + stats: + - timeout 30s + - socket /var/lib/haproxy/stats + ca-base: /etc/ssl/certs + crt-base: /etc/ssl/private + ssl-default-bind-ciphers: EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH + ssl-default-bind-options: 'ssl-min-ver TLSv1.2 ssl-max-ver TLSv1.3' + ssl-default-server-ciphers: kEECDH+aRSA+AES:kRSA+AES:+AES256:RC4-SHA:!kEDH:!LOW:!EXP:!MD5:!aNULL:!eNULL + ssl-default-server-options: no-sslv3 + tune.ssl.default-dh-param: 2048 + +profiles::haproxy::server::defaults: + mode: http + option: + - httplog + - dontlognull + - http-server-close + - forwardfor except 127.0.0.0/8 + - redispatch + timeout: + - http-request 10s + - queue 1m + - connect 10s + - client 5m + - server 5m + - http-keep-alive 10s + - check 10s + retries: 3 + maxconn: 5000 diff --git a/site/profiles/manifests/haproxy/balancemember.pp b/site/profiles/manifests/haproxy/balancemember.pp new file mode 100644 index 0000000..6acbf84 --- /dev/null +++ b/site/profiles/manifests/haproxy/balancemember.pp @@ -0,0 +1,19 @@ +# profiles::haproxy::balancemember +define profiles::haproxy::balancemember ( + String $service, + Array[Stdlib::Port] $ports, + Array $options = ['check'], +) { + + $location_environment = "${facts['country']}-${facts['region']}-${facts['environment']}" + $balancemember_tag = "${service}_${location_environment}" + + @@haproxy::balancermember { $balancemember_tag: + listening_service => $service, + ports => $ports, + server_names => $facts['networking']['hostname'], + ipaddresses => $facts['networking']['ip'], + options => $options, + tag => $balancemember_tag, + } +} diff --git a/site/profiles/manifests/haproxy/certlist.pp b/site/profiles/manifests/haproxy/certlist.pp new file mode 100644 index 0000000..301bd8c --- /dev/null +++ b/site/profiles/manifests/haproxy/certlist.pp @@ -0,0 +1,18 @@ +# profiles::haproxy::certlist +class profiles::haproxy::certlist ( + Boolean $enabled = true, + Stdlib::Absolutepath $path = '/etc/haproxy/certificate.list', + Array[Stdlib::Absolutepath] $certificates = [] +) { + + if $enabled { + file { $path: + ensure => 'file', + owner => 'root', + group => 'root', + mode => '0600', + content => template('profiles/haproxy/certificate.list.erb') + } + } + +} diff --git a/site/profiles/manifests/haproxy/fe_http.pp b/site/profiles/manifests/haproxy/fe_http.pp new file mode 100644 index 0000000..19909c1 --- /dev/null +++ b/site/profiles/manifests/haproxy/fe_http.pp @@ -0,0 +1,21 @@ +# default http frontend +class profiles::haproxy::fe_http ( + Stdlib::IP::Address $bind_addr = $facts['networking']['ip'], + Stdlib::Port $bind_port = 80, + Array $bind_opts = ['transparent'], + Array $acls = [], + Array $http_request = [], +) { + haproxy::frontend { 'fe_http': + description => 'Default HTTP Frontend', + bind => { "${bind_addr}:${bind_port}" => $bind_opts }, + mode => 'http', + options => { + 'acl' => $acls, + 'http-request' => $http_request, + 'use_backend' => [ + '%[req.hdr(host),lower,map(/etc/haproxy/domains-to-backends.map,be_default)]', + ], + }, + } +} diff --git a/site/profiles/manifests/haproxy/fe_https.pp b/site/profiles/manifests/haproxy/fe_https.pp new file mode 100644 index 0000000..7e98328 --- /dev/null +++ b/site/profiles/manifests/haproxy/fe_https.pp @@ -0,0 +1,21 @@ +# default https frontend +class profiles::haproxy::fe_https ( + Stdlib::IP::Address $bind_addr = $facts['networking']['ip'], + Stdlib::Port $bind_port = 443, + Array $bind_opts = [], + Array $acls = [], + Array $http_request = [], +) { + haproxy::frontend { 'fe_https': + description => 'Default HTTPS Frontend', + bind => { "${bind_addr}:${bind_port}" => $bind_opts }, + mode => 'http', + options => { + 'acl' => $acls, + 'http-request' => $http_request, + 'use_backend' => [ + '%[req.hdr(host),lower,map(/etc/haproxy/domains-to-backends.map,be_default)]', + ], + }, + } +} diff --git a/site/profiles/manifests/haproxy/listener.pp b/site/profiles/manifests/haproxy/listener.pp new file mode 100644 index 0000000..3df3b35 --- /dev/null +++ b/site/profiles/manifests/haproxy/listener.pp @@ -0,0 +1,21 @@ +# profiles::haproxy::listener +define profiles::haproxy::listener ( + Boolean $bind = false, + Boolean $listen = false, + Enum['roundrobin', 'leastconn'] $balance = 'roundrobin', + Array $option = ['tcplog'], + Enum['tcp', 'http'] $mode = 'http', + Stdlib::Port $ports = 443, +) { + + haproxy::listen { 'puppet00': + ipaddress => $facts['networking']['ip'], + ports => $ports, + mode => $mode, + options => { + 'option' => $option, + 'balance' => $balance, + }, + } +} + diff --git a/site/profiles/manifests/haproxy/ls_stats.pp b/site/profiles/manifests/haproxy/ls_stats.pp new file mode 100644 index 0000000..7c8bc9d --- /dev/null +++ b/site/profiles/manifests/haproxy/ls_stats.pp @@ -0,0 +1,19 @@ +# the default status listener +class profiles::haproxy::ls_stats ( + Stdlib::IP::Address $bind_addr = $facts['networking']['ip'], + Stdlib::Port $bind_port = 9090, + Array $bind_opts = [], + String $user = 'admin', + String $pass = 'admin', +) { + haproxy::listen { 'stats': + bind => { "${bind_addr}:${bind_port}" => $bind_opts }, + options => { + 'mode' => 'http', + 'stats' => [ + 'uri /', + "auth ${user}:${pass}", + ], + }, + } +} diff --git a/site/profiles/manifests/haproxy/mappings.pp b/site/profiles/manifests/haproxy/mappings.pp new file mode 100644 index 0000000..ec8a1e4 --- /dev/null +++ b/site/profiles/manifests/haproxy/mappings.pp @@ -0,0 +1,9 @@ +# profiles::haproxy::mappings +class profiles::haproxy::mappings ( + Array $list = [] +) { + haproxy::mapfile { 'domains-to-backends': + ensure => 'present', + mappings => $list, + } +} diff --git a/site/profiles/manifests/haproxy/selinux.pp b/site/profiles/manifests/haproxy/selinux.pp new file mode 100644 index 0000000..a5b7271 --- /dev/null +++ b/site/profiles/manifests/haproxy/selinux.pp @@ -0,0 +1,32 @@ +# profiles::haproxy::selinux +class profiles::haproxy::selinux ( + Array[String] $sebooleans = [], + Array[Stdlib::Port] $ports = [], +) { + + # manage enforcing mode + include profiles::selinux::setenforce + + # manage selinux requirements for haproxy + if $::facts['os']['selinux']['config_mode'] == 'enforcing' { + + # set context for ports + $ports.each |$port| { + selinux::port { "haproxy_port_${port}": + ensure => 'present', + seltype => 'http_port_t', + protocol => 'tcp', + port => $port, + } + } + + # enable sebooleans + $sebooleans.each |$bool| { + selboolean { $bool: + value => on, + persistent => true, + } + } + } +} + diff --git a/site/profiles/manifests/haproxy/server.pp b/site/profiles/manifests/haproxy/server.pp new file mode 100644 index 0000000..63e4de7 --- /dev/null +++ b/site/profiles/manifests/haproxy/server.pp @@ -0,0 +1,60 @@ +# configure a haproxy server +class profiles::haproxy::server ( + Hash $globals = {}, + Hash $defaults = {}, +){ + + # default global/defaults arrays + $global_options = { + 'log' => "${facts['networking']['ip']} local0", + 'chroot' => '/var/lib/haproxy', + 'pidfile' => '/var/run/haproxy.pid', + 'maxconn' => '4000', + 'user' => 'haproxy', + 'group' => 'haproxy', + 'daemon' => '', + 'stats' => 'socket /var/lib/haproxy/stats', + } + $default_options = { + 'log' => 'global', + 'stats' => 'enable', + 'option' => ['redispatch'], + 'retries' => '3', + 'timeout' => [ + 'http-request 10s', + 'queue 1m', + 'connect 10s', + 'client 1m', + 'server 1m', + 'check 10s', + ], + 'maxconn' => '8000', + } + + # merge the default globals/defaults with those provided as params + $merged_global_options = merge($global_options, $globals) + $merged_default_options = merge($default_options, $defaults) + + # manage selinux + include profiles::haproxy::selinux + + # create the haproxy service/instance + class { 'haproxy': + global_options => $merged_global_options, + defaults_options => $merged_default_options, + require => Class['profiles::haproxy::selinux'] + } + + include profiles::haproxy::certlist # manage the certificate list file + include profiles::haproxy::mappings # manage the domain to backend mappings + include profiles::haproxy::ls_stats # default status listener + include profiles::haproxy::fe_http # default http frontend + include profiles::haproxy::fe_https # default https frontend + + $backends = lookup('haproxy::backend').keys + $backends.each |$backend| { + $location_environment = "${facts['country']}-${facts['region']}-${facts['environment']}" + $tag = "${location_environment}_${backend}" + Haproxy::Balancermember <<| tag == $tag |>> + } +} diff --git a/site/profiles/templates/haproxy/certificate.list.erb b/site/profiles/templates/haproxy/certificate.list.erb new file mode 100644 index 0000000..85c8efa --- /dev/null +++ b/site/profiles/templates/haproxy/certificate.list.erb @@ -0,0 +1,3 @@ +<% @certificates.each do |item| %> +<%= item %> +<% end %> diff --git a/site/roles/manifests/infra/halb/haproxy.pp b/site/roles/manifests/infra/halb/haproxy.pp index 81c7455..6b128b4 100644 --- a/site/roles/manifests/infra/halb/haproxy.pp +++ b/site/roles/manifests/infra/halb/haproxy.pp @@ -2,4 +2,5 @@ class roles::infra::halb::haproxy { include profiles::defaults include profiles::base + include profiles::haproxy::server }