ops: migrate HN subscription hostname to t34

This commit is contained in:
hellcat 2026-10-28 12:45:10 +08:00
parent ffa338f0ee
commit 360c155fae
8 changed files with 83 additions and 18 deletions

View File

@ -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. 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. 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. 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. 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.

View File

@ -8,8 +8,8 @@ http {
server_tokens off; server_tokens off;
server { server {
listen 64444 ssl; listen 64444 ssl;
ssl_certificate /etc/hn-gateway-letsencrypt/live/c2.hardnerd-nodes.com/fullchain.pem; ssl_certificate /etc/hn-gateway-letsencrypt/live/t34.hardnerd-nodes.com/fullchain.pem;
ssl_certificate_key /etc/hn-gateway-letsencrypt/live/c2.hardnerd-nodes.com/privkey.pem; ssl_certificate_key /etc/hn-gateway-letsencrypt/live/t34.hardnerd-nodes.com/privkey.pem;
root /var/www/html; root /var/www/html;
index index.php; index index.php;
location / { try_files $uri /index.php$is_args$args; } location / { try_files $uri /index.php$is_args$args; }

View File

@ -2,4 +2,4 @@
Description=Renew isolated HN subscription gateway certificate Description=Renew isolated HN subscription gateway certificate
[Service] [Service]
Type=oneshot 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

View File

@ -1,7 +1,7 @@
server { server {
listen 80; listen 80;
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; access_log off;
location ^~ /.well-known/acme-challenge/ { root /var/lib/hn-sgw/acme; } location ^~ /.well-known/acme-challenge/ { root /var/lib/hn-sgw/acme; }
location / { return 301 https://$host$request_uri; } location / { return 301 https://$host$request_uri; }
@ -9,10 +9,10 @@ server {
server { server {
listen 443 ssl; listen 443 ssl;
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; include /etc/nginx/hn-cloudflare-realip.conf;
ssl_certificate /etc/hn-gateway-letsencrypt/live/c2.hardnerd-nodes.com/fullchain.pem; ssl_certificate /etc/hn-gateway-letsencrypt/live/t34.hardnerd-nodes.com/fullchain.pem;
ssl_certificate_key /etc/hn-gateway-letsencrypt/live/c2.hardnerd-nodes.com/privkey.pem; ssl_certificate_key /etc/hn-gateway-letsencrypt/live/t34.hardnerd-nodes.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2 TLSv1.3;
access_log off; access_log off;
error_log /var/log/nginx/hn-subscribe-error.log crit; error_log /var/log/nginx/hn-subscribe-error.log crit;
@ -31,8 +31,8 @@ server {
listen 443 ssl; listen 443 ssl;
listen [::]:443 ssl; listen [::]:443 ssl;
server_name hardnerd-nodes.com www.hardnerd-nodes.com; server_name hardnerd-nodes.com www.hardnerd-nodes.com;
ssl_certificate /etc/hn-gateway-letsencrypt/live/c2.hardnerd-nodes.com/fullchain.pem; ssl_certificate /etc/hn-gateway-letsencrypt/live/t34.hardnerd-nodes.com/fullchain.pem;
ssl_certificate_key /etc/hn-gateway-letsencrypt/live/c2.hardnerd-nodes.com/privkey.pem; ssl_certificate_key /etc/hn-gateway-letsencrypt/live/t34.hardnerd-nodes.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2 TLSv1.3;
access_log off; access_log off;
return 301 https://hardnerd.net$request_uri; return 301 https://hardnerd.net$request_uri;

View File

@ -61,19 +61,19 @@ def prepare():
def certificate(): def certificate():
assert STATE.is_dir() and CERT.is_dir() assert STATE.is_dir() and CERT.is_dir()
verify_original() 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) 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(): 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) secret=json.load(sys.stdin)
assert isinstance(secret.get('username'),str)and isinstance(secret.get('password'),str) 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 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' 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' 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) 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 link=STATE/'ssl'/name
if link.is_symlink():assert link.resolve()==target.resolve() if link.is_symlink():assert link.resolve()==target.resolve()
else:link.symlink_to(target) 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']: 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()) 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']) 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.') verify_original();print('HN isolated gateway started; valid-token acceptance still required.')
if __name__=='__main__': if __name__=='__main__':

View File

@ -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()<deadline,'HN readiness timed out; inspect and restore backup'
time.sleep(2)
run(['docker','exec',name,'nginx','-t'])
run(['nginx','-t']);run(['systemctl','reload','nginx']);run(['systemctl','daemon-reload'])
data=json.loads((STATE/'deployment.json').read_text());data['host']=NEW;data['commit']=subprocess.check_output(['git','-C',str(BASE),'rev-parse','HEAD'],text=True).strip()
(STATE/'deployment.json').write_text(json.dumps(data,indent=2));(STATE/'deployment.json').chmod(0o600)
protected();(BACKUP/'activated.json').write_text(json.dumps({'host':NEW,'commit':data['commit']}));(BACKUP/'activated.json').chmod(0o600)
print('HN t34 gateway activated. Application URL and DNS retirement require final acceptance.')
if __name__=='__main__':
assert os.geteuid()==0
parser=argparse.ArgumentParser();parser.add_argument('phase',choices=['prepare','certificate','activate','verify-original']);args=parser.parse_args()
{'prepare':prepare,'certificate':certificate,'activate':activate,'verify-original':protected}[args.phase]()

View File

@ -1,10 +1,10 @@
proxy_set_header Host c2.hardnerd-nodes.com; proxy_set_header Host t34.hardnerd-nodes.com;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-Proto https;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_ssl_server_name on; proxy_ssl_server_name on;
proxy_ssl_name c2.hardnerd-nodes.com; proxy_ssl_name t34.hardnerd-nodes.com;
proxy_ssl_verify on; proxy_ssl_verify on;
proxy_ssl_verify_depth 3; proxy_ssl_verify_depth 3;
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;

View File

@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
set -eu set -eu
[ "${RENEWED_LINEAGE:-}" = /etc/hn-gateway-letsencrypt/live/c2.hardnerd-nodes.com ] || exit 0 [ "${RENEWED_LINEAGE:-}" = /etc/hn-gateway-letsencrypt/live/t34.hardnerd-nodes.com ] || exit 0
nginx -t nginx -t
for container in hn-subscribe-gateway hn-subscribe-admin; do for container in hn-subscribe-gateway hn-subscribe-admin; do
docker exec "$container" nginx -t docker exec "$container" nginx -t