ops: migrate HN subscription hostname to t34
This commit is contained in:
parent
ffa338f0ee
commit
360c155fae
@ -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.
|
||||||
|
|||||||
@ -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; }
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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__':
|
||||||
|
|||||||
63
scripts/hn/gateway/migrate-t34.py
Normal file
63
scripts/hn/gateway/migrate-t34.py
Normal 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]()
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user