ops: add isolated HN subscription gateway deployment
This commit is contained in:
parent
6e1a2ed1fe
commit
d8a509114a
15
scripts/hn/gateway/README.md
Normal file
15
scripts/hn/gateway/README.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# HN independent subscription gateway
|
||||||
|
|
||||||
|
This deployment reuses the fixed, already-running DPL SubSieve images while keeping HN configuration, credentials, volumes, network, ports, certificates and upstream independent. It does not update or restart DPL/FUC containers.
|
||||||
|
|
||||||
|
Target: registered gateway-01 via the white SSH jump host. Public subscription host: c2.hardnerd-nodes.com. Upstream: https://hardnerd.net. Local management: https://127.0.0.1:26444 only; no public management virtual host is installed.
|
||||||
|
|
||||||
|
Publish the repository through Git before running the exact checked-out release. Run install.py prepare, then install.py certificate after public DNS/ACME readiness, then install.py start with an authorized administrator JSON object on stdin. Never place credentials in arguments or this repository. Each phase has state guards; inspect partial state before resuming rather than rerunning initialization.
|
||||||
|
|
||||||
|
Preflight checks free RAM/disk, fixed image IDs, all three loopback ports and the bridge subnet. Configuration uses ports 28080/28443/26444, hn_sgw_config / hn_sgw_logs, hn_sgw_net (172.28.32.0/24), and /var/lib/hn-sgw. Gateway/admin limits are 160MB/128MB and 0.5 CPU each. The state backup is /root/codex-backups/hn-gateway/20261027-first-install. Certificate files and registered account copy are isolated in /etc/hn-gateway-letsencrypt; no global renewal hooks run. Renewal reloads only the two HN containers and gracefully reloads host Nginx.
|
||||||
|
|
||||||
|
Public paths are limited to /s/{token} and /api/v1/client/subscribe. Host access logs are disabled; short paths become canonical API paths before entering the container, whose access log excludes query strings. TLS verification stays enabled both from host to container and from container to HN. Only Cloudflare ranges are trusted for CF-Connecting-IP; only the bridge gateway is trusted inside the container. The documented white host is allowlisted for server-side acceptance. Root and www hosts redirect to the HN main website.
|
||||||
|
|
||||||
|
Before changing HN's application subscription URL, verify a valid restricted QA token in supported client formats plus invalid/disabled tokens, no-store response headers, and the original-instance baseline. Empty HN node lists must be reported as empty; they do not constitute node connectivity acceptance. Cloudflare cache bypass is required for this dedicated subscription zone; preserve platform protections unless a specific compatibility change is authorized.
|
||||||
|
|
||||||
|
Rollback: restore the backed-up HN HTTP-only host file, nginx -t then graceful reload; stop only HN Compose services and HN timers, preserving HN volumes, credentials and backups for investigation. Do not run compose down on DPL/FUC or restart shared services. Current production evidence is maintained in docs/maintenance/HN_PRODUCTION.md and STATUS.md.
|
||||||
29
scripts/hn/gateway/gateway-admin.conf
Normal file
29
scripts/hn/gateway/gateway-admin.conf
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes 1;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
events { worker_connections 256; }
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
server_tokens off;
|
||||||
|
server {
|
||||||
|
listen 64444 ssl;
|
||||||
|
ssl_certificate /etc/hn-gateway-letsencrypt/live/c2.hardnerd-nodes.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/hn-gateway-letsencrypt/live/c2.hardnerd-nodes.com/privkey.pem;
|
||||||
|
root /var/www/html;
|
||||||
|
index index.php;
|
||||||
|
location / { try_files $uri /index.php$is_args$args; }
|
||||||
|
location ~ ^/api/[a-z_]+\.php$ {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
}
|
||||||
|
location ~ \.php$ {
|
||||||
|
try_files $uri /index.php$is_args$args;
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
}
|
||||||
|
location ~ /\. { deny all; }
|
||||||
|
}
|
||||||
|
}
|
||||||
37
scripts/hn/gateway/gateway-nginx.conf
Normal file
37
scripts/hn/gateway/gateway-nginx.conf
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes 1;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
events { worker_connections 1024; }
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
include /etc/nginx/subscribe/real_ip.conf;
|
||||||
|
# Do not record subscription credentials in access logs.
|
||||||
|
log_format main '$remote_addr [$time_local] "$request_method $uri $server_protocol" $status $body_bytes_sent "$http_user_agent"';
|
||||||
|
access_log /var/log/subscribe/access.log main;
|
||||||
|
error_log /var/log/subscribe/error.log crit;
|
||||||
|
sendfile on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
server_tokens off;
|
||||||
|
resolver 1.1.1.1 valid=60s ipv6=off;
|
||||||
|
resolver_timeout 5s;
|
||||||
|
map $http_upgrade $connection_upgrade { default upgrade; '' close; }
|
||||||
|
include /etc/nginx/subscribe/cloud_geo.conf;
|
||||||
|
include /etc/nginx/subscribe/whitelist.conf;
|
||||||
|
include /etc/nginx/subscribe/ua_custom.conf;
|
||||||
|
include /etc/nginx/subscribe/ua_whitelist.conf;
|
||||||
|
server { listen 80 default_server; return 444; }
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
proxy_ssl_verify on;
|
||||||
|
proxy_ssl_verify_depth 3;
|
||||||
|
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
|
||||||
|
include /etc/nginx/subscribe/blacklist.conf;
|
||||||
|
include /etc/nginx/subscribe/protect.conf;
|
||||||
|
location / { return 404; }
|
||||||
|
}
|
||||||
|
}
|
||||||
27
scripts/hn/gateway/gateway-protect.conf
Normal file
27
scripts/hn/gateway/gateway-protect.conf
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
location ^~ /api/v1/client/subscribe {
|
||||||
|
if ($is_cloud_ip = 1) { set $block_reason "cloud"; }
|
||||||
|
if ($bad_subscribe_ua = 1) { set $block_reason "ua"; }
|
||||||
|
if ($is_custom_bad_ua = 1) { set $block_reason "ua"; }
|
||||||
|
if ($is_ua_whitelisted = 1) { set $block_reason ""; }
|
||||||
|
if ($whitelist_ip = 1) { set $block_reason ""; }
|
||||||
|
if ($block_reason = "cloud") { return 403 "Forbidden: Cloud IP"; }
|
||||||
|
if ($block_reason = "ua") { return 403 "Forbidden: Invalid Client"; }
|
||||||
|
limit_req zone=subscribe_limit burst=5 nodelay;
|
||||||
|
limit_req_status 429;
|
||||||
|
set $upstream_backend ${V2B_BACKEND};
|
||||||
|
proxy_pass $upstream_backend;
|
||||||
|
proxy_set_header Host ${V2B_HOST};
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
proxy_ssl_server_name on;
|
||||||
|
proxy_ssl_name ${V2B_HOST};
|
||||||
|
proxy_ssl_verify on;
|
||||||
|
proxy_ssl_verify_depth 3;
|
||||||
|
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_connect_timeout 10s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
add_header Cache-Control "private, no-store" always;
|
||||||
|
add_header X-Subscribe-Filter "active" always;
|
||||||
|
}
|
||||||
53
scripts/hn/gateway/gateway.compose.yml
Normal file
53
scripts/hn/gateway/gateway.compose.yml
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
name: hn-sgw
|
||||||
|
services:
|
||||||
|
gateway:
|
||||||
|
image: sha256:d8807a5b2442bc25ce4fbc1312354b9dcbb0f538d75181ef735f4ea980760e70
|
||||||
|
pull_policy: never
|
||||||
|
container_name: hn-subscribe-gateway
|
||||||
|
restart: unless-stopped
|
||||||
|
mem_limit: 160m
|
||||||
|
cpus: 0.5
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:28080:80"
|
||||||
|
- "127.0.0.1:28443:443"
|
||||||
|
environment:
|
||||||
|
V2B_BACKEND: https://hardnerd.net
|
||||||
|
V2B_HOST: hardnerd.net
|
||||||
|
REAL_IP_HEADER: X-Real-IP
|
||||||
|
AXISNOW_TRUSTED_IPS: 172.28.32.1
|
||||||
|
volumes:
|
||||||
|
- config:/etc/nginx/subscribe
|
||||||
|
- logs:/var/log/subscribe
|
||||||
|
- /etc/hn-gateway-letsencrypt:/etc/hn-gateway-letsencrypt:ro
|
||||||
|
- /var/lib/hn-sgw/ssl:/etc/nginx/ssl:ro
|
||||||
|
- ./gateway-nginx.conf:/etc/nginx/templates-src/nginx.conf:ro
|
||||||
|
- ./gateway-protect.conf:/etc/nginx/templates-src/subscribe_protect.conf.template:ro
|
||||||
|
networks: [hn]
|
||||||
|
logging: {driver: json-file, options: {max-size: "5m", max-file: "2"}}
|
||||||
|
admin:
|
||||||
|
image: sha256:31ff8ea3e8f958a835edbd1579709017da75b742ff3a5984ea59d12ea0775577
|
||||||
|
pull_policy: never
|
||||||
|
container_name: hn-subscribe-admin
|
||||||
|
restart: unless-stopped
|
||||||
|
mem_limit: 128m
|
||||||
|
cpus: 0.5
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:26444:64444"
|
||||||
|
env_file:
|
||||||
|
- path: /var/lib/hn-sgw/admin.env
|
||||||
|
format: raw
|
||||||
|
volumes:
|
||||||
|
- config:/etc/nginx/subscribe
|
||||||
|
- logs:/var/log/subscribe
|
||||||
|
- /etc/hn-gateway-letsencrypt:/etc/hn-gateway-letsencrypt:ro
|
||||||
|
- ./gateway-admin.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
networks: [hn]
|
||||||
|
logging: {driver: json-file, options: {max-size: "5m", max-file: "2"}}
|
||||||
|
volumes:
|
||||||
|
config: {name: hn_sgw_config}
|
||||||
|
logs: {name: hn_sgw_logs}
|
||||||
|
networks:
|
||||||
|
hn:
|
||||||
|
name: hn_sgw_net
|
||||||
|
ipam:
|
||||||
|
config: [{subnet: 172.28.32.0/24}]
|
||||||
5
scripts/hn/gateway/hn-gateway-cert.service
Normal file
5
scripts/hn/gateway/hn-gateway-cert.service
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Renew isolated HN subscription gateway certificate
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/certbot renew --config-dir /etc/hn-gateway-letsencrypt --work-dir /var/lib/hn-sgw/certbot --logs-dir /var/log/hn-gateway-certbot --cert-name c2.hardnerd-nodes.com --no-directory-hooks --no-random-sleep-on-renew --quiet --deploy-hook /opt/hn-gateway/provision/scripts/hn/gateway/renew-gateway.sh
|
||||||
8
scripts/hn/gateway/hn-gateway-cert.timer
Normal file
8
scripts/hn/gateway/hn-gateway-cert.timer
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Renew HN subscription certificate
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*-*-* 04:30:00
|
||||||
|
RandomizedDelaySec=3600
|
||||||
|
Persistent=true
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
5
scripts/hn/gateway/hn-gateway-cloudflare-ips.service
Normal file
5
scripts/hn/gateway/hn-gateway-cloudflare-ips.service
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Refresh HN gateway Cloudflare trusted ranges
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/python3 /opt/hn-gateway/provision/scripts/hn/gateway/update-cloudflare-ips.py
|
||||||
8
scripts/hn/gateway/hn-gateway-cloudflare-ips.timer
Normal file
8
scripts/hn/gateway/hn-gateway-cloudflare-ips.timer
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Refresh HN Cloudflare trust ranges
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*-*-* 04:30:00
|
||||||
|
RandomizedDelaySec=3600
|
||||||
|
Persistent=true
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
39
scripts/hn/gateway/host.conf
Normal file
39
scripts/hn/gateway/host.conf
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name c2.hardnerd-nodes.com hardnerd-nodes.com www.hardnerd-nodes.com;
|
||||||
|
access_log off;
|
||||||
|
location ^~ /.well-known/acme-challenge/ { root /var/lib/hn-sgw/acme; }
|
||||||
|
location / { return 301 https://$host$request_uri; }
|
||||||
|
}
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
server_name c2.hardnerd-nodes.com;
|
||||||
|
include /etc/nginx/hn-cloudflare-realip.conf;
|
||||||
|
ssl_certificate /etc/hn-gateway-letsencrypt/live/c2.hardnerd-nodes.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/hn-gateway-letsencrypt/live/c2.hardnerd-nodes.com/privkey.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
access_log off;
|
||||||
|
error_log /var/log/nginx/hn-subscribe-error.log crit;
|
||||||
|
location ~ ^/s/([^/]+)/?$ {
|
||||||
|
rewrite ^/s/([^/]+)/?$ /api/v1/client/subscribe?token=$1 break;
|
||||||
|
proxy_pass https://127.0.0.1:28443;
|
||||||
|
include /etc/nginx/hn-subscribe-proxy.conf;
|
||||||
|
}
|
||||||
|
location = /api/v1/client/subscribe {
|
||||||
|
proxy_pass https://127.0.0.1:28443;
|
||||||
|
include /etc/nginx/hn-subscribe-proxy.conf;
|
||||||
|
}
|
||||||
|
location / { return 404; }
|
||||||
|
}
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
server_name hardnerd-nodes.com www.hardnerd-nodes.com;
|
||||||
|
ssl_certificate /etc/hn-gateway-letsencrypt/live/c2.hardnerd-nodes.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/hn-gateway-letsencrypt/live/c2.hardnerd-nodes.com/privkey.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
access_log off;
|
||||||
|
return 301 https://hardnerd.net$request_uri;
|
||||||
|
}
|
||||||
95
scripts/hn/gateway/install.py
Executable file
95
scripts/hn/gateway/install.py
Executable file
@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Guarded HN gateway first deployment; never operates existing DPL/FUC containers."""
|
||||||
|
from pathlib import Path
|
||||||
|
import argparse,hashlib,ipaddress,json,os,shutil,socket,subprocess,sys
|
||||||
|
BASE=Path(__file__).resolve().parent
|
||||||
|
STATE=Path('/var/lib/hn-sgw')
|
||||||
|
BACKUP=Path('/root/codex-backups/hn-gateway/20261027-first-install')
|
||||||
|
CERT=Path('/etc/hn-gateway-letsencrypt')
|
||||||
|
HOST=Path('/etc/nginx/conf.d/hn-subscribe.conf')
|
||||||
|
ORIGINAL=['dpl-subscribe-gateway','dpl-subscribe-admin','subscribe-gateway','subscribe-admin']
|
||||||
|
NEW=['hn-subscribe-gateway','hn-subscribe-admin']
|
||||||
|
|
||||||
|
def run(args,**kwargs):return subprocess.run(args,check=True,**kwargs)
|
||||||
|
def output(args):return subprocess.check_output(args,text=True)
|
||||||
|
def write(path,data,mode=0o644):
|
||||||
|
p=Path(path);assert not p.exists(),str(p);p.parent.mkdir(parents=True,exist_ok=True);p.write_text(data);p.chmod(mode)
|
||||||
|
def original_state():
|
||||||
|
containers=json.loads(output(['docker','inspect',*ORIGINAL]))
|
||||||
|
return {'containers':{x['Name']:{'id':x['Id'],'started':x['State']['StartedAt'],'restarts':x['RestartCount'],'running':x['State']['Running']}for x in containers},'nginx':{str(p):hashlib.sha256(p.read_bytes()).hexdigest()for p in Path('/etc/nginx/conf.d').glob('*.conf')if p!=HOST}}
|
||||||
|
def verify_original():
|
||||||
|
before=json.loads((BACKUP/'protected-baseline.json').read_text());now=original_state()
|
||||||
|
assert now['containers']==before['containers'],'Original containers changed; inspect before proceeding'
|
||||||
|
assert all(now['nginx'].get(p)==h for p,h in before['nginx'].items()),'Original Nginx configuration changed'
|
||||||
|
print('DPL/FUC original container IDs, start times and Nginx configurations unchanged.')
|
||||||
|
def compose(*args):run(['docker','compose','-f',str(BASE/'gateway.compose.yml'),*args],cwd=BASE)
|
||||||
|
|
||||||
|
def prepare():
|
||||||
|
assert not STATE.exists() and not CERT.exists() and not HOST.exists()
|
||||||
|
assert not BACKUP.exists()
|
||||||
|
for name in NEW:
|
||||||
|
assert subprocess.run(['docker','container','inspect',name],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL).returncode!=0
|
||||||
|
for port in [28080,28443,26444]:
|
||||||
|
with socket.socket()as s:s.bind(('127.0.0.1',port))
|
||||||
|
available=int(next(line.split()[1]for line in Path('/proc/meminfo').read_text().splitlines()if line.startswith('MemAvailable:')))
|
||||||
|
assert available>=300*1024,'Insufficient available memory for two bounded HN containers'
|
||||||
|
assert shutil.disk_usage('/').free>=1024**3
|
||||||
|
ids=output(['docker','network','ls','-q']).split()
|
||||||
|
networks=json.loads(output(['docker','network','inspect',*ids]))
|
||||||
|
proposed=ipaddress.ip_network('172.28.32.0/24')
|
||||||
|
for n in networks:
|
||||||
|
for c in n.get('IPAM',{}).get('Config')or[]:
|
||||||
|
if c.get('Subnet'):
|
||||||
|
existing=ipaddress.ip_network(c['Subnet']);assert existing.version!=4 or not proposed.overlaps(existing),'HN subnet collision'
|
||||||
|
for image in ['sha256:d8807a5b2442bc25ce4fbc1312354b9dcbb0f538d75181ef735f4ea980760e70','sha256:31ff8ea3e8f958a835edbd1579709017da75b742ff3a5984ea59d12ea0775577']:
|
||||||
|
run(['docker','image','inspect',image],stdout=subprocess.DEVNULL)
|
||||||
|
BACKUP.mkdir(parents=True,mode=0o700);BACKUP.chmod(0o700)
|
||||||
|
write(BACKUP/'protected-baseline.json',json.dumps(original_state(),indent=2),0o600)
|
||||||
|
for folder in [STATE,STATE/'acme',STATE/'ssl',STATE/'certbot',Path('/var/log/hn-gateway-certbot')]:folder.mkdir(mode=0o755,parents=True,exist_ok=False)
|
||||||
|
CERT.mkdir(mode=0o700)
|
||||||
|
# Reuse the same host's existing registered ACME account; never copy or run global renewal hooks.
|
||||||
|
shutil.copytree('/etc/dpl-letsencrypt/accounts',CERT/'accounts')
|
||||||
|
for parent,dirs,files in os.walk(CERT/'accounts'):
|
||||||
|
os.chmod(parent,0o700)
|
||||||
|
for f in files:os.chmod(Path(parent)/f,0o600)
|
||||||
|
write('/etc/nginx/hn-cloudflare-realip.conf',Path('/etc/nginx/dpl-cloudflare-realip.conf').read_text())
|
||||||
|
write('/etc/nginx/hn-subscribe-proxy.conf',(BASE/'proxy.conf').read_text())
|
||||||
|
http=(BASE/'host.conf').read_text().split('\nserver {',1)[0]+'\n'
|
||||||
|
write(HOST,http);run(['nginx','-t']);run(['systemctl','reload','nginx']);verify_original()
|
||||||
|
print('HN HTTP ACME route prepared; existing services untouched.')
|
||||||
|
|
||||||
|
def certificate():
|
||||||
|
assert STATE.is_dir() and CERT.is_dir()
|
||||||
|
verify_original()
|
||||||
|
args=['certbot','certonly','--webroot','-w',str(STATE/'acme'),'--config-dir',str(CERT),'--work-dir',str(STATE/'certbot'),'--logs-dir','/var/log/hn-gateway-certbot','--cert-name','c2.hardnerd-nodes.com','-d','c2.hardnerd-nodes.com','-d','hardnerd-nodes.com','-d','www.hardnerd-nodes.com','--non-interactive','--no-directory-hooks']
|
||||||
|
with (BACKUP/'certificate.log').open('w')as log:run(args,stdout=log,stderr=subprocess.STDOUT)
|
||||||
|
print(output(['openssl','x509','-in',str(CERT/'live/c2.hardnerd-nodes.com/fullchain.pem'),'-noout','-dates','-ext','subjectAltName']))
|
||||||
|
|
||||||
|
def start():
|
||||||
|
verify_original();assert (CERT/'live/c2.hardnerd-nodes.com/fullchain.pem').is_file()
|
||||||
|
secret=json.load(sys.stdin)
|
||||||
|
assert isinstance(secret.get('username'),str)and isinstance(secret.get('password'),str)
|
||||||
|
assert all('\n' not in secret[k]for k in ['username','password'])
|
||||||
|
assert not (STATE/'admin.env').exists()
|
||||||
|
write(STATE/'admin.env','ADMIN_USER='+secret['username']+'\nADMIN_PASS='+secret['password']+'\nV2B_BACKEND=https://hardnerd.net\nV2B_HOST=hardnerd.net\n',0o600)
|
||||||
|
for name,target in [('cert.pem',CERT/'live/c2.hardnerd-nodes.com/fullchain.pem'),('key.pem',CERT/'live/c2.hardnerd-nodes.com/privkey.pem')]:
|
||||||
|
(STATE/'ssl'/name).symlink_to(target)
|
||||||
|
run(['docker','volume','create','hn_sgw_config'],stdout=subprocess.DEVNULL)
|
||||||
|
directory=Path(output(['docker','volume','inspect','-f','{{.Mountpoint}}','hn_sgw_config']).strip())
|
||||||
|
write(directory/'whitelist_ips.txt','104.168.30.40\n')
|
||||||
|
compose('config','--quiet');compose('up','-d')
|
||||||
|
for name in NEW:run(['docker','exec',name,'nginx','-t'])
|
||||||
|
old=HOST.read_text();write(BACKUP/'hn-http-only.conf',old,0o600)
|
||||||
|
HOST.write_text((BASE/'host.conf').read_text())
|
||||||
|
try:run(['nginx','-t']);run(['systemctl','reload','nginx'])
|
||||||
|
except Exception:HOST.write_text(old);run(['nginx','-t']);raise
|
||||||
|
for name in ['hn-gateway-cert.service','hn-gateway-cert.timer','hn-gateway-cloudflare-ips.service','hn-gateway-cloudflare-ips.timer']:
|
||||||
|
write(Path('/etc/systemd/system')/name,(BASE/name).read_text())
|
||||||
|
run(['systemctl','daemon-reload']);run(['systemctl','enable','--now','hn-gateway-cert.timer','hn-gateway-cloudflare-ips.timer'])
|
||||||
|
write(STATE/'deployment.json',json.dumps({'commit':output(['git','-C',str(BASE),'rev-parse','HEAD']).strip(),'host':'c2.hardnerd-nodes.com','upstream':'https://hardnerd.net','containers':NEW,'admin_address':'https://127.0.0.1:26444','public_admin':False},indent=2),0o600)
|
||||||
|
verify_original();print('HN isolated gateway started; valid-token acceptance still required.')
|
||||||
|
|
||||||
|
if __name__=='__main__':
|
||||||
|
parser=argparse.ArgumentParser();parser.add_argument('phase',choices=['prepare','certificate','start','verify-original']);args=parser.parse_args()
|
||||||
|
assert os.geteuid()==0,'Run on the registered gateway host as root'
|
||||||
|
{'prepare':prepare,'certificate':certificate,'start':start,'verify-original':verify_original}[args.phase]()
|
||||||
16
scripts/hn/gateway/proxy.conf
Normal file
16
scripts/hn/gateway/proxy.conf
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
proxy_set_header Host c2.hardnerd-nodes.com;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_ssl_server_name on;
|
||||||
|
proxy_ssl_name c2.hardnerd-nodes.com;
|
||||||
|
proxy_ssl_verify on;
|
||||||
|
proxy_ssl_verify_depth 3;
|
||||||
|
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
|
||||||
|
proxy_connect_timeout 10s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
proxy_hide_header Cache-Control;
|
||||||
|
add_header Cache-Control "private, no-store" always;
|
||||||
|
proxy_no_cache 1;
|
||||||
|
proxy_cache_bypass 1;
|
||||||
9
scripts/hn/gateway/renew-gateway.sh
Executable file
9
scripts/hn/gateway/renew-gateway.sh
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
[ "${RENEWED_LINEAGE:-}" = /etc/hn-gateway-letsencrypt/live/c2.hardnerd-nodes.com ] || exit 0
|
||||||
|
nginx -t
|
||||||
|
for container in hn-subscribe-gateway hn-subscribe-admin; do
|
||||||
|
docker exec "$container" nginx -t
|
||||||
|
docker exec "$container" nginx -s reload
|
||||||
|
done
|
||||||
|
systemctl reload nginx
|
||||||
40
scripts/hn/gateway/update-cloudflare-ips.py
Executable file
40
scripts/hn/gateway/update-cloudflare-ips.py
Executable file
@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Update the gateway vhosts' trusted ranges, retaining the last valid configuration on failure."""
|
||||||
|
import ipaddress
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
target = pathlib.Path("/etc/nginx/hn-cloudflare-realip.conf")
|
||||||
|
ranges = []
|
||||||
|
for version in (4, 6):
|
||||||
|
result = subprocess.run(["curl", "-4", "--fail", "--silent", "--show-error", "--max-time", "30",
|
||||||
|
f"https://www.cloudflare.com/ips-v{version}"], check=True, capture_output=True, text=True)
|
||||||
|
nets = [ipaddress.ip_network(line.strip(), strict=True) for line in result.stdout.splitlines() if line.strip()]
|
||||||
|
assert 5 <= len(nets) <= 100 if version == 4 else 2 <= len(nets) <= 100, "Unexpected range count"
|
||||||
|
assert all(n.version == version and n.prefixlen >= (8 if version == 4 else 16) for n in nets), "Invalid trust range"
|
||||||
|
ranges.extend(str(n) for n in nets)
|
||||||
|
assert len(ranges) == len(set(ranges)), "Duplicate ranges"
|
||||||
|
data = ("# Subscription vhosts: synchronized from Cloudflare official lists.\n" +
|
||||||
|
"".join(f"set_real_ip_from {n};\n" for n in ranges) +
|
||||||
|
"real_ip_header CF-Connecting-IP;\nreal_ip_recursive off;\n").encode()
|
||||||
|
old = target.read_bytes() if target.exists() else None
|
||||||
|
if old == data:
|
||||||
|
print("Cloudflare trust ranges unchanged")
|
||||||
|
raise SystemExit
|
||||||
|
temporary = target.with_suffix(".conf.new")
|
||||||
|
temporary.write_bytes(data)
|
||||||
|
temporary.chmod(0o644)
|
||||||
|
os.replace(temporary, target)
|
||||||
|
try:
|
||||||
|
subprocess.run(["nginx", "-t"], check=True)
|
||||||
|
subprocess.run(["systemctl", "reload", "nginx"], check=True)
|
||||||
|
except Exception:
|
||||||
|
if old is None:
|
||||||
|
target.unlink()
|
||||||
|
else:
|
||||||
|
temporary.write_bytes(old)
|
||||||
|
temporary.chmod(0o644)
|
||||||
|
os.replace(temporary, target)
|
||||||
|
raise
|
||||||
|
print(f"HN Cloudflare trust ranges updated: {len(ranges)} ranges")
|
||||||
Loading…
Reference in New Issue
Block a user