Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9db9afae8d |
+31
-8
@@ -88,13 +88,36 @@
|
||||
|
||||
# SSH Hostkey Signing
|
||||
|
||||
The `sshca` ssh engine, its `signhost` role, the `sshca/sign/signhost` policy and
|
||||
the `sshsigner` approle are managed in terraform-vault:
|
||||
## create ssh engine, key, set ttl
|
||||
vault secrets enable -path=ssh-host-signer ssh
|
||||
vault write ssh-host-signer/config/ca generate_signing_key=true
|
||||
vault secrets tune -max-lease-ttl=87600h ssh-host-signer
|
||||
|
||||
- `config/ssh_secret_backend/sshca.yaml`
|
||||
- `config/ssh_secret_backend_role/sshca/signhost.yaml`
|
||||
- `config/auth_approle_role/approle/sshsigner.yaml`
|
||||
- `policies/sshca/sign/signhost.yaml`
|
||||
## create role
|
||||
vault write ssh-host-signer/roles/hostrole \
|
||||
key_type=ca \
|
||||
algorithm_signer=rsa-sha2-256 \
|
||||
ttl=87600h \
|
||||
allow_host_certificates=true \
|
||||
allowed_domains="unkin.net" \
|
||||
allow_subdomains=true \
|
||||
allow_baredomains=true
|
||||
|
||||
## get the sshsigner approle id
|
||||
vault read -field=role_id auth/approle/role/sshsigner/role-id
|
||||
## create policy to use hostrole
|
||||
cat <<EOF > sshsign-host.hcl
|
||||
path "ssh-host-signer/sign/hostrole" {
|
||||
capabilities = ["create", "update"]
|
||||
}
|
||||
EOF
|
||||
|
||||
vault policy write sshsign-host-policy sshsign-host.hcl
|
||||
|
||||
vault write auth/approle/role/sshsign-host-role \
|
||||
bind_secret_id=false \
|
||||
token_policies="sshsign-host-policy" \
|
||||
token_ttl=30s \
|
||||
token_max_ttl=30s \
|
||||
token_bound_cidrs="198.18.17.3/32,198.18.13.32/32,198.18.13.33/32,198.18.13.34/32"
|
||||
|
||||
## get the sshsign-host-role approle id
|
||||
vault read -field=role_id auth/approle/role/sshsign-host-role/role-id
|
||||
|
||||
@@ -178,8 +178,6 @@ lookup_options:
|
||||
convert_to: Sensitive
|
||||
stalwart::fallback_admin_password:
|
||||
convert_to: Sensitive
|
||||
wireguard::interfaces:
|
||||
convert_to: Sensitive
|
||||
|
||||
facts_path: '/opt/puppetlabs/facter/facts.d'
|
||||
|
||||
@@ -369,7 +367,6 @@ ssh::server::options:
|
||||
|
||||
profiles::ssh::knownhosts::lines:
|
||||
- '@cert-authority * ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC1HD97vYxLTniE4qNpGuftUlvmkEXIuX8+7nbENv/IzsGUghEDRtyThjQ7ojNKIsQ7f8wXr0gMcI+fAPfrbcOMHCAoYMomikwL0b3h95SZI40q3CyM+0DMnwiVVDX6C1QxkO2Rv9cszSkCa85NotJhXiUuTBI9BFcRPy+mAhbpAru+bfypYofI0wW97XNTl8Jgwmni5MgutBIQAokFIn5ux8iWxndCH3AqDtmkwC5DfQeQ+wZx7rkwqJEpJffQzrjb1gIM6P9hDCVBBVPh/3o80IJ69rFWrJAZUb+JpG4cXJH0NcSW+wqc3JCT/x3q8VlHwOTXSlNNKtOJCRx73mB8e1XTTy2a9FgpKDDg5XQXWHAViJDz1RTRL9gRefMylRgKz4bXoTuY9kJWM8hPTyUejtukbJThlBJc3OmDxBZBF7F0iqB11pHexok43OCEiANodVa36eWu9/5X032Vm48fZ1/akDPY/NSy3wAn7kwut+A0/JAHFHASrq+1mt9YurkJegI+YHXO6eEWpBIpmI7ORHJbGL4MhkHrxYzVamuP8CkU7tXzsv138+wpOcRHNp9yJY4PT40BZkRf/O3O+jt3pj9Dj8rvgywF2W6hFzywh3Y78upOprRkQlQtHfsI8EyrYI8/hUw2u3H+3yPXh3YjWfqvWVG1BRLRHBV7m90uaw=='
|
||||
- '@cert-authority * ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDi80G0GtKMdRj4azPwxbJW46NG0y8seSVSnvFm2Ka/nckdUb/3lRlQuPYf1tkbqqFlcXjDM8+u0tzM2kLsgfn0Dpujm1fuoA66yGGweBjtxLFErH5+6+/KND5I9w8LMY2AtVztnBk/CVx8RwgrooABDRZiBH7OOAOpJqqFI7LvEsv61zC0nAqbYJ8uxfx+r4lJ9dUhK1woitEqC4npSRUn5KJK+KAEd7AzcUkZb6TO9A3oRPz1nQ/qU+QNMmVUi+wRj9kJR18mxErugyLLTNcFDBqSdHNun3eUNBUmoS2cAa8ZVscOLzbUGejVK9UY06Q7wu+J34Fzl8CN5tBqRHGZ3ykqGZ6gG+O0Egr9Rm3Obgkujpio2uTh27LhBy72vjgJ8kTFmPlzANTJFpKlsy91usSFPh8WZeIo6VpPLqnC32rjfNkfqHyPAeJ3+rdOkUZoDjMoZc3wxxLZTgMU5ud1w4LxQNRkNiaT/tRRNQF8o+pygkS7xxKduBBMzYdRS1OifaS4gyvP82oOyquWywhyFNtG7ph8FQwc34puM7FyxfaJ0XL/+zAfPMhDoOojvofADJo73R+FgVyer+mCJw4KtWJ4JzZrNTYz1Xl8WlhF8RDzOYfSH46qzJFa9FcRqgP4LUkgzdvcQ+1hBFpvpHtw3vl3DiqtZDDbK9SyrEtdnw=='
|
||||
|
||||
profiles::base::groups::local:
|
||||
admins:
|
||||
@@ -403,7 +400,7 @@ networking::route_defaults:
|
||||
|
||||
# logging:
|
||||
victorialogs::client::journald::enable: true
|
||||
victorialogs::client::journald::inserturl: https://logs-ingest.k8s.syd1.au.unkin.net:443/insert/journald
|
||||
victorialogs::client::journald::inserturl: https://vlinsert.service.consul:9428/insert/journald
|
||||
|
||||
# FIXME these are for the proxmox ceph cluster
|
||||
profiles::ceph::client::fsid: 7f7f00cb-95de-498c-8dcc-14b54e4e9ca8
|
||||
|
||||
@@ -3,7 +3,23 @@ haproxy_server_k8s_syd1_traefik_internal: 'k8s-traefik-internal 198.18.200.4:443
|
||||
haproxy_server_k8s_syd1_traefik_external: 'k8s-traefik-external 198.18.199.0:443 ssl verify none check inter 2s rise 3 fall 2'
|
||||
|
||||
profiles::haproxy::dns::ipaddr: "%{hiera('anycast_ip')}"
|
||||
profiles::haproxy::dns::vrrp_cnames: []
|
||||
profiles::haproxy::dns::vrrp_cnames:
|
||||
- sonarr.main.unkin.net
|
||||
- radarr.main.unkin.net
|
||||
- lidarr.main.unkin.net
|
||||
- readarr.main.unkin.net
|
||||
- prowlarr.main.unkin.net
|
||||
- nzbget.main.unkin.net
|
||||
- git.unkin.net
|
||||
- fafflix.unkin.net
|
||||
- grafana.unkin.net
|
||||
- dashboard.ceph.unkin.net
|
||||
- mail-webadmin.main.unkin.net
|
||||
- mail-in.main.unkin.net
|
||||
- mail.main.unkin.net
|
||||
- autoconfig.main.unkin.net
|
||||
- autodiscover.main.unkin.net
|
||||
- auth.unkin.net
|
||||
|
||||
profiles::haproxy::mappings:
|
||||
fe_http:
|
||||
@@ -20,6 +36,7 @@ profiles::haproxy::mappings:
|
||||
- 'jellyfin.main.unkin.net be_jellyfin'
|
||||
- 'fafflix.unkin.net be_jellyfin'
|
||||
- 'git.unkin.net be_gitea'
|
||||
- 'grafana.unkin.net be_grafana'
|
||||
- 'dashboard.ceph.unkin.net be_ceph_dashboard'
|
||||
- 'mail-webadmin.main.unkin.net be_stalwart_webadmin'
|
||||
- 'autoconfig.main.unkin.net be_stalwart_webadmin'
|
||||
@@ -39,6 +56,7 @@ profiles::haproxy::mappings:
|
||||
- 'jellyfin.main.unkin.net be_jellyfin'
|
||||
- 'fafflix.unkin.net be_jellyfin'
|
||||
- 'git.unkin.net be_gitea'
|
||||
- 'grafana.unkin.net be_grafana'
|
||||
- 'dashboard.ceph.unkin.net be_ceph_dashboard'
|
||||
- 'mail-webadmin.main.unkin.net be_stalwart_webadmin'
|
||||
- 'autoconfig.main.unkin.net be_stalwart_webadmin'
|
||||
@@ -63,6 +81,7 @@ profiles::haproxy::frontends:
|
||||
- 'acl_jellyfin req.hdr(host) -i jellyfin.main.unkin.net'
|
||||
- 'acl_fafflix req.hdr(host) -i fafflix.unkin.net'
|
||||
- 'acl_gitea req.hdr(host) -i git.unkin.net'
|
||||
- 'acl_grafana req.hdr(host) -i grafana.unkin.net'
|
||||
- 'acl_ceph_dashboard req.hdr(host) -i dashboard.ceph.unkin.net'
|
||||
- 'acl_stalwart_webadmin req.hdr(host) -i mail-webadmin.main.unkin.net'
|
||||
- 'acl_stalwart_webadmin req.hdr(host) -i autoconfig.main.unkin.net'
|
||||
@@ -84,6 +103,7 @@ profiles::haproxy::frontends:
|
||||
- 'set-header X-Frame-Options DENY if acl_jellyfin'
|
||||
- 'set-header X-Frame-Options DENY if acl_fafflix'
|
||||
- 'set-header X-Frame-Options DENY if acl_gitea'
|
||||
- 'set-header X-Frame-Options DENY if acl_grafana'
|
||||
- 'set-header X-Frame-Options DENY if acl_ceph_dashboard'
|
||||
- 'set-header X-Frame-Options DENY if acl_stalwart_webadmin'
|
||||
- 'set-header X-Frame-Options DENY if acl_kanidm'
|
||||
@@ -399,6 +419,7 @@ profiles::haproxy::certlist::certificates:
|
||||
- /etc/pki/tls/letsencrypt/nzbget.main.unkin.net/fullchain_combined.pem
|
||||
- /etc/pki/tls/letsencrypt/fafflix.unkin.net/fullchain_combined.pem
|
||||
- /etc/pki/tls/letsencrypt/git.unkin.net/fullchain_combined.pem
|
||||
- /etc/pki/tls/letsencrypt/grafana.unkin.net/fullchain_combined.pem
|
||||
- /etc/pki/tls/letsencrypt/dashboard.ceph.unkin.net/fullchain_combined.pem
|
||||
- /etc/pki/tls/letsencrypt/auth.unkin.net/fullchain_combined.pem
|
||||
- /etc/pki/tls/vault/certificate.pem
|
||||
@@ -411,7 +432,9 @@ profiles::pki::vault::alt_names:
|
||||
- mail-webadmin.main.unkin.net
|
||||
|
||||
# additional cnames
|
||||
profiles::haproxy::dns::cnames: []
|
||||
profiles::haproxy::dns::cnames:
|
||||
- au-syd1-pve.main.unkin.net
|
||||
- au-syd1-pve-api.main.unkin.net
|
||||
|
||||
# letsencrypt certificates
|
||||
certbot::client::service: haproxy
|
||||
@@ -426,5 +449,6 @@ certbot::client::domains:
|
||||
- nzbget.main.unkin.net
|
||||
- fafflix.unkin.net
|
||||
- git.unkin.net
|
||||
- grafana.unkin.net
|
||||
- dashboard.ceph.unkin.net
|
||||
- auth.unkin.net
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
# primary interface is the WAN uplink; pin host identity to the dum0 loopback
|
||||
networking_loopback0_ip: 198.18.2.160
|
||||
networking_loopback1_ip: 198.18.21.160
|
||||
|
||||
# dns: keep the local dnsmasq resolver
|
||||
profiles::dns::base::nameservers:
|
||||
- 127.0.0.1
|
||||
profiles::dns::base::search:
|
||||
- main.unkin.net
|
||||
profiles::dns::base::primary_interface: dum0
|
||||
profiles::dns::updater::deny_ranges:
|
||||
- 198.18.199.0/24
|
||||
- 198.18.200.0/24
|
||||
- 10.42.0.0/16
|
||||
- 10.43.0.0/16
|
||||
- 10.10.12.0/24 # wg0
|
||||
- 103.216.190.0/23 # wan uplink
|
||||
profiles::consul::client::host_addr: "%{hiera('networking_loopback0_ip')}"
|
||||
|
||||
# ssh: listen on localhost and dum0 only; knock out the common wan primary ip
|
||||
lookup_options:
|
||||
ssh::server::options:
|
||||
merge:
|
||||
strategy: deep
|
||||
knockout_prefix: '--'
|
||||
ssh::server::options:
|
||||
ListenAddress:
|
||||
- "--%{facts.networking.ip}"
|
||||
- 127.0.0.1
|
||||
- "%{hiera('networking_loopback0_ip')}"
|
||||
profiles::ssh::sign::principals:
|
||||
- "%{hiera('networking_loopback0_ip')}"
|
||||
|
||||
# frrouting
|
||||
frrouting::ospfd_router_id: "%{hiera('networking_loopback0_ip')}"
|
||||
frrouting::ospfd_interfaces:
|
||||
dum0:
|
||||
area: 0.0.0.0
|
||||
dum1:
|
||||
area: 0.0.0.0
|
||||
bond0.201:
|
||||
area: 0.0.0.0
|
||||
frrouting::ospf_preferred_source_enable: true
|
||||
frrouting::ospf_preferred_source: "%{hiera('networking_loopback1_ip')}"
|
||||
@@ -77,7 +77,6 @@ profiles::yum::global::repos:
|
||||
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-internal/
|
||||
gpgcheck: false
|
||||
mirrorlist: absent
|
||||
metadata_expire: '60'
|
||||
rpm-vendor:
|
||||
name: rpm-vendor
|
||||
descr: rpm-vendor repository
|
||||
@@ -85,7 +84,6 @@ profiles::yum::global::repos:
|
||||
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-vendor/
|
||||
gpgcheck: false
|
||||
mirrorlist: absent
|
||||
metadata_expire: '60'
|
||||
# Per-release variants, resolved from the host's EL major version so el8
|
||||
# hosts pull rpm-internal-el8/rpm-vendor-el8, el9 hosts el9, etc.
|
||||
rpm-internal-release:
|
||||
@@ -95,7 +93,6 @@ profiles::yum::global::repos:
|
||||
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-internal-el%{facts.os.release.major}/
|
||||
gpgcheck: false
|
||||
mirrorlist: absent
|
||||
metadata_expire: '60'
|
||||
rpm-vendor-release:
|
||||
name: rpm-vendor-el%{facts.os.release.major}
|
||||
descr: rpm-vendor-el%{facts.os.release.major} repository
|
||||
@@ -103,7 +100,6 @@ profiles::yum::global::repos:
|
||||
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-vendor-el%{facts.os.release.major}/
|
||||
gpgcheck: false
|
||||
mirrorlist: absent
|
||||
metadata_expire: '60'
|
||||
|
||||
# Additional repositories - default to absent, roles can override with ensure: present
|
||||
# FRRouting repositories
|
||||
|
||||
@@ -60,7 +60,6 @@ profiles::yum::global::repos:
|
||||
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-internal-f%{facts.os.release.major}/
|
||||
gpgcheck: false
|
||||
mirrorlist: absent
|
||||
metadata_expire: '60'
|
||||
rpm-vendor:
|
||||
name: rpm-vendor-f%{facts.os.release.major}
|
||||
descr: rpm-vendor-f%{facts.os.release.major} repository
|
||||
@@ -68,4 +67,3 @@ profiles::yum::global::repos:
|
||||
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-vendor-f%{facts.os.release.major}/
|
||||
gpgcheck: false
|
||||
mirrorlist: absent
|
||||
metadata_expire: '60'
|
||||
|
||||
@@ -65,9 +65,6 @@ profiles::nginx::simpleproxy::locations:
|
||||
- 127.0.0.1
|
||||
- "%{facts.networking.ip}"
|
||||
- 198.18.24.0/24
|
||||
- 198.18.21.0/24
|
||||
- 198.18.15.0/24
|
||||
- 198.18.19.0/24
|
||||
location_deny:
|
||||
- all
|
||||
# authorised access from external
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
hiera_include:
|
||||
- frrouting
|
||||
- exporters::frr_exporter
|
||||
|
||||
# routing
|
||||
sysctl::base::values:
|
||||
net.ipv4.ip_forward:
|
||||
value: '1'
|
||||
net.ipv4.conf.all.rp_filter:
|
||||
value: '0'
|
||||
net.ipv4.conf.default.rp_filter:
|
||||
value: '0'
|
||||
# overrides 50-redhat.conf's per-interface rp_filter=1 (applied by udev on link add); sysctl -n can't glob, so no enforce
|
||||
net.ipv4.conf.*.rp_filter:
|
||||
value: '0'
|
||||
enforce: false
|
||||
|
||||
# frrouting
|
||||
exporters::frr_exporter::enable: true
|
||||
frrouting::ospfd_redistribute:
|
||||
- connected
|
||||
frrouting::daemons:
|
||||
ospfd: true
|
||||
|
||||
# consul
|
||||
profiles::consul::client::node_rules:
|
||||
- resource: service
|
||||
segment: frr_exporter
|
||||
disposition: write
|
||||
@@ -14,5 +14,6 @@ certbot::domains:
|
||||
- nzbget.main.unkin.net
|
||||
- fafflix.unkin.net
|
||||
- git.unkin.net
|
||||
- grafana.unkin.net
|
||||
- dashboard.ceph.unkin.net
|
||||
- auth.unkin.net
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
profiles::puppet::autosign::subnet_ranges:
|
||||
- '198.18.2.0/24'
|
||||
- '198.18.13.0/24'
|
||||
- '198.18.14.0/24'
|
||||
- '198.18.15.0/24'
|
||||
@@ -27,19 +26,6 @@ profiles::puppet::cobbler_enc::packages:
|
||||
- 'requests'
|
||||
- 'PyYAML'
|
||||
profiles::puppet::enc::repo: https://git.service.au-syd1.consul/unkinben/puppet-enc.git
|
||||
|
||||
# Deep-merged with the entries in roles/infra/puppet.yaml.
|
||||
profiles::packages::include:
|
||||
encapic:
|
||||
ensure: '0.2.0'
|
||||
certmanager:
|
||||
ensure: '0.2.0'
|
||||
sshsignhost:
|
||||
ensure: '0.1.0'
|
||||
|
||||
profiles::puppet::encapic::encapi_url: https://encapi.k8s.syd1.au.unkin.net
|
||||
profiles::puppet::server::external_nodes: '/usr/bin/encapic-enc'
|
||||
|
||||
profiles::puppet::r10k::r10k_repo: https://git.unkin.net/unkin/puppet-r10k.git
|
||||
profiles::puppet::g10k::bin_path: '/usr/bin/g10k'
|
||||
profiles::puppet::g10k::cfg_path: '/etc/puppetlabs/r10k/r10k.yaml'
|
||||
@@ -64,6 +50,7 @@ profiles::helpers::sshsignhost::vault_config:
|
||||
mount_point: 'sshca'
|
||||
approle_path: 'approle'
|
||||
role_name: 'signhost'
|
||||
output_path: '/tmp/sshsignhost'
|
||||
role_id: "%{lookup('sshsignhost::role_id')}"
|
||||
|
||||
profiles::puppet::server::agent_server: 'puppet.query.consul'
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
# manage dnsmasq as a dns forwarder and dhcp relay
|
||||
class dnsmasq (
|
||||
Boolean $manage_package = true,
|
||||
Boolean $manage_service = true,
|
||||
String $package_name = 'dnsmasq',
|
||||
String $service_name = 'dnsmasq',
|
||||
Stdlib::Absolutepath $config_file = '/etc/dnsmasq.conf',
|
||||
Stdlib::Absolutepath $config_dir = '/etc/dnsmasq.d',
|
||||
Boolean $purge_config_dir = false,
|
||||
Array[String] $interfaces = [],
|
||||
Array[Stdlib::IP::Address] $listen_addresses = ['127.0.0.1'],
|
||||
Enum['bind-interfaces', 'bind-dynamic', 'none'] $bind_mode = 'bind-interfaces',
|
||||
Boolean $no_resolv = false,
|
||||
Array[String] $servers = [],
|
||||
Hash[String, Array[String]] $forwards = {},
|
||||
Optional[Integer[0]] $cache_size = undef,
|
||||
Boolean $domain_needed = true,
|
||||
Boolean $bogus_priv = true,
|
||||
Array[String] $dhcp_relays = [],
|
||||
Array[String] $options = [],
|
||||
) {
|
||||
|
||||
if $manage_package {
|
||||
package { $package_name:
|
||||
ensure => installed,
|
||||
before => File[$config_file, $config_dir],
|
||||
}
|
||||
}
|
||||
|
||||
file { $config_dir:
|
||||
ensure => directory,
|
||||
recurse => $purge_config_dir,
|
||||
purge => $purge_config_dir,
|
||||
}
|
||||
|
||||
file { $config_file:
|
||||
ensure => file,
|
||||
owner => 'root',
|
||||
group => 'root',
|
||||
mode => '0644',
|
||||
content => template('dnsmasq/dnsmasq.conf.erb'),
|
||||
}
|
||||
|
||||
if $manage_service {
|
||||
service { $service_name:
|
||||
ensure => running,
|
||||
enable => true,
|
||||
subscribe => File[$config_file, $config_dir],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
# THIS FILE IS MANAGED BY PUPPET
|
||||
user=dnsmasq
|
||||
group=dnsmasq
|
||||
conf-dir=<%= @config_dir %>,.rpmnew,.rpmsave,.rpmorig
|
||||
|
||||
<% @interfaces.each do |iface| -%>
|
||||
interface=<%= iface %>
|
||||
<% end -%>
|
||||
<% unless @listen_addresses.empty? -%>
|
||||
listen-address=<%= @listen_addresses.join(',') %>
|
||||
<% end -%>
|
||||
<% unless @bind_mode == 'none' -%>
|
||||
<%= @bind_mode %>
|
||||
<% end -%>
|
||||
<% if @no_resolv -%>
|
||||
no-resolv
|
||||
<% end -%>
|
||||
<% if @domain_needed -%>
|
||||
domain-needed
|
||||
<% end -%>
|
||||
<% if @bogus_priv -%>
|
||||
bogus-priv
|
||||
<% end -%>
|
||||
<% if @cache_size -%>
|
||||
cache-size=<%= @cache_size %>
|
||||
<% end -%>
|
||||
<% @servers.each do |server| -%>
|
||||
server=<%= server %>
|
||||
<% end -%>
|
||||
<% @forwards.keys.sort.each do |domain| -%>
|
||||
<% @forwards[domain].each do |server| -%>
|
||||
server=/<%= domain %>/<%= server %>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
<% @dhcp_relays.each do |relay| -%>
|
||||
dhcp-relay=<%= relay %>
|
||||
<% end -%>
|
||||
<% @options.each do |line| -%>
|
||||
<%= line %>
|
||||
<% end -%>
|
||||
@@ -3,21 +3,13 @@
|
||||
require 'facter'
|
||||
require 'yaml'
|
||||
require 'net/http'
|
||||
require 'openssl'
|
||||
require 'uri'
|
||||
require 'fileutils'
|
||||
|
||||
# EncapiENC module: Fetches ENC data from encapi, caches it, and provides structured facts.
|
||||
module EncapiENC
|
||||
# CobblerENC module: Fetches ENC data from Cobbler, caches it, and provides structured facts.
|
||||
module CobblerENC
|
||||
CACHE_FILE = '/var/cache/puppet_enc.yaml'
|
||||
CACHE_TTL = 7 * 24 * 60 * 60 # 7 days in seconds
|
||||
# Facter runs under Puppet's vendored ruby, whose OpenSSL trusts only
|
||||
# /opt/puppetlabs/puppet/ssl/cert.pem and never the system trust store, so the
|
||||
# estate CA anchor profiles::pki::vaultca installs has to be named explicitly.
|
||||
CA_BUNDLE_PATHS = [
|
||||
'/etc/pki/ca-trust/source/anchors/vaultcaroot.pem',
|
||||
'/usr/local/share/ca-certificates/vaultcaroot.pem'
|
||||
].freeze
|
||||
@enc_data = nil # In-memory cache for the ENC response
|
||||
|
||||
def self.read_cache
|
||||
@@ -37,22 +29,9 @@ module EncapiENC
|
||||
File.write(CACHE_FILE, cache_data.to_yaml)
|
||||
end
|
||||
|
||||
def self.ca_bundle
|
||||
CA_BUNDLE_PATHS.find { |path| File.exist?(path) }
|
||||
end
|
||||
|
||||
def self.http_client(uri)
|
||||
client = Net::HTTP.new(uri.host, uri.port)
|
||||
client.use_ssl = true
|
||||
client.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
||||
bundle = ca_bundle
|
||||
client.ca_file = bundle if bundle
|
||||
client
|
||||
end
|
||||
|
||||
def self.fetch_from_encapi
|
||||
uri = URI("https://encapi.k8s.syd1.au.unkin.net/cblr/svc/op/puppet/hostname/#{Facter.value(:fqdn) || Facter.value(:hostname)}")
|
||||
response = http_client(uri).request(Net::HTTP::Get.new(uri))
|
||||
def self.fetch_from_cobbler
|
||||
uri = URI("http://cobbler.main.unkin.net/cblr/svc/op/puppet/hostname/#{Facter.value(:fqdn) || Facter.value(:hostname)}")
|
||||
response = Net::HTTP.get_response(uri)
|
||||
|
||||
raise "Failed to fetch ENC data. HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
||||
|
||||
@@ -62,7 +41,7 @@ module EncapiENC
|
||||
def self.retrieve_enc_data
|
||||
return @enc_data if @enc_data
|
||||
|
||||
@enc_data = fetch_from_encapi
|
||||
@enc_data = fetch_from_cobbler
|
||||
write_cache(@enc_data)
|
||||
@enc_data
|
||||
end
|
||||
@@ -70,26 +49,26 @@ module EncapiENC
|
||||
def self.fetch_enc_data
|
||||
retrieve_enc_data
|
||||
rescue StandardError => e
|
||||
Facter.warn("Error retrieving encapi ENC data: #{e.message}")
|
||||
Facter.warn("Error retrieving Cobbler ENC data: #{e.message}")
|
||||
@enc_data = read_cache
|
||||
return @enc_data unless @enc_data.empty?
|
||||
|
||||
raise 'No cached ENC data available and encapi is unreachable.'
|
||||
raise 'No cached ENC data available and Cobbler is down.'
|
||||
end
|
||||
|
||||
def self.enc_role
|
||||
fetch_enc_data.fetch('classes', {}).keys.first || raise('ENC Role not found in encapi ENC response')
|
||||
fetch_enc_data.fetch('classes', {}).keys.first || raise('ENC Role not found in Cobbler ENC response')
|
||||
end
|
||||
|
||||
def self.enc_env
|
||||
fetch_enc_data.fetch('environment', nil) || raise('ENC Environment not found in encapi ENC response')
|
||||
fetch_enc_data.fetch('environment', nil) || raise('ENC Environment not found in Cobbler ENC response')
|
||||
end
|
||||
end
|
||||
|
||||
Facter.add('enc_role') do
|
||||
setcode { EncapiENC.enc_role }
|
||||
setcode { CobblerENC.enc_role }
|
||||
end
|
||||
|
||||
Facter.add('enc_env') do
|
||||
setcode { EncapiENC.enc_env }
|
||||
setcode { CobblerENC.enc_env }
|
||||
end
|
||||
|
||||
@@ -5,7 +5,6 @@ require 'ipaddr'
|
||||
# a class that creates facts based on the subnet
|
||||
class SubnetAttributes
|
||||
SUBNET_TO_ATTRIBUTES = {
|
||||
'198.18.2.0/24' => { environment: 'prod', region: 'syd1', country: 'au', zone: 'common' }, # router loopbacks
|
||||
'198.18.13.0/24' => { environment: 'prod', region: 'syd1', country: 'au', zone: 'common' },
|
||||
'198.18.14.0/24' => { environment: 'prod', region: 'syd1', country: 'au', zone: 'common' },
|
||||
'198.18.15.0/24' => { environment: 'prod', region: 'syd1', country: 'au', zone: 'common' },
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# manage wireguard interfaces via wg-quick
|
||||
class wireguard (
|
||||
Boolean $manage_package = true,
|
||||
String $package_name = 'wireguard-tools',
|
||||
Variant[Hash, Sensitive[Hash]] $interfaces = {},
|
||||
) {
|
||||
|
||||
if $manage_package {
|
||||
package { $package_name:
|
||||
ensure => installed,
|
||||
before => File['/etc/wireguard'],
|
||||
}
|
||||
}
|
||||
|
||||
file { '/etc/wireguard':
|
||||
ensure => directory,
|
||||
owner => 'root',
|
||||
group => 'root',
|
||||
mode => '0700',
|
||||
}
|
||||
|
||||
# hiera hands eyaml secrets over as plain strings inside the (Sensitive) hash; re-wrap them per resource
|
||||
$raw = $interfaces ? {
|
||||
Sensitive => $interfaces.unwrap,
|
||||
default => $interfaces,
|
||||
}
|
||||
|
||||
$raw.each |String $iface, Hash $data| {
|
||||
$peers = $data.get('peers', []).map |Hash $peer| {
|
||||
$peer['preshared_key'] =~ String ? {
|
||||
true => $peer + { 'preshared_key' => Sensitive($peer['preshared_key']) },
|
||||
default => $peer,
|
||||
}
|
||||
}
|
||||
$private_key = $data['private_key'] =~ String ? {
|
||||
true => Sensitive($data['private_key']),
|
||||
default => $data['private_key'],
|
||||
}
|
||||
|
||||
wireguard::interface { $iface:
|
||||
* => $data + { 'peers' => $peers, 'private_key' => $private_key },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
# manage one wg-quick interface; without private_key, /etc/wireguard/<iface>.key is generated once and loaded via PostUp
|
||||
define wireguard::interface (
|
||||
Array[Stdlib::IP::Address] $addresses,
|
||||
Optional[Stdlib::Port] $listen_port = undef,
|
||||
Optional[Integer[1280, 9000]] $mtu = undef,
|
||||
Optional[Sensitive[String[1]]] $private_key = undef,
|
||||
Array[Struct[{
|
||||
public_key => String[1],
|
||||
allowed_ips => Variant[String[1], Array[String[1], 1]],
|
||||
preshared_key => Optional[Sensitive[String[1]]],
|
||||
endpoint => Optional[String[1]],
|
||||
persistent_keepalive => Optional[Integer[0, 65535]],
|
||||
}]] $peers = [],
|
||||
) {
|
||||
|
||||
$conf = "/etc/wireguard/${name}.conf"
|
||||
$key = $private_key.then |$k| { $k.unwrap }
|
||||
|
||||
if $private_key =~ Undef {
|
||||
$keyfile = "/etc/wireguard/${name}.key"
|
||||
|
||||
exec { "wireguard_genkey_${name}":
|
||||
command => "/bin/sh -c 'umask 077; wg genkey > ${keyfile}'",
|
||||
creates => $keyfile,
|
||||
path => ['/usr/bin', '/usr/sbin', '/bin', '/sbin'],
|
||||
require => File['/etc/wireguard'],
|
||||
}
|
||||
|
||||
file { $keyfile:
|
||||
ensure => file,
|
||||
owner => 'root',
|
||||
group => 'root',
|
||||
mode => '0600',
|
||||
require => Exec["wireguard_genkey_${name}"],
|
||||
before => [File[$conf], Service["wg-quick@${name}"]],
|
||||
}
|
||||
}
|
||||
|
||||
file { $conf:
|
||||
ensure => file,
|
||||
owner => 'root',
|
||||
group => 'root',
|
||||
mode => '0600',
|
||||
content => Sensitive(template('wireguard/wg.conf.erb')),
|
||||
show_diff => false,
|
||||
notify => Exec["wireguard_syncconf_${name}"],
|
||||
}
|
||||
|
||||
service { "wg-quick@${name}":
|
||||
ensure => running,
|
||||
enable => true,
|
||||
require => File[$conf],
|
||||
}
|
||||
|
||||
# syncconf applies peer/key changes without bouncing the tunnel; address/mtu changes need a manual restart
|
||||
exec { "wireguard_syncconf_${name}":
|
||||
command => "/bin/bash -c 'wg syncconf ${name} <(wg-quick strip ${name})'",
|
||||
onlyif => "/usr/sbin/ip link show ${name}",
|
||||
path => ['/usr/bin', '/usr/sbin', '/bin', '/sbin'],
|
||||
refreshonly => true,
|
||||
require => Service["wg-quick@${name}"],
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
# THIS FILE IS MANAGED BY PUPPET
|
||||
[Interface]
|
||||
<% @addresses.each do |addr| -%>
|
||||
Address = <%= addr %>
|
||||
<% end -%>
|
||||
<% if @listen_port -%>
|
||||
ListenPort = <%= @listen_port %>
|
||||
<% end -%>
|
||||
<% if @mtu -%>
|
||||
MTU = <%= @mtu %>
|
||||
<% end -%>
|
||||
<% if @key -%>
|
||||
PrivateKey = <%= @key %>
|
||||
<% else -%>
|
||||
PostUp = wg set %i private-key /etc/wireguard/%i.key
|
||||
<% end -%>
|
||||
<% @peers.each do |peer| -%>
|
||||
|
||||
[Peer]
|
||||
PublicKey = <%= peer['public_key'] %>
|
||||
<% if peer['preshared_key'] -%>
|
||||
PresharedKey = <%= peer['preshared_key'].unwrap %>
|
||||
<% end -%>
|
||||
AllowedIPs = <%= Array(peer['allowed_ips']).join(', ') %>
|
||||
<% if peer['endpoint'] -%>
|
||||
Endpoint = <%= peer['endpoint'] %>
|
||||
<% end -%>
|
||||
<% if peer['persistent_keepalive'] -%>
|
||||
PersistentKeepalive = <%= peer['persistent_keepalive'] %>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
@@ -24,7 +24,8 @@ class profiles::dns::updater (
|
||||
Stdlib::AbsolutePath $config_dir = '/etc/dns-updater',
|
||||
Stdlib::AbsolutePath $master_basedir = lookup('profiles::dns::master::basedir'),
|
||||
# dns-updater daemon (replaces the dns-update shell script). 'latest' so hosts
|
||||
# pick up new releases (e.g. the record filter).
|
||||
# pick up new releases (e.g. the record filter); rpm-internal metadata_expire
|
||||
# is 1h so this does not thrash.
|
||||
String $package_ensure = 'latest',
|
||||
Stdlib::AbsolutePath $api_socket = '/run/dns-updater/api.sock',
|
||||
String $resync = '10m',
|
||||
|
||||
@@ -1,28 +1,89 @@
|
||||
# profiles::helpers::certmanager
|
||||
#
|
||||
# renders the config.yaml read by the certmanager binary (RPM-installed)
|
||||
# wrapper class for python, pip and venv
|
||||
class profiles::helpers::certmanager (
|
||||
String $script_name = 'certmanager',
|
||||
Stdlib::AbsolutePath $base_path = "/opt/${script_name}",
|
||||
Stdlib::AbsolutePath $venv_path = "${base_path}/venv",
|
||||
Stdlib::AbsolutePath $config_path = "${base_path}/config.yaml",
|
||||
Hash $vault_config = {},
|
||||
Enum['approle','kubernetes'] $auth_method = 'approle',
|
||||
String[1] $k8s_mount = 'k8s/au/syd1',
|
||||
String[1] $k8s_role = 'puppet_certmanager',
|
||||
Stdlib::AbsolutePath $jwt_path = '/var/run/secrets/kubernetes.io/serviceaccount/token',
|
||||
String $owner = 'root',
|
||||
String $group = 'root',
|
||||
Boolean $systempkgs = false,
|
||||
String $version = 'system',
|
||||
Array[String[1]] $packages = ['requests', 'pyyaml'],
|
||||
){
|
||||
|
||||
file { $base_path:
|
||||
ensure => directory,
|
||||
mode => '0755',
|
||||
owner => $owner,
|
||||
group => $group,
|
||||
}
|
||||
if $::facts['python3_version'] {
|
||||
|
||||
file { $config_path:
|
||||
ensure => file,
|
||||
mode => '0660',
|
||||
owner => 'puppet',
|
||||
group => 'root',
|
||||
content => Sensitive(template("profiles/helpers/${script_name}_config.yaml.erb")),
|
||||
require => File[$base_path],
|
||||
# class parameters supply the auth defaults; $vault_config may override them
|
||||
$vault_settings = {
|
||||
'auth_method' => $auth_method,
|
||||
'k8s_mount' => $k8s_mount,
|
||||
'k8s_role' => $k8s_role,
|
||||
'jwt_path' => $jwt_path,
|
||||
} + $vault_config
|
||||
|
||||
$python_version = $version ? {
|
||||
'system' => $::facts['python3_version'],
|
||||
default => $version,
|
||||
}
|
||||
|
||||
# ensure the base_path exists
|
||||
file { $base_path:
|
||||
ensure => directory,
|
||||
mode => '0755',
|
||||
owner => $owner,
|
||||
group => $group,
|
||||
}
|
||||
|
||||
# create a venv
|
||||
python::pyvenv { $venv_path :
|
||||
ensure => present,
|
||||
version => $python_version,
|
||||
systempkgs => $systempkgs,
|
||||
venv_dir => $venv_path,
|
||||
owner => $owner,
|
||||
group => $group,
|
||||
require => File[$base_path],
|
||||
}
|
||||
|
||||
# install the required pip packages
|
||||
$packages.each |String $package| {
|
||||
python::pip { "${venv_path}_${package}":
|
||||
ensure => present,
|
||||
pkgname => $package,
|
||||
virtualenv => $venv_path,
|
||||
}
|
||||
}
|
||||
|
||||
# create the script from a template
|
||||
file { "${base_path}/${script_name}":
|
||||
ensure => file,
|
||||
mode => '0755',
|
||||
content => template("profiles/helpers/${script_name}.erb"),
|
||||
require => Python::Pyvenv[$venv_path],
|
||||
}
|
||||
|
||||
# create the config from a template
|
||||
file { $config_path:
|
||||
ensure => file,
|
||||
mode => '0660',
|
||||
owner => 'puppet',
|
||||
group => 'root',
|
||||
content => Sensitive(template("profiles/helpers/${script_name}_config.yaml.erb")),
|
||||
require => Python::Pyvenv[$venv_path],
|
||||
}
|
||||
|
||||
# create symbolic link in $PATH
|
||||
file { "/usr/local/bin/${script_name}":
|
||||
ensure => 'link',
|
||||
target => "${base_path}/${script_name}",
|
||||
require => File["${base_path}/${script_name}"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,89 @@
|
||||
# profiles::helpers::sshsignhost
|
||||
#
|
||||
# renders the config.yaml read by the sshsignhost binary (RPM-installed)
|
||||
# wrapper class for python, pip and venv
|
||||
class profiles::helpers::sshsignhost (
|
||||
String $script_name = 'sshsignhost',
|
||||
Stdlib::AbsolutePath $base_path = "/opt/${script_name}",
|
||||
Stdlib::AbsolutePath $venv_path = "${base_path}/venv",
|
||||
Stdlib::AbsolutePath $config_path = "${base_path}/config.yaml",
|
||||
Hash $vault_config = {},
|
||||
Enum['approle','kubernetes'] $auth_method = 'approle',
|
||||
String[1] $k8s_mount = 'k8s/au/syd1',
|
||||
String[1] $k8s_role = 'puppet_sshsigner',
|
||||
Stdlib::AbsolutePath $jwt_path = '/var/run/secrets/kubernetes.io/serviceaccount/token',
|
||||
String $owner = 'root',
|
||||
String $group = 'root',
|
||||
Boolean $systempkgs = false,
|
||||
String $version = 'system',
|
||||
Array[String[1]] $packages = ['requests', 'pyyaml'],
|
||||
){
|
||||
|
||||
file { $base_path:
|
||||
ensure => directory,
|
||||
mode => '0755',
|
||||
owner => $owner,
|
||||
group => $group,
|
||||
}
|
||||
if $::facts['python3_version'] {
|
||||
|
||||
file { $config_path:
|
||||
ensure => file,
|
||||
mode => '0660',
|
||||
owner => 'puppet',
|
||||
group => 'root',
|
||||
content => Sensitive(template("profiles/helpers/${script_name}_config.yaml.erb")),
|
||||
require => File[$base_path],
|
||||
# class parameters supply the auth defaults; $vault_config may override them
|
||||
$vault_settings = {
|
||||
'auth_method' => $auth_method,
|
||||
'k8s_mount' => $k8s_mount,
|
||||
'k8s_role' => $k8s_role,
|
||||
'jwt_path' => $jwt_path,
|
||||
} + $vault_config
|
||||
|
||||
$python_version = $version ? {
|
||||
'system' => $::facts['python3_version'],
|
||||
default => $version,
|
||||
}
|
||||
|
||||
# ensure the base_path exists
|
||||
file { $base_path:
|
||||
ensure => directory,
|
||||
mode => '0755',
|
||||
owner => $owner,
|
||||
group => $group,
|
||||
}
|
||||
|
||||
# create a venv
|
||||
python::pyvenv { $venv_path :
|
||||
ensure => present,
|
||||
version => $python_version,
|
||||
systempkgs => $systempkgs,
|
||||
venv_dir => $venv_path,
|
||||
owner => $owner,
|
||||
group => $group,
|
||||
require => File[$base_path],
|
||||
}
|
||||
|
||||
# install the required pip packages
|
||||
$packages.each |String $package| {
|
||||
python::pip { "${venv_path}_${package}":
|
||||
ensure => present,
|
||||
pkgname => $package,
|
||||
virtualenv => $venv_path,
|
||||
}
|
||||
}
|
||||
|
||||
# create the script from a template
|
||||
file { "${base_path}/${script_name}":
|
||||
ensure => file,
|
||||
mode => '0755',
|
||||
content => template("profiles/helpers/${script_name}.erb"),
|
||||
require => Python::Pyvenv[$venv_path],
|
||||
}
|
||||
|
||||
# create the config from a template
|
||||
file { $config_path:
|
||||
ensure => file,
|
||||
mode => '0660',
|
||||
owner => 'puppet',
|
||||
group => 'root',
|
||||
content => Sensitive(template("profiles/helpers/${script_name}_config.yaml.erb")),
|
||||
require => Python::Pyvenv[$venv_path],
|
||||
}
|
||||
|
||||
# create symbolic link in $PATH
|
||||
file { "/usr/local/bin/${script_name}":
|
||||
ensure => 'link',
|
||||
target => "${base_path}/${script_name}",
|
||||
require => File["${base_path}/${script_name}"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# Class: profiles::puppet::encapic
|
||||
#
|
||||
# Manages the configuration for the encapic ENC client. The package itself is
|
||||
# installed through profiles::packages (pinned in hiera); this class owns the
|
||||
# config so the encapi endpoint can change without repackaging.
|
||||
class profiles::puppet::encapic (
|
||||
Stdlib::HTTPUrl $encapi_url,
|
||||
Stdlib::AbsolutePath $config_dir = '/etc/encapic',
|
||||
String $config_name = 'encapic.conf',
|
||||
String $owner = 'root',
|
||||
String $group = 'root',
|
||||
) {
|
||||
|
||||
# The RPM ships this file as %config(noreplace), so puppet must write it only
|
||||
# once the package is present or the install overwrites it.
|
||||
file { $config_dir:
|
||||
ensure => directory,
|
||||
mode => '0755',
|
||||
owner => $owner,
|
||||
group => $group,
|
||||
require => Package['encapic'],
|
||||
}
|
||||
|
||||
file { "${config_dir}/${config_name}":
|
||||
ensure => file,
|
||||
mode => '0644',
|
||||
owner => $owner,
|
||||
group => $group,
|
||||
content => "ENCAPI_URL=${encapi_url}\n",
|
||||
require => File[$config_dir],
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ class profiles::puppet::puppetmaster (
|
||||
include profiles::puppet::g10k
|
||||
include profiles::puppet::enc
|
||||
include profiles::puppet::cobbler_enc
|
||||
include profiles::puppet::encapic
|
||||
include profiles::puppet::autosign
|
||||
include profiles::puppet::gems
|
||||
include profiles::helpers::certmanager
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
#!<%= @venv_path %>/bin/python
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
import yaml
|
||||
from zipfile import ZipFile
|
||||
|
||||
# remove this after certs are generated everywhere
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
def load_config(config_path):
|
||||
with open(config_path, 'r') as file:
|
||||
config = yaml.safe_load(file)
|
||||
return config['vault']
|
||||
|
||||
def authenticate_approle(vault_config):
|
||||
url = f"{vault_config['addr']}/v1/auth/{vault_config['approle_path']}/login"
|
||||
payload = {
|
||||
"role_id": vault_config['role_id'],
|
||||
}
|
||||
response = requests.post(url, json=payload, verify=False)
|
||||
if response.status_code == 200:
|
||||
auth_response = response.json()
|
||||
return auth_response['auth']['client_token']
|
||||
else:
|
||||
print(f"Error authenticating with AppRole: {response.text}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
class VaultAuthError(Exception):
|
||||
pass
|
||||
|
||||
def authenticate_kubernetes(vault_config):
|
||||
jwt_path = vault_config.get('jwt_path') or '/var/run/secrets/kubernetes.io/serviceaccount/token'
|
||||
try:
|
||||
with open(jwt_path, 'r') as file:
|
||||
jwt = file.read().strip()
|
||||
except OSError as error:
|
||||
raise VaultAuthError(f"cannot read service account token '{jwt_path}': {error}")
|
||||
if not jwt:
|
||||
raise VaultAuthError(f"service account token '{jwt_path}' is empty")
|
||||
url = f"{vault_config['addr']}/v1/auth/{vault_config['k8s_mount']}/login"
|
||||
payload = {
|
||||
"role": vault_config['k8s_role'],
|
||||
"jwt": jwt,
|
||||
}
|
||||
response = requests.post(url, json=payload, verify=False)
|
||||
if response.status_code != 200:
|
||||
raise VaultAuthError(
|
||||
f"kubernetes login as role '{vault_config['k8s_role']}' on mount "
|
||||
f"'{vault_config['k8s_mount']}' was rejected ({response.status_code}): {response.text}"
|
||||
)
|
||||
return response.json()['auth']['client_token']
|
||||
|
||||
def authenticate(vault_config):
|
||||
auth_method = vault_config.get('auth_method', 'approle')
|
||||
if auth_method == 'approle':
|
||||
client_token = authenticate_approle(vault_config)
|
||||
if not client_token:
|
||||
raise VaultAuthError("approle login was rejected")
|
||||
return client_token
|
||||
if auth_method == 'kubernetes':
|
||||
return authenticate_kubernetes(vault_config)
|
||||
raise VaultAuthError(f"unsupported auth_method '{auth_method}': expected 'approle' or 'kubernetes'")
|
||||
|
||||
def request_certificate(common_name, alt_names, ip_sans, expiry_days, vault_config):
|
||||
try:
|
||||
client_token = authenticate(vault_config)
|
||||
except VaultAuthError as error:
|
||||
print(f"Failed to authenticate with Vault: {error}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
url = f"{vault_config['addr']}/v1/{vault_config['mount_point']}/issue/{vault_config['role_name']}"
|
||||
headers = {'X-Vault-Token': client_token}
|
||||
payload = {
|
||||
"common_name": common_name,
|
||||
"alt_names": ",".join(alt_names),
|
||||
"ip_sans": ",".join(ip_sans),
|
||||
"ttl": f"{expiry_days}d"
|
||||
}
|
||||
response = requests.post(url, headers=headers, json=payload, verify=False)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
print(f"Error requesting certificate: {response.text}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
def save_cert_files(certificate_response, common_name, compress, config, json_output):
|
||||
base_path = config.get('output_path', '.')
|
||||
cert_dir = os.path.join(base_path, common_name)
|
||||
if json_output:
|
||||
import json
|
||||
output = {
|
||||
'certificate': certificate_response['data']['certificate'],
|
||||
'private_key': certificate_response['data']['private_key'],
|
||||
'full_chain': certificate_response['data']['issuing_ca'] + "\n" + certificate_response['data']['certificate'],
|
||||
}
|
||||
print(json.dumps(output))
|
||||
elif not compress:
|
||||
os.makedirs(cert_dir, exist_ok=True)
|
||||
with open(os.path.join(cert_dir, "certificate.crt"), "w") as cert_file:
|
||||
cert_file.write(certificate_response['data']['certificate'])
|
||||
with open(os.path.join(cert_dir, "private.key"), "w") as key_file:
|
||||
key_file.write(certificate_response['data']['private_key'])
|
||||
with open(os.path.join(cert_dir, "full_chain.crt"), "w") as full_chain_file:
|
||||
full_chain_file.write(certificate_response['data']['issuing_ca'] + "\n" + certificate_response['data']['certificate'])
|
||||
else:
|
||||
zip_name = f"{os.path.join(base_path, common_name)}.zip"
|
||||
with ZipFile(zip_name, 'w') as zipf:
|
||||
zipf.writestr("certificate.crt", certificate_response['data']['certificate'])
|
||||
zipf.writestr("private.key", certificate_response['data']['private_key'])
|
||||
zipf.writestr("full_chain.crt", certificate_response['data']['issuing_ca'] + "\n" + certificate_response['data']['certificate'])
|
||||
|
||||
def main(config_file):
|
||||
config = load_config(config_file)
|
||||
parser = argparse.ArgumentParser(description='Request and retrieve a certificate from Vault.')
|
||||
parser.add_argument('common_name', type=str, help='Common Name for the certificate')
|
||||
parser.add_argument('-a', '--alt-names', type=str, default='', help='Comma-separated alternative names for the certificate')
|
||||
parser.add_argument('-i', '--ip-sans', type=str, default='', help='Comma-separated IP Subject Alternative Names for the certificate')
|
||||
parser.add_argument('-e', '--expiry-days', type=int, default=365, help='Validity of the certificate in days (default: 365)')
|
||||
parser.add_argument('-c', '--compress', action='store_true', help='Compress the certificate, key, and full chain into a zip file')
|
||||
parser.add_argument('--json', action='store_true', help='Output results in JSON format')
|
||||
args = parser.parse_args()
|
||||
alt_names = [name.strip() for name in args.alt_names.split(',') if name]
|
||||
ip_sans = [ip.strip() for ip in args.ip_sans.split(',') if ip]
|
||||
certificate_response = request_certificate(args.common_name, alt_names, ip_sans, args.expiry_days, config)
|
||||
if certificate_response:
|
||||
if args.json:
|
||||
save_cert_files(certificate_response, args.common_name, args.compress, config, True)
|
||||
else:
|
||||
save_cert_files(certificate_response, args.common_name, args.compress, config, False)
|
||||
else:
|
||||
print("Failed to obtain certificate.", file=sys.stderr)
|
||||
exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
config_file = '<%= @config_path %>'
|
||||
main(config_file)
|
||||
@@ -1,7 +1,14 @@
|
||||
vault:
|
||||
addr: '<%= @vault_config['addr'] %>'
|
||||
role_id: '<%= @vault_config['role_id'] %>'
|
||||
approle_path: '<%= @vault_config['approle_path'] %>'
|
||||
mount_point: '<%= @vault_config['mount_point'] %>'
|
||||
role_name: '<%= @vault_config['role_name'] %>'
|
||||
output_path: '<%= @vault_config['output_path'] %>'
|
||||
addr: '<%= @vault_settings['addr'] %>'
|
||||
auth_method: '<%= @vault_settings['auth_method'] %>'
|
||||
<% if @vault_settings['auth_method'] == 'kubernetes' -%>
|
||||
k8s_mount: '<%= @vault_settings['k8s_mount'] %>'
|
||||
k8s_role: '<%= @vault_settings['k8s_role'] %>'
|
||||
jwt_path: '<%= @vault_settings['jwt_path'] %>'
|
||||
<% else -%>
|
||||
role_id: '<%= @vault_settings['role_id'] %>'
|
||||
approle_path: '<%= @vault_settings['approle_path'] %>'
|
||||
<% end -%>
|
||||
mount_point: '<%= @vault_settings['mount_point'] %>'
|
||||
role_name: '<%= @vault_settings['role_name'] %>'
|
||||
output_path: '<%= @vault_settings['output_path'] %>'
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
#!<%= @venv_path %>/bin/python
|
||||
import argparse
|
||||
import sys
|
||||
import requests
|
||||
import json
|
||||
import yaml
|
||||
|
||||
# remove this after certs are generated everywhere
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
def load_config(config_path):
|
||||
with open(config_path, 'r') as file:
|
||||
config = yaml.safe_load(file)
|
||||
return config['vault']
|
||||
|
||||
def authenticate_approle(vault_config):
|
||||
url = f"{vault_config['addr']}/v1/auth/{vault_config['approle_path']}/login"
|
||||
payload = {
|
||||
"role_id": vault_config['role_id'],
|
||||
}
|
||||
response = requests.post(url, json=payload, verify=False)
|
||||
if response.status_code == 200:
|
||||
auth_response = response.json()
|
||||
return auth_response['auth']['client_token']
|
||||
else:
|
||||
print(f"Error authenticating with AppRole: {response.text}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
class VaultAuthError(Exception):
|
||||
pass
|
||||
|
||||
def authenticate_kubernetes(vault_config):
|
||||
jwt_path = vault_config.get('jwt_path') or '/var/run/secrets/kubernetes.io/serviceaccount/token'
|
||||
try:
|
||||
with open(jwt_path, 'r') as file:
|
||||
jwt = file.read().strip()
|
||||
except OSError as error:
|
||||
raise VaultAuthError(f"cannot read service account token '{jwt_path}': {error}")
|
||||
if not jwt:
|
||||
raise VaultAuthError(f"service account token '{jwt_path}' is empty")
|
||||
url = f"{vault_config['addr']}/v1/auth/{vault_config['k8s_mount']}/login"
|
||||
payload = {
|
||||
"role": vault_config['k8s_role'],
|
||||
"jwt": jwt,
|
||||
}
|
||||
response = requests.post(url, json=payload, verify=False)
|
||||
if response.status_code != 200:
|
||||
raise VaultAuthError(
|
||||
f"kubernetes login as role '{vault_config['k8s_role']}' on mount "
|
||||
f"'{vault_config['k8s_mount']}' was rejected ({response.status_code}): {response.text}"
|
||||
)
|
||||
return response.json()['auth']['client_token']
|
||||
|
||||
def authenticate(vault_config):
|
||||
auth_method = vault_config.get('auth_method', 'approle')
|
||||
if auth_method == 'approle':
|
||||
client_token = authenticate_approle(vault_config)
|
||||
if not client_token:
|
||||
raise VaultAuthError("approle login was rejected")
|
||||
return client_token
|
||||
if auth_method == 'kubernetes':
|
||||
return authenticate_kubernetes(vault_config)
|
||||
raise VaultAuthError(f"unsupported auth_method '{auth_method}': expected 'approle' or 'kubernetes'")
|
||||
|
||||
def sign_ssh_certificate(vault_config, public_key, valid_principals, ttl):
|
||||
try:
|
||||
client_token = authenticate(vault_config)
|
||||
except VaultAuthError as error:
|
||||
print(f"Failed to authenticate with Vault: {error}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
# Prepare the SSH certificate signing request
|
||||
url = f"{vault_config['addr']}/v1/{vault_config['mount_point']}/sign/{vault_config['role_name']}"
|
||||
headers = {'X-Vault-Token': client_token}
|
||||
payload = {
|
||||
"cert_type": "host",
|
||||
"public_key": public_key,
|
||||
"valid_principals": valid_principals,
|
||||
"ttl": ttl
|
||||
}
|
||||
|
||||
# Request the SSH certificate signing
|
||||
response = requests.post(url, headers=headers, json=payload, verify=False)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
print(f"Error requesting certificate: {response.text}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
def main(config_file):
|
||||
config = load_config(config_file)
|
||||
parser = argparse.ArgumentParser(description='Sign SSH host certificate using Vault.')
|
||||
parser.add_argument('--public_key', required=True, help='SSH public key as a string')
|
||||
parser.add_argument('--valid_principals', required=True, help='Comma-separated list of valid principals')
|
||||
parser.add_argument('--ttl', default='87600h', help='Time-to-live for the certificate (default: 87600h)')
|
||||
parser.add_argument('--json', action='store_true', help='Output the resulting certificate as JSON')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load configuration
|
||||
config = load_config(config_file)
|
||||
|
||||
# Sign SSH certificate
|
||||
response = sign_ssh_certificate(config, args.public_key, args.valid_principals, args.ttl)
|
||||
|
||||
if response and 'data' in response and 'signed_key' in response['data']:
|
||||
if args.json:
|
||||
output = {
|
||||
'signed_key': response['data']['signed_key'],
|
||||
}
|
||||
print(json.dumps(output))
|
||||
else:
|
||||
print(response['data']['signed_key'])
|
||||
else:
|
||||
print("Error: The response does not contain the expected data.", file=sys.stderr)
|
||||
exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
config_file = '<%= @config_path %>'
|
||||
main(config_file)
|
||||
@@ -1,6 +1,14 @@
|
||||
vault:
|
||||
addr: '<%= @vault_config['addr'] %>'
|
||||
role_id: '<%= @vault_config['role_id'] %>'
|
||||
approle_path: '<%= @vault_config['approle_path'] %>'
|
||||
mount_point: '<%= @vault_config['mount_point'] %>'
|
||||
role_name: '<%= @vault_config['role_name'] %>'
|
||||
addr: '<%= @vault_settings['addr'] %>'
|
||||
auth_method: '<%= @vault_settings['auth_method'] %>'
|
||||
<% if @vault_settings['auth_method'] == 'kubernetes' -%>
|
||||
k8s_mount: '<%= @vault_settings['k8s_mount'] %>'
|
||||
k8s_role: '<%= @vault_settings['k8s_role'] %>'
|
||||
jwt_path: '<%= @vault_settings['jwt_path'] %>'
|
||||
<% else -%>
|
||||
role_id: '<%= @vault_settings['role_id'] %>'
|
||||
approle_path: '<%= @vault_settings['approle_path'] %>'
|
||||
<% end -%>
|
||||
mount_point: '<%= @vault_settings['mount_point'] %>'
|
||||
role_name: '<%= @vault_settings['role_name'] %>'
|
||||
output_path: '<%= @vault_settings['output_path'] %>'
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# roles::infra::network::router
|
||||
# an ospf router; frr only, interfaces and firewall are managed outside puppet
|
||||
#
|
||||
class roles::infra::network::router {
|
||||
if $facts['firstrun'] {
|
||||
include profiles::defaults
|
||||
include profiles::firstrun::init
|
||||
}else{
|
||||
include profiles::defaults
|
||||
include profiles::base
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user