#!/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,time 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','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/t34.hardnerd-nodes.com/fullchain.pem'),'-noout','-dates','-ext','subjectAltName'])) def start(): 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/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) run(['docker','volume','create','hn_sgw_config'],stdout=subprocess.DEVNULL) directory=Path(output(['docker','volume','inspect','-f','{{.Mountpoint}}','hn_sgw_config']).strip()) if not (directory/'whitelist_ips.txt').exists():write(directory/'whitelist_ips.txt','104.168.30.40\n') else:assert (directory/'whitelist_ips.txt').read_text()=='104.168.30.40\n' compose('config','--quiet');compose('up','-d') # The image downloads its public IP list on first start before creating cloud_geo.conf. deadline=time.monotonic()+55 for name in NEW: while subprocess.run(['docker','exec',name,'nginx','-t'],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL).returncode: assert time.monotonic()