From 360c155fae5bbf86654fdcba1d0de33b80fb7de4 Mon Sep 17 00:00:00 2001 From: hellcat Date: Wed, 28 Oct 2026 12:45:10 +0800 Subject: [PATCH] ops: migrate HN subscription hostname to t34 --- scripts/hn/gateway/README.md | 4 +- scripts/hn/gateway/gateway-admin.conf | 4 +- scripts/hn/gateway/hn-gateway-cert.service | 2 +- scripts/hn/gateway/host.conf | 12 ++--- scripts/hn/gateway/install.py | 10 ++-- scripts/hn/gateway/migrate-t34.py | 63 ++++++++++++++++++++++ scripts/hn/gateway/proxy.conf | 4 +- scripts/hn/gateway/renew-gateway.sh | 2 +- 8 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 scripts/hn/gateway/migrate-t34.py diff --git a/scripts/hn/gateway/README.md b/scripts/hn/gateway/README.md index e78fe1e..970d0f1 100644 --- a/scripts/hn/gateway/README.md +++ b/scripts/hn/gateway/README.md @@ -2,7 +2,7 @@ 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. +Target: registered gateway-01 via the white SSH jump host. Public subscription host: t34.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. @@ -13,3 +13,5 @@ Public paths are limited to /s/{token} and /api/v1/client/subscribe. Host access 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. + +2026-10-28 endpoint migration: publish first, then run migrate-t34.py prepare, certificate, activate in sequence. The old c2 certificate is retained for rollback; the active certificate, SNI and isolated renewal service use t34. Retire old DNS only after new endpoint and application URL acceptance. Backup: /root/codex-backups/hn-gateway/20261028-t34. diff --git a/scripts/hn/gateway/gateway-admin.conf b/scripts/hn/gateway/gateway-admin.conf index ada317a..44f2a78 100644 --- a/scripts/hn/gateway/gateway-admin.conf +++ b/scripts/hn/gateway/gateway-admin.conf @@ -8,8 +8,8 @@ http { 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; + ssl_certificate /etc/hn-gateway-letsencrypt/live/t34.hardnerd-nodes.com/fullchain.pem; + ssl_certificate_key /etc/hn-gateway-letsencrypt/live/t34.hardnerd-nodes.com/privkey.pem; root /var/www/html; index index.php; location / { try_files $uri /index.php$is_args$args; } diff --git a/scripts/hn/gateway/hn-gateway-cert.service b/scripts/hn/gateway/hn-gateway-cert.service index 86c38c6..5c27a91 100644 --- a/scripts/hn/gateway/hn-gateway-cert.service +++ b/scripts/hn/gateway/hn-gateway-cert.service @@ -2,4 +2,4 @@ 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 +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 t34.hardnerd-nodes.com --no-directory-hooks --no-random-sleep-on-renew --quiet --deploy-hook /opt/hn-gateway/provision/scripts/hn/gateway/renew-gateway.sh diff --git a/scripts/hn/gateway/host.conf b/scripts/hn/gateway/host.conf index cc45b0c..2126aff 100644 --- a/scripts/hn/gateway/host.conf +++ b/scripts/hn/gateway/host.conf @@ -1,7 +1,7 @@ server { listen 80; listen [::]:80; - server_name c2.hardnerd-nodes.com hardnerd-nodes.com www.hardnerd-nodes.com; + server_name t34.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; } @@ -9,10 +9,10 @@ server { server { listen 443 ssl; listen [::]:443 ssl; - server_name c2.hardnerd-nodes.com; + server_name t34.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_certificate /etc/hn-gateway-letsencrypt/live/t34.hardnerd-nodes.com/fullchain.pem; + ssl_certificate_key /etc/hn-gateway-letsencrypt/live/t34.hardnerd-nodes.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; access_log off; error_log /var/log/nginx/hn-subscribe-error.log crit; @@ -31,8 +31,8 @@ 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_certificate /etc/hn-gateway-letsencrypt/live/t34.hardnerd-nodes.com/fullchain.pem; + ssl_certificate_key /etc/hn-gateway-letsencrypt/live/t34.hardnerd-nodes.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; access_log off; return 301 https://hardnerd.net$request_uri; diff --git a/scripts/hn/gateway/install.py b/scripts/hn/gateway/install.py index af852c1..1627c3b 100755 --- a/scripts/hn/gateway/install.py +++ b/scripts/hn/gateway/install.py @@ -61,19 +61,19 @@ def prepare(): 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'] + 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','t34.hardnerd-nodes.com','-d','t34.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'])) + print(output(['openssl','x509','-in',str(CERT/'live/t34.hardnerd-nodes.com/fullchain.pem'),'-noout','-dates','-ext','subjectAltName'])) def start(): - verify_original();assert (CERT/'live/c2.hardnerd-nodes.com/fullchain.pem').is_file() + verify_original();assert (CERT/'live/t34.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']) environment='ADMIN_USER='+secret['username']+'\nADMIN_PASS='+secret['password']+'\nV2B_BACKEND=https://hardnerd.net\nV2B_HOST=hardnerd.net\n' if (STATE/'admin.env').exists():assert (STATE/'admin.env').read_text()==environment,'Existing HN credentials differ; stop for inspection' else:write(STATE/'admin.env',environment,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')]: + for name,target in [('cert.pem',CERT/'live/t34.hardnerd-nodes.com/fullchain.pem'),('key.pem',CERT/'live/t34.hardnerd-nodes.com/privkey.pem')]: link=STATE/'ssl'/name if link.is_symlink():assert link.resolve()==target.resolve() else:link.symlink_to(target) @@ -96,7 +96,7 @@ def start(): 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) + write(STATE/'deployment.json',json.dumps({'commit':output(['git','-C',str(BASE),'rev-parse','HEAD']).strip(),'host':'t34.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__': diff --git a/scripts/hn/gateway/migrate-t34.py b/scripts/hn/gateway/migrate-t34.py new file mode 100644 index 0000000..f6acba7 --- /dev/null +++ b/scripts/hn/gateway/migrate-t34.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""One-time, guarded HN endpoint migration; no DPL/FUC mutations.""" +from pathlib import Path +import argparse,json,os,shutil,subprocess,sys,time +sys.dont_write_bytecode=True +sys.path.insert(0,str(Path(__file__).resolve().parent)) +import install as deployment +BASE=Path(__file__).resolve().parent +BACKUP=Path('/root/codex-backups/hn-gateway/20261028-t34') +STATE=deployment.STATE +OLD='c2.hardnerd-nodes.com' +NEW='t34.hardnerd-nodes.com' +HOST=deployment.HOST +PROXY=Path('/etc/nginx/hn-subscribe-proxy.conf') +UNIT=Path('/etc/systemd/system/hn-gateway-cert.service') +def run(args,**kw):return subprocess.run(args,check=True,**kw) +def protected(): + before=json.loads((BACKUP/'protected.json').read_text());now=deployment.original_state() + assert now['containers']==before['containers'],'Original containers changed' + assert all(now['nginx'].get(k)==v for k,v in before['nginx'].items()),'Original virtual hosts changed' + print('DPL/FUC container and original Nginx baseline preserved.') +def prepare(): + assert not BACKUP.exists() and OLD in HOST.read_text() + assert json.loads((STATE/'deployment.json').read_text())['host']==OLD + BACKUP.mkdir(mode=0o700,parents=True) + (BACKUP/'protected.json').write_text(json.dumps(deployment.original_state()));(BACKUP/'protected.json').chmod(0o600) + for source,name in [(HOST,'host.conf'),(PROXY,'proxy.conf'),(UNIT,'cert.service'),(STATE/'deployment.json','deployment.json')]: + shutil.copy2(source,BACKUP/name);(BACKUP/name).chmod(0o600) + (BACKUP/'symlinks.json').write_text(json.dumps({name:os.readlink(STATE/'ssl'/name) for name in ['cert.pem','key.pem']}));(BACKUP/'symlinks.json').chmod(0o600) + http='server_name '+OLD+' hardnerd-nodes.com www.hardnerd-nodes.com;' + s=HOST.read_text();assert s.count(http)==1 + HOST.write_text(s.replace(http,'server_name '+OLD+' '+NEW+' hardnerd-nodes.com www.hardnerd-nodes.com;',1)) + try:run(['nginx','-t']);run(['systemctl','reload','nginx']) + except Exception:shutil.copy2(BACKUP/'host.conf',HOST);raise + protected();print('HN t34 ACME route ready; c2 subscription still active.') +def certificate(): + protected() + args=['certbot','certonly','--webroot','-w',str(STATE/'acme'),'--config-dir',str(deployment.CERT),'--work-dir',str(STATE/'certbot'),'--logs-dir','/var/log/hn-gateway-certbot','--cert-name',NEW,'-d',NEW,'-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) + run(['openssl','x509','-in',str(deployment.CERT/'live'/NEW/'fullchain.pem'),'-noout','-dates','-ext','subjectAltName']) +def activate(): + protected();assert not (BACKUP/'activated.json').exists() + assert (deployment.CERT/'live'/NEW/'fullchain.pem').is_file() + for name,file in [('cert.pem','fullchain.pem'),('key.pem','privkey.pem')]: + temp=STATE/'ssl'/(name+'.new');assert not temp.exists();temp.symlink_to(deployment.CERT/'live'/NEW/file);temp.replace(STATE/'ssl'/name) + PROXY.write_text((BASE/'proxy.conf').read_text());HOST.write_text((BASE/'host.conf').read_text());UNIT.write_text((BASE/'hn-gateway-cert.service').read_text()) + # Recreate only HN so bind-mounted templates pick up their new inode, retaining volumes and credentials. + deployment.compose('up','-d','--force-recreate','gateway','admin') + deadline=time.monotonic()+55 + for name in deployment.NEW: + while subprocess.run(['docker','exec',name,'nginx','-t'],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL).returncode: + assert time.monotonic()