Skip to content

Commit

Permalink
Merge pull request #141 from SUNET/jocar-geteduroam
Browse files Browse the repository at this point in the history
A mixed bag
  • Loading branch information
theseal authored Mar 15, 2024
2 parents 930ce9c + eb21dc4 commit a958a7c
Show file tree
Hide file tree
Showing 15 changed files with 2,132 additions and 3 deletions.
9 changes: 9 additions & 0 deletions files/geteduroam/certbot-renewal-hook
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/sh -eu
# vim: ai ts=4 sts=4 et sw=4

install -m 644 ${RENEWED_LINEAGE}/fullchain.pem \
/opt/geteduroam/cert/radius.pem
# group 101 is freerad inside the container
install -m 640 -g 101 \
${RENEWED_LINEAGE}/privkey.pem \
/opt/geteduroam/cert/radius.key
33 changes: 33 additions & 0 deletions files/geteduroam/md-signer2.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
-----BEGIN CERTIFICATE-----
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
-----END CERTIFICATE-----
29 changes: 29 additions & 0 deletions files/geteduroam/swamid-qa.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIE/jCCAuagAwIBAgIJAPccoL7mvtNVMA0GCSqGSIb3DQEBBQUAMC0xKzApBgNV
BAMTIk1ldGFkYXRhIFNpZ25lciAtIFNXQU1JRCBRQSAtIDIwMjMwHhcNMjMwNTE2
MDczNTI5WhcNMzMwNTE2MDczNTI5WjAtMSswKQYDVQQDEyJNZXRhZGF0YSBTaWdu
ZXIgLSBTV0FNSUQgUUEgLSAyMDIzMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
CgKCAgEAypbymzgez4Ihx2lZynlQtnOvYzUJ9lI4X5x8GYxt0Jza1OIg+vtu45xa
uNlPK3UszvlgdcMhhp6yYab6A215cozdOGwf5uJYPD5QQrTm7chIPOlUP1gSql7R
uJubUl5lG2AaXcruhNUzcBwMM2BYPoL8atYrnQkajVwplrfnjPbFXjep/hEEK/kd
w+PCVcOnDg6YUyukR1yezo0YWW7BRag5BPS4gNtP+Qd7cYLfKW5ZJPnlxX0NQkxV
nEEBgyD4CMi3sXByg0RSlCBagzq/s5/brf6+vCisbE+ArdTW9/eIql2gDQhgCbWs
eFSLP2R1xR3EetRS8stu2SsNSsDPQKICOqO0IxJkh9mdXFyK/56YJBrwcbVmSfOF
WW1z7/CPY+fBt3pbWyIx3UYUtu+4RxVxUPLSpD/d4sPBeA2GrB5dpy1ETLT4LzVh
ithgEZXcTrzUqzhXupgB72FvY4Bp/CbRd3piMwc3hFOIOKqIY20D2PcnclfkM2JW
dUWgYl8XdGoS+Mwp41wQO71Gw1kmrszWw70VcnPWevQABH7/vZz6/iQyItO5O6VY
selCZ9EOkBxbYcslFfQWlq+qHfPtQ0EHRWWCggIUzqs0515pKEOaJmbKVwj671eZ
0xxdK/gJEZYIun0R89Rm/3zLMs51MF2WoV+DUHVfW2yTyh3a+6MCAwEAAaMhMB8w
HQYDVR0OBBYEFP3n01J1wDRrsIRh0jihx54UechOMA0GCSqGSIb3DQEBBQUAA4IC
AQCRFuixQcj0PEP00JUyDuja7zsxlNQ0qPrEgR72yBRqWwfR8E9J4V4HlkyTCeMp
WgtxzDAxe7HdTyeNJW4dPKB8y01IkwxdfRDoZhXiJPPoCBgm2K8YxVyGJZljo4R9
wY6NlpzjaCdoJ6atKFuACGgkJBogBbM3upD5A2S09DDr/Glsx64LdkU6CEmMpdgD
xiolLpcKoTNY3QQbpEjnwYuIGB8MjP0WsSwBoLRd4bdoR9F0Xf28cmeJDtdKvM+S
JS0OJ2uCKYKXZIqrlVhBhcGQKBgteZLD2zuFLLq444Ikm9bwX79bpVPM4r3mqgzi
WA5Tpx45gdQtlKJAWpuU+ZwS2UXnVI75Jhe9SIZyp8bG/JaKqgpXazyRaFEiKJH4
sfogkV4i8rYfCvqzo0sC5+LosrlMXhPdoinJ9jT7166iB9oqm3RWk3BZmoEX4c/1
NS1iOygHCwoLWFKzp4nfSmPiotfS5HPRZZF4nlwKCpuWmeRT8D7U4cyoW++GTaow
IBS+Z8HHUpHAnNL7BNkNv360uVl8llN7SF9ZKt+dE8RmWtKUY5Sn6CQp7f+Fmlfi
vnYGsJz2Qf+/vv0B9mUDTqCRIwhDGiVH9RIEjvJP/L9qq1LOKsuJFpvWvJwvfDo+
0FtTA1bFiP3XzfQncys7eNi0AwRXd6x0IQ9VUtufpaix5g==
-----END CERTIFICATE-----
26 changes: 26 additions & 0 deletions manifests/certbot/acmed.pp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# acmed - automatated
class sunet::certbot::acmed(
String $server = 'https://acme-d.sunet.se',
){

include sunet::packages::certbot
include sunet::packages::python3_requests

file { '/etc/letsencrypt/acme-dns-auth.py':
ensure => file,
mode => '0700',
content => template('sunet/certbot/acme-dns-auth.py.erb'),
}

$acmed_clients = lookup('certbot_acmed_clients', undef, undef, {})
file { '/etc/letsencrypt/acmedns.json':
content => inline_template("<%= @acmed_clients.to_json %>\n"),
notify => Exec['certbot_issuing'],
}
$domain_arg = join($acmed_clients.keys, ' -d ')

exec {'certbot_issuing':
command => "certbot certonly --no-eff-email --agree-tos -m [email protected] --manual --manual-auth-hook /etc/letsencrypt/acme-dns-auth.py --preferred-challenges dns -d ${domain_arg}",
refreshonly => true,
}
}
109 changes: 109 additions & 0 deletions manifests/geteduroam.pp
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Get eduroam
class sunet::geteduroam(
String $domain,
String $realm,
Array $resolvers = [],
Boolean $mariadb = true,
String $mariadb_host = $facts['ipaddress_default'],
Boolean $app = true,
Boolean $radius = true,
String $app_tag = 'latest',
String $freeradius_tag = 'latest',
Array $app_admins = [],
Array $required_scoped_affiliation = [],
Boolean $qa_federation = false,
){

ensure_resource('sunet::misc::create_dir', '/opt/geteduroam/config', { owner => 'root', group => 'root', mode => '0750'})
ensure_resource('sunet::misc::create_dir', '/opt/geteduroam/cert', { owner => 'root', group => 'root', mode => '0755'})

if $mariadb {
sunet::mariadb { 'geteduroam_db':
}
}

if $radius {
sunet::nftables::allow { 'expose-allow-radius':
from => lookup('radius_servers', undef, undef,['192.36.171.226', '192.36.171.227', '2001:6b0:8:2::226', '2001:6b0:8:2::227'] ),
port => 1812,
proto => 'udp'
}

file { '/etc/letsencrypt/renewal-hooks/deploy/geteduroam':
ensure => file,
mode => '0700',
content => file('sunet/geteduroam/certbot-renewal-hook'),
}

$shared_secret = lookup('shared_secret', undef, undef, undef)
file { '/opt/geteduroam/config/clients.conf':
content => template('sunet/geteduroam/clients.conf.erb'),
mode => '0755',
}
}
if $app {
ensure_resource('sunet::misc::create_dir', '/opt/geteduroam/var', { owner => 'root', group => 'www-data', mode => '0770'})

if lookup('saml_metadata_key', undef, undef, undef) != undef {
sunet::snippets::secret_file { '/opt/geteduroam/cert/saml.key': hiera_key => 'saml_metadata_key' }
# assume cert is in cosmos repo
} else {
# make key pair
sunet::snippets::keygen {'saml_metadata_key':
key_file => '/opt/geteduroam/cert/saml.key',
cert_file => '/opt/geteduroam/cert/saml.pem',
}
}

file { '/opt/geteduroam/config/letswifi.conf.php':
content => template('sunet/geteduroam/letswifi.conf.simplesaml.php.erb'),
mode => '0755',
}
file { '/opt/geteduroam/config/config.php':
content => template('sunet/geteduroam/config.php.erb'),
mode => '0755',
}
file { '/opt/geteduroam/config/authsources.php':
content => template('sunet/geteduroam/authsources.php.erb'),
mode => '0755',
}

if $qa_federation {
file { '/opt/geteduroam/cert/swamid.crt':
content => file('sunet/geteduroam/swamid-qa.crt'),
mode => '0755',
}
} else {
file { '/opt/geteduroam/cert/swamid.crt':
content => file('sunet/geteduroam/md-signer2.crt'),
mode => '0755',
}
}
file { '/opt/geteduroam/cert/saml.key':
group => 'www-data',
mode => '0750',
}
sunet::nftables::allow { 'expose-allow-http':
from => 'any',
port => 80,
}
sunet::nftables::allow { 'expose-allow-https':
from => 'any',
port => 443,
}
class { 'sunet::dehydrated::client': domain => $domain, ssl_links => true }
}

file { '/opt/geteduroam/cert/radius.key': ensure => link, target => "/etc/letsencrypt/live/${realm}/privkey.pem" }
file { '/opt/geteduroam/cert/radius.pem': ensure => link, target => "/etc/letsencrypt/live/${realm}/fullchain.pem" }


sunet::docker_compose { 'geteduroam':
content => template('sunet/geteduroam/docker-compose.yml.erb'),
service_name => 'geteduroam',
compose_dir => '/opt/',
compose_filename => 'docker-compose.yml',
description => 'geteduroam',
}

}
2 changes: 1 addition & 1 deletion manifests/mariadb.pp
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
$mariadb_database = lookup('mariadb_database', undef, undef,undef)
$mariadb_backup_password = lookup('mariadb_root_password', undef, undef,'NOT_SET_IN_HIERA')
$clients = lookup('mariadb_clients', undef, undef,['127.0.0.1'])
$cluster_nodes = lookup('mariadb_cluster_nodes', undef, undef,['127.0.0.1'])
$cluster_nodes = lookup('mariadb_cluster_nodes', undef, undef,[])
$mariadb_dir = '/opt/mariadb'
$server_id = 1000 + Integer($facts['networking']['hostname'][-1])

Expand Down
3 changes: 3 additions & 0 deletions manifests/packages/certbot.pp
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class sunet::packages::certbot {
package { 'certbot': ensure => installed }
}
3 changes: 3 additions & 0 deletions manifests/packages/python3_requests.pp
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class sunet::packages::python3_requests {
package { 'python3-requests': ensure => installed }
}
7 changes: 5 additions & 2 deletions manifests/snippets/ssh_keygen.pp
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
# Ssh keygen
define sunet::snippets::ssh_keygen($key_file=undef) {
define sunet::snippets::ssh_keygen(
$key_file=undef,
$key_type='ed25519',
) {
$_key_file = $key_file ? {
undef => $name,
default => $key_file
}
exec { "${name}-ssh-key":
command => "ssh-keygen -N '' -f ${_key_file}",
command => "ssh-keygen -t ${key_type} -N '' -f ${_key_file}",
onlyif => "test ! -s ${_key_file}.pub -o ! -s ${_key_file}"
}
}
154 changes: 154 additions & 0 deletions templates/certbot/acme-dns-auth.py.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
import json
import os
import requests
import sys

### EDIT THESE: Configuration values ###

# URL to acme-dns instance
ACMEDNS_URL = "<%= @server %>"
# Path for acme-dns credential storage
STORAGE_PATH = "/etc/letsencrypt/acmedns.json"
# Whitelist for address ranges to allow the updates from
# Example: ALLOW_FROM = ["192.168.10.0/24", "::1/128"]
ALLOW_FROM = []
# Force re-registration. Overwrites the already existing acme-dns accounts.
FORCE_REGISTER = False

### DO NOT EDIT BELOW THIS POINT ###
### HERE BE DRAGONS ###

DOMAIN = os.environ["CERTBOT_DOMAIN"]
if DOMAIN.startswith("*."):
DOMAIN = DOMAIN[2:]
VALIDATION_DOMAIN = "_acme-challenge."+DOMAIN
VALIDATION_TOKEN = os.environ["CERTBOT_VALIDATION"]


class AcmeDnsClient(object):
"""
Handles the communication with ACME-DNS API
"""

def __init__(self, acmedns_url):
self.acmedns_url = acmedns_url

def register_account(self, allowfrom):
"""Registers a new ACME-DNS account"""

if allowfrom:
# Include whitelisted networks to the registration call
reg_data = {"allowfrom": allowfrom}
res = requests.post(self.acmedns_url+"/register",
data=json.dumps(reg_data))
else:
res = requests.post(self.acmedns_url+"/register")
if res.status_code == 201:
# The request was successful
return res.json()
else:
# Encountered an error
msg = ("Encountered an error while trying to register a new acme-dns "
"account. HTTP status {}, Response body: {}")
print(msg.format(res.status_code, res.text))
sys.exit(1)

def update_txt_record(self, account, txt):
"""Updates the TXT challenge record to ACME-DNS subdomain."""
update = {"subdomain": account['subdomain'], "txt": txt}
headers = {"X-Api-User": account['username'],
"X-Api-Key": account['password'],
"Content-Type": "application/json"}
res = requests.post(self.acmedns_url+"/update",
headers=headers,
data=json.dumps(update))
if res.status_code == 200:
# Successful update
return
else:
msg = ("Encountered an error while trying to update TXT record in "
"acme-dns. \n"
"------- Request headers:\n{}\n"
"------- Request body:\n{}\n"
"------- Response HTTP status: {}\n"
"------- Response body: {}")
s_headers = json.dumps(headers, indent=2, sort_keys=True)
s_update = json.dumps(update, indent=2, sort_keys=True)
s_body = json.dumps(res.json(), indent=2, sort_keys=True)
print(msg.format(s_headers, s_update, res.status_code, s_body))
sys.exit(1)

class Storage(object):
def __init__(self, storagepath):
self.storagepath = storagepath
self._data = self.load()

def load(self):
"""Reads the storage content from the disk to a dict structure"""
data = dict()
filedata = ""
try:
with open(self.storagepath, 'r') as fh:
filedata = fh.read()
except IOError as e:
if os.path.isfile(self.storagepath):
# Only error out if file exists, but cannot be read
print("ERROR: Storage file exists but cannot be read")
sys.exit(1)
try:
data = json.loads(filedata)
except ValueError:
if len(filedata) > 0:
# Storage file is corrupted
print("ERROR: Storage JSON is corrupted")
sys.exit(1)
return data

def save(self):
"""Saves the storage content to disk"""
serialized = json.dumps(self._data)
try:
with os.fdopen(os.open(self.storagepath,
os.O_WRONLY | os.O_CREAT, 0o600), 'w') as fh:
fh.truncate()
fh.write(serialized)
except IOError as e:
print("ERROR: Could not write storage file.")
sys.exit(1)

def put(self, key, value):
"""Puts the configuration value to storage and sanitize it"""
# If wildcard domain, remove the wildcard part as this will use the
# same validation record name as the base domain
if key.startswith("*."):
key = key[2:]
self._data[key] = value

def fetch(self, key):
"""Gets configuration value from storage"""
try:
return self._data[key]
except KeyError:
return None

if __name__ == "__main__":
# Init
client = AcmeDnsClient(ACMEDNS_URL)
storage = Storage(STORAGE_PATH)

# Check if an account already exists in storage
account = storage.fetch(DOMAIN)
if FORCE_REGISTER or not account:
# Create and save the new account
account = client.register_account(ALLOW_FROM)
storage.put(DOMAIN, account)
storage.save()

# Display the notification for the user to update the main zone
msg = "Please add the following CNAME record to your main DNS zone:\n{}"
cname = "{} CNAME {}.".format(VALIDATION_DOMAIN, account["fulldomain"])
print(msg.format(cname))

# Update the TXT record in acme-dns instance
client.update_txt_record(account, VALIDATION_TOKEN)
Loading

0 comments on commit a958a7c

Please sign in to comment.