This commit is contained in:
mwiegand 2021-06-13 15:40:33 +02:00
parent d4a68bc3cc
commit 2ef06345eb
17 changed files with 532 additions and 7 deletions

13
bundles/apt/items.py Normal file
View file

@ -0,0 +1,13 @@
actions = {
'apt_update': {
'command': 'apt-get update',
'needed_by': {
'pkg_apt:',
},
'triggered': True,
'cascade_skip': False,
},
}
for package, options in node.metadata.get('apt/packages', {}).items():
pkg_apt[package] = options

View file

@ -15,8 +15,8 @@ MEMBERS_PAGING_NUM = 100
PROTOCOL = http
SSH_DOMAIN = ${domain}
DOMAIN = ${domain}
HTTP_ADDR = 127.0.0.1
HTTP_PORT = 22000
HTTP_ADDR = 0.0.0.0
HTTP_PORT = 3500
ROOT_URL = https://${domain}/
DISABLE_SSH = false
SSH_PORT = 22

View file

@ -0,0 +1,5 @@
CONFIG_D=/etc/dehydrated/conf.d
BASEDIR=/var/lib/dehydrated
WELLKNOWN="${BASEDIR}/acme-challenges"
DOMAINS_TXT="/etc/dehydrated/domains.txt"
HOOK="/etc/dehydrated/hook.sh"

View file

@ -0,0 +1,3 @@
% for domain, aliases in sorted(node.metadata.get('letsencrypt/domains', {}).items()):
${domain} ${' '.join(sorted(aliases))}
% endfor

View file

@ -0,0 +1,37 @@
deploy_cert() {<%text>
local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}"</%text>
% for service, config in node.metadata.get('letsencrypt/concat_and_deploy', {}).items():
# concat_and_deploy ${service}
if [ "$DOMAIN" = "${config['match_domain']}" ]; then
cat $KEYFILE > ${config['target']}
cat $FULLCHAINFILE >> ${config['target']}
% if 'chown' in config:
chown ${config['chown']} ${config['target']}
% endif
% if 'chmod' in config:
chmod ${config['chmod']} ${config['target']}
% endif
% if 'commands' in config:
% for command in config['commands']:
${command}
% endfor
% endif
fi
% endfor
}
exit_hook() {<%text>
local ERROR="${1:-}"</%text>
% for service in sorted(node.metadata.get('letsencrypt/reload_after', set())):
systemctl reload-or-restart ${service}
% endfor
}
<%text>
HANDLER="$1"; shift
if [[ "${HANDLER}" =~ ^(deploy_cert|exit_hook)$ ]]; then
"$HANDLER" "$@"
fi</%text>

View file

@ -0,0 +1,31 @@
#!/bin/sh
domain=$1
just_check=$2
cert_path="/var/lib/dehydrated/certs/$domain"
already_exists=false
if [ -f "$cert_path/privkey.pem" -a -f "$cert_path/fullchain.pem" -a -f "$cert_path/chain.pem" ]
then
already_exists=true
fi
if [ "$just_check" = true ]
then
if [ "$already_exists" = true ]
then
exit 0
else
exit 1
fi
fi
if [ "$already_exists" != true ]
then
rm -r "$cert_path"
mkdir -p "$cert_path"
openssl req -x509 -newkey rsa:4096 -nodes -days 3650 -subj "/CN=$domain" -keyout "$cert_path/privkey.pem" -out "$cert_path/fullchain.pem"
chmod 0600 "$cert_path/privkey.pem"
cp "$cert_path/fullchain.pem" "$cert_path/chain.pem"
fi

View file

@ -0,0 +1,50 @@
assert node.has_bundle('nginx')
directories = {
'/etc/dehydrated/conf.d': {},
'/var/lib/dehydrated/acme-challenges': {},
}
files = {
'/etc/dehydrated/domains.txt': {
'content_type': 'mako',
'triggers': {
'action:letsencrypt_update_certificates',
},
},
'/etc/dehydrated/config': {
'triggers': {
'action:letsencrypt_update_certificates',
},
},
'/etc/dehydrated/hook.sh': {
'content_type': 'mako',
'mode': '0755',
},
'/etc/dehydrated/letsencrypt-ensure-some-certificate': {
'mode': '0755',
},
}
actions['letsencrypt_update_certificates'] = {
'command': 'dehydrated --cron --accept-terms --challenge http-01',
'triggered': True,
'needs': {
'svc_systemd:nginx',
},
}
for domain, _ in node.metadata.get('letsencrypt/domains').items():
actions['letsencrypt_ensure-some-certificate_{}'.format(domain)] = {
'command': '/etc/dehydrated/letsencrypt-ensure-some-certificate {}'.format(domain),
'unless': '/etc/dehydrated/letsencrypt-ensure-some-certificate {} true'.format(domain),
'needs': {
'file:/etc/dehydrated/letsencrypt-ensure-some-certificate',
},
'needed_by': {
'svc_systemd:nginx',
},
'triggers': {
'action:letsencrypt_update_certificates',
},
}

View file

@ -0,0 +1,16 @@
defaults = {
'apt': {
'packages': {
'dehydrated': {},
},
},
'cron': {
'letsencrypt_renew': '{} 4 * * * root /usr/bin/dehydrated --cron --accept-terms --challenge http-01 > /dev/null'.format((node.magic_number % 60)),
'letsencrypt_cleanup': '{} 4 * * 0 root /usr/bin/dehydrated --cleanup > /dev/null'.format((node.magic_number % 60)),
},
'pacman': {
'packages': {
'dehydrated': {},
},
},
}

View file

@ -0,0 +1,34 @@
user www-data;
worker_processes 10;
pid /var/run/nginx.pid;
events {
worker_connections 500;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
charset UTF-8;
override_charset on;
sendfile on;
#tcp_nopush on;
keepalive_timeout 15;
client_body_timeout 12;
client_header_timeout 12;
send_timeout 10;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
client_body_buffer_size 10K;
client_header_buffer_size 1k;
client_max_body_size 1M;
large_client_header_buffers 2 1k;
include /etc/nginx/sites/*;
}

View file

@ -0,0 +1,13 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
location /.well-known/acme-challenge/ {
alias /var/lib/dehydrated/acme-challenges/;
}
location / {
return 404;
}
}

View file

@ -0,0 +1,121 @@
server {
% if domain_aliases:
server_name ${domain} ${' '.join(sorted(domain_aliases))};
% else:
server_name ${domain};
% endif
root ${webroot if webroot else '/var/www/{}/'.format(vhost)};
index index.php index.html index.htm;
listen 80;
listen [::]:80;
% if ssl:
location / {
return 308 https://$host$request_uri;
}
% if ssl == 'letsencrypt':
location /.well-known/acme-challenge/ {
alias /var/lib/dehydrated/acme-challenges/;
}
% endif
}
server {
% if domain_aliases:
server_name ${domain} ${' '.join(sorted(domain_aliases))};
% else:
server_name ${domain};
% endif
root ${webroot if webroot else '/var/www/{}/'.format(vhost)};
index index.php index.html index.htm;
listen 443 ssl http2;
listen [::]:443 ssl http2;
% if ssl == 'letsencrypt':
ssl_certificate /var/lib/dehydrated/certs/${domain}/fullchain.pem;
ssl_certificate_key /var/lib/dehydrated/certs/${domain}/privkey.pem;
% else:
ssl_certificate /etc/nginx/ssl/${vhost}.crt;
ssl_certificate_key /etc/nginx/ssl/${vhost}.key;
% endif
ssl_dhparam /etc/ssl/certs/dhparam.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
% endif
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
access_log /var/log/nginx/access-${vhost}.log;
error_log /var/log/nginx/error-${vhost}.log;
% if max_body_size:
client_max_body_size ${max_body_size};
% elif proxy or php:
client_max_body_size 5M;
% endif
% if not do_not_set_content_security_headers:
add_header Referrer-Policy same-origin;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
% endif
add_header Permissions-Policy interest-cohort=();
location /.well-known/acme-challenge/ {
alias /var/lib/dehydrated/acme-challenges/;
}
% if security_txt:
location = /.well-known/security.txt {
alias /etc/nginx/security.txt.d/${vhost};
}
% endif
% if proxy:
% for location, options in proxy.items():
location ${location} {
proxy_pass ${options['target']};
proxy_http_version ${options.get('http_version', '1.1')};
proxy_set_header Host ${domain};
% if options.get('websockets', False):
proxy_set_header Connection "upgrade";
proxy_set_header Upgrade $http_upgrade;
% endif
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
% if ssl:
proxy_set_header X-Forwarded-Proto HTTPS;
% endif
proxy_set_header X-Forwarded-Host ${domain};
% for option, value in options.get('proxy_set_header', {}).items():
proxy_set_header ${option} ${value};
% endfor
% if location != '/':
proxy_set_header X-Script-Name ${location};
% endif
proxy_buffering off;
}
% endfor
% endif
% if php:
location ~ \.php$ {
include fastcgi.conf;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php/php${php_version}-fpm.sock;
}
% endif
% if extras:
<%include file="extras/${node.name}/${vhost}" />
% endif
}

View file

@ -0,0 +1,6 @@
server {
listen 127.0.0.1:22999 default_server;
server_name _;
stub_status;
}

121
bundles/nginx/items.py Normal file
View file

@ -0,0 +1,121 @@
from datetime import datetime, timedelta
directories = {
'/etc/nginx/sites': {
'purge': True,
'triggers': {
'svc_systemd:nginx:restart',
},
},
'/etc/nginx/ssl': {
'purge': True,
'triggers': {
'svc_systemd:nginx:restart',
},
},
'/var/www': {},
}
files = {
'/etc/nginx/nginx.conf': {
'content_type': 'mako',
'context': {
'username': 'www-data',
**node.metadata['nginx'],
},
'triggers': {
'svc_systemd:nginx:restart',
},
},
'/etc/nginx/sites/stub_status': {
'triggers': {
'svc_systemd:nginx:restart',
},
},
'/etc/nginx/sites/000-port80.conf': {
'source': 'port80.conf',
'triggers': {
'svc_systemd:nginx:restart',
},
},
}
actions = {
'nginx-generate-dhparam': {
'command': 'openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048',
'unless': 'test -f /etc/ssl/certs/dhparam.pem',
},
}
svc_systemd = {
'nginx': {
'needs': {
'action:nginx-generate-dhparam',
'pkg_apt:nginx',
},
},
}
for vhost, config in node.metadata.get('nginx/vhosts', {}).items():
files[f'/etc/nginx/sites/{vhost}'] = {
'source': 'site_template',
'content_type': 'mako',
'context': {
'create_access_log': config.get('access_log', node.metadata.get('nginx/access_log', False)),
'php_version': node.metadata.get('php/version', ''),
'vhost': vhost,
**config,
},
'needs': set(),
'needed_by': {
'svc_systemd:nginx',
'svc_systemd:nginx:restart',
},
'triggers': {
'svc_systemd:nginx:restart',
},
}
if not 'webroot' in config:
directories[f'/var/www/{vhost}'] = {}
if node.has_bundle('zfs'):
directories[f'/var/www/{vhost}']['needs'] = {
'bundle:zfs',
}
directories[f'/var/www/{vhost}'].update(config.get('webroot_config', {}))
if config.get('ssl', 'letsencrypt') == 'letsencrypt':
files[f'/etc/nginx/sites/{vhost}']['needs'].add('action:letsencrypt_ensure-some-certificate_{}'.format(config['domain']))
files[f'/etc/nginx/sites/{vhost}']['needed_by'].add('action:letsencrypt_update_certificates')
elif config.get('ssl', 'letsencrypt'):
files[f'/etc/nginx/ssl/{vhost}.crt'] = {
'content_type': 'mako',
'source': 'ssl_template',
'context': {
'domain': config['ssl'],
},
'needed_by': {
'svc_systemd:nginx',
'svc_systemd:nginx:restart',
},
'triggers': {
'svc_systemd:nginx:reload',
},
}
files[f'/etc/nginx/ssl/{vhost}.key'] = {
'content': repo.vault.decrypt_file('ssl/{}.key.pem.vault'.format(config['ssl'])),
'mode': '0600',
'needed_by': {
'svc_systemd:nginx',
'svc_systemd:nginx:restart',
},
'triggers': {
'svc_systemd:nginx:reload',
},
}
files[f'/etc/nginx/sites/{vhost}']['needs'].add(f'file:/etc/nginx/ssl/{vhost}.crt')
files[f'/etc/nginx/sites/{vhost}']['needs'].add(f'file:/etc/nginx/ssl/{vhost}.key')

45
bundles/nginx/metadata.py Normal file
View file

@ -0,0 +1,45 @@
from bundlewrap.metadata import atomic
defaults = {
'apt': {
'packages': {
'nginx': {},
},
},
'nginx': {
'worker_connections': 768,
},
}
@metadata_reactor.provides(
'letsencrypt/domains',
'letsencrypt/reload_after',
'nginx/vhosts',
)
def letsencrypt(metadata):
if not node.has_bundle('letsencrypt'):
raise DoNotRunAgain
domains = {}
vhosts = {}
for vhost, config in metadata.get('nginx/vhosts', {}).items():
if config.get('ssl', 'letsencrypt') == 'letsencrypt':
domain = config.get('domain', vhost)
domains[domain] = config.get('domain_aliases', set())
vhosts[vhost] = {
'ssl': 'letsencrypt',
}
return {
'letsencrypt': {
'domains': domains,
'reload_after': {
'nginx',
},
},
'nginx': {
'vhosts': vhosts,
},
}

View file

@ -1,7 +1,3 @@
pkg_apt = {
'postgresql': {},
}
if node.has_bundle('zfs'):
pkg_apt[postgresql]\
.setdefault('needs', [])\

View file

@ -11,6 +11,11 @@ defaults = {
},
'databases': {},
},
'apt': {
'packages': {
'postgresql': {},
},
},
}
if node.has_bundle('zfs'):

View file

@ -1 +1,30 @@
{}
{
'hostname': '162.55.188.157',
'groups': [
'debian-10',
],
'bundles': [
'nginx',
'letsencrypt',
],
'metadata': {
'nginx': {
'vhosts': {
'nextcloud': {
'domain': 'test.ckn.li',
'ssl': 'letsencrypt',
'letsencrypt': {
'active': True,
'force_ssl': False,
},
'proxy': {
'/': {
'target': 'https://mail.sublimity.de:443',
'websocket': True,
},
},
},
},
},
},
}