diff --git a/config/.env.sample b/config/.env.sample index af4b7c3..8b5f225 100644 --- a/config/.env.sample +++ b/config/.env.sample @@ -1,8 +1,11 @@ # Leave it empty if you use SSH keys SSH_PASSWD= -# Domain to server directly, without CloudFlare -DOMAIN_LE=direct.openfreemap.org +# Domain to server directly, with Let's Encrypt certificates +DOMAIN_LE=le.openfreemap.org + +# Let's Encrypt account email +LE_EMAIL=user@example.com # Domain via CloudFlare, using origin certificates # Please put cf.key and cf.cert files in config/certs diff --git a/init-server.py b/init-server.py index 78edc08..2abdec8 100755 --- a/init-server.py +++ b/init-server.py @@ -9,7 +9,7 @@ from fabric import Config, Connection from ssh_lib import CONFIG_DIR, HTTP_HOST_BIN, OFM_DIR, REMOTE_CONFIG, SCRIPTS_DIR, TILE_GEN_BIN from ssh_lib.benchmark import c1000k, wrk from ssh_lib.kernel import kernel_tweaks_ofm -from ssh_lib.nginx import lego, nginx +from ssh_lib.nginx import certbot, lego, nginx from ssh_lib.pkg_base import pkg_base, pkg_upgrade from ssh_lib.planetiler import planetiler from ssh_lib.rclone import rclone @@ -97,11 +97,12 @@ def prepare_tile_gen(c): def upload_http_host_config(c): - domain_le = dotenv_values(f'{CONFIG_DIR}/.env').get('DOMAIN_LE', '').strip() - domain_cf = dotenv_values(f'{CONFIG_DIR}/.env').get('DOMAIN_CF', '').strip() - skip_planet = ( - dotenv_values(f'{CONFIG_DIR}/.env').get('SKIP_PLANET', '').lower().strip() == 'true' - ) + env_values = dotenv_values(f'{CONFIG_DIR}/.env') + + domain_le = env_values.get('DOMAIN_LE', '').strip() + domain_cf = env_values.get('DOMAIN_CF', '').strip() + skip_planet = env_values.get('SKIP_PLANET', '').lower().strip() == 'true' + le_email = env_values.get('LE_EMAIL', '').strip() if not (domain_le or domain_cf): sys.exit('Please specify DOMAIN_LE or DOMAIN_CF in config/.env') @@ -117,6 +118,7 @@ def upload_http_host_config(c): 'domain_le': domain_le, 'domain_cf': domain_cf, 'skip_planet': skip_planet, + 'le_email': le_email, } host_config_str = json.dumps(host_config, indent=2, ensure_ascii=False) @@ -126,7 +128,7 @@ def upload_http_host_config(c): def prepare_http_host(c): nginx(c) - lego(c) + certbot(c) c.sudo('rm -rf /data/ofm/http_host/logs') c.sudo('mkdir -p /data/ofm/http_host/logs') @@ -260,14 +262,12 @@ def tile_gen(hostname, user, port): def debug(hostname, user, port): c = get_connection(hostname, user, port) - lego(c) + upload_http_host_config(c) - # upload_http_host_config(c) + upload_https_host_files(c) - # upload_https_host_files(c) # run_http_host_sync(c) - - # sudo_cmd(c, '/data/ofm/venv/bin/python -u /data/ofm/http_host/bin/host_manager.py nginx-sync') + sudo_cmd(c, '/data/ofm/venv/bin/python -u /data/ofm/http_host/bin/host_manager.py nginx-sync') if __name__ == '__main__': diff --git a/scripts/http_host/http_host_lib/__init__.py b/scripts/http_host/http_host_lib/__init__.py index 7d41ac3..7aebcc1 100644 --- a/scripts/http_host/http_host_lib/__init__.py +++ b/scripts/http_host/http_host_lib/__init__.py @@ -10,6 +10,8 @@ DEFAULT_ASSETS_DIR = Path('/data/ofm/http_host/assets') MNT_DIR = Path('/mnt/ofm') OFM_CONFIG_DIR = Path('/data/ofm/config') +CERTS_DIR = Path('/data/nginx/certs') + try: with open('/data/ofm/config/http_host.json') as fp: HOST_CONFIG = json.load(fp) diff --git a/scripts/http_host/http_host_lib/nginx.py b/scripts/http_host/http_host_lib/nginx.py index a4323cc..69d55bc 100644 --- a/scripts/http_host/http_host_lib/nginx.py +++ b/scripts/http_host/http_host_lib/nginx.py @@ -1,20 +1,76 @@ +import shutil import subprocess import sys from pathlib import Path -from http_host_lib import DEFAULT_RUNS_DIR, HOST_CONFIG, MNT_DIR, NGINX_DIR, OFM_CONFIG_DIR +from http_host_lib import ( + CERTS_DIR, + DEFAULT_RUNS_DIR, + HOST_CONFIG, + MNT_DIR, + NGINX_DIR, + OFM_CONFIG_DIR, +) def write_nginx_config(): curl_text_mix = '' - if HOST_CONFIG['domain_cf']: + domain_cf = HOST_CONFIG['domain_cf'] + domain_le = HOST_CONFIG['domain_le'] + + # processing Cloudflare config + if domain_cf: + if not (CERTS_DIR / 'cf.cert').exists() or not (CERTS_DIR / 'cf.key').exists(): + sys.exit('cf.cert or cf.key missing') + curl_text_mix += create_nginx_conf( template_path=NGINX_DIR / 'cf.conf', local='ofm_cf', - domain=HOST_CONFIG['domain_cf'], + domain=domain_cf, ) + # processing Let's Encrypt config + if domain_le: + le_cert = CERTS_DIR / 'le.cert' + le_key = CERTS_DIR / 'le.key' + + if not (CERTS_DIR / 'le.cert').exists() or not (CERTS_DIR / 'le.key').exists(): + shutil.copyfile(Path('/etc/nginx/ssl/dummy.crt'), le_cert) + shutil.copyfile(Path('/etc/nginx/ssl/dummy.key'), le_key) + + curl_text_mix += create_nginx_conf( + template_path=NGINX_DIR / 'le.conf', + local='ofm_le', + domain=domain_le, + ) + + subprocess.run(['nginx', '-t'], check=True) + subprocess.run(['systemctl', 'reload', 'nginx'], check=True) + + subprocess.run( + [ + 'lego', + '--accept-tos', + '--email', + HOST_CONFIG['le_email'], + '--http', + '--http.webroot=/data/nginx/acme-challenges/', + '--domains', + domain_le, + '--http-timeout=30', + '--path=/data/nginx/lego/', + 'run', + ], + check=True, + ) + + # link lego certs to nginx dir + le_cert.unlink() + le_key.unlink() + le_cert.symlink_to(Path(f'/data/nginx/lego/certificates/{domain_le}.crt')) + le_key.symlink_to(Path(f'/data/nginx/lego/certificates/{domain_le}.key')) + subprocess.run(['nginx', '-t'], check=True) subprocess.run(['systemctl', 'reload', 'nginx'], check=True) diff --git a/scripts/http_host/http_host_lib/nginx/cf.conf b/scripts/http_host/http_host_lib/nginx/cf.conf index eaa8a89..73179ae 100644 --- a/scripts/http_host/http_host_lib/nginx/cf.conf +++ b/scripts/http_host/http_host_lib/nginx/cf.conf @@ -13,7 +13,7 @@ server { ssl_certificate_key /data/nginx/certs/cf.key; ssl_session_timeout 1d; - ssl_session_cache shared:MozSSL:10m; # about 40000 sessions + ssl_session_cache shared:MozSSL:10m; # about 40000 sessions ssl_session_tickets off; # modern configuration diff --git a/scripts/http_host/http_host_lib/nginx/le.conf b/scripts/http_host/http_host_lib/nginx/le.conf index 1a2cd40..9203663 100644 --- a/scripts/http_host/http_host_lib/nginx/le.conf +++ b/scripts/http_host/http_host_lib/nginx/le.conf @@ -8,11 +8,11 @@ server { listen [::]:443 ssl; http2 on; - ssl_certificate /data/nginx/certs/cf.cert; - ssl_certificate_key /data/nginx/certs/cf.key; + ssl_certificate /data/nginx/certs/le.cert; + ssl_certificate_key /data/nginx/certs/le.key; ssl_session_timeout 1d; - ssl_session_cache shared:MozSSL:10m; # about 40000 sessions + ssl_session_cache shared:MozSSL:10m; # about 40000 sessions ssl_session_tickets off; ssl_dhparam /etc/nginx/ffdhe2048.txt; @@ -23,11 +23,16 @@ server { ssl_prefer_server_ciphers off; # access log disabled by default - #access_log /data/ofm/http_host/logs_nginx/cf-access.log access_json buffer=32k; + #access_log /data/ofm/http_host/logs_nginx/le-access.log access_json buffer=32k; access_log off; - error_log /data/ofm/http_host/logs_nginx/cf-error.log; + error_log /data/ofm/http_host/logs_nginx/le-error.log; + + location ^~ /.well-known/acme-challenge/ { + # trailing slash + root /data/nginx/acme-challenges; + try_files $uri =404; + } __LOCATION_BLOCKS__ } - diff --git a/ssh_lib/nginx.py b/ssh_lib/nginx.py index 4568a57..cd9e943 100644 --- a/ssh_lib/nginx.py +++ b/ssh_lib/nginx.py @@ -81,6 +81,8 @@ def lego(c): f'wget -q "{url}" -O /tmp/lego/out.tar.gz', ) c.run('tar xzvf /tmp/lego/out.tar.gz -C /tmp/lego') - c.run('mv /tmp/lego/lego /usr/bin') - c.run('chmod +x /usr/bin/lego') + c.run('chmod +x /tmp/lego/lego') + c.run('mv /tmp/lego/lego /usr/local/bin') c.run('rm -rf /tmp/lego*') + + c.run('mkdir -p /data/nginx/acme-challenges/') diff --git a/ssh_lib/utils.py b/ssh_lib/utils.py index 62f53f9..3d2b9d4 100644 --- a/ssh_lib/utils.py +++ b/ssh_lib/utils.py @@ -1,9 +1,11 @@ import os import secrets import string +import sys from pathlib import Path import requests +from invoke import UnexpectedExit def put( @@ -76,7 +78,22 @@ def append_str(c, remote_path, str_): def sudo_cmd(c, cmd, *, user=None): cmd = cmd.replace('"', '\\"') - c.sudo(f'bash -c "{cmd}"', user=user) + + try: + c.sudo(f'bash -c "{cmd}"', user=user) + except UnexpectedExit as e: + print(f'Command failed: {e.result.command}') + print(f'Error: {e.result.stderr}') + sys.exit(1) + + +def run_nice(c, cmd): + try: + c.run(cmd) + except UnexpectedExit as e: + print(f'Command failed: {e.result.command}') + print(f'Error: {e.result.stderr}') + sys.exit(1) def set_permission(c, path, *, permissions=None, user=None, group=None):