71 lines
3.9 KiB
Python
71 lines
3.9 KiB
Python
"""Freeze public chrony dependencies and narrow IPv4 egress for the PID1 realm."""
|
|
import hashlib
|
|
import io
|
|
import ipaddress
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
|
|
out=Path(sys.argv[1])
|
|
image='d75bb2137bd95cae072ebdd83c4e53e7a50366b5fc7ecd986b700860f42e09c2'
|
|
name='qualification-c01-nts-deps-'+out.name
|
|
env={**os.environ,'CONTAINERS_CONF_OVERRIDE':str(out/'containers.conf')}
|
|
base=['podman','run','--rm','--pull=never','--network=none','--read-only','--cap-drop=ALL',
|
|
'--security-opt=no-new-privileges','--cpus=2','--memory=512m','--memory-swap=512m',
|
|
'--http-proxy=false','--entrypoint=/usr/bin/ldd',image]
|
|
listing=subprocess.check_output([*base,'/usr/sbin/chronyd','/usr/bin/chronyc'],env=env,text=True)
|
|
paths={'/usr/sbin/chronyd','/usr/bin/chronyc'} | {w for w in listing.split() if w.startswith('/') and not w.endswith(':')}
|
|
ordered=sorted(paths)
|
|
resolved=subprocess.check_output([*base[:-2],'--entrypoint=/usr/bin/readlink',image,'-f',*ordered],env=env,text=True).splitlines()
|
|
assert len(resolved)==len(ordered)
|
|
targets=dict(zip(ordered,resolved))
|
|
subprocess.run(['podman','create','--pull=never','--name',name,'--network=none',
|
|
'--cap-drop=ALL','--entrypoint=/bin/false',image],env=env,check=True,stdout=subprocess.DEVNULL)
|
|
files={};manifest={}
|
|
for path in sorted(paths):
|
|
assert '..' not in Path(path).parts
|
|
raw=subprocess.check_output(['podman','cp',name+':'+targets[path],'-'],env=env)
|
|
with tarfile.open(fileobj=io.BytesIO(raw)) as archive:
|
|
members=[m for m in archive.getmembers() if m.isfile()]
|
|
assert len(members)==1 and members[0].uid==0 and not members[0].mode&0o022
|
|
data=archive.extractfile(members[0]).read()
|
|
assert data.startswith(b'\x7fELF')
|
|
files[path]=data
|
|
manifest[path]={'source_image':image,'source_image_path':targets[path],'sha256':hashlib.sha256(data).hexdigest()}
|
|
# One pinned public shared-library closure for nft and the required Tor owner
|
|
# tool. Never overlay a second libcap/loader variant after NTS verification.
|
|
# No host configurations, keys or services are copied.
|
|
host_paths=set()
|
|
for program in ('/usr/sbin/nft','/usr/bin/tor'):
|
|
host_paths.add(program)
|
|
host_paths.update(w for w in subprocess.check_output(['ldd',program],text=True).split() if w.startswith('/'))
|
|
for path in host_paths:
|
|
source=Path(path).resolve(strict=True)
|
|
for entry in (source,*source.parents):
|
|
meta=entry.stat();assert meta.st_uid==0 and not meta.st_mode&0o022
|
|
files[path]=source.read_bytes()
|
|
manifest[path]={'source':str(source),'sha256':hashlib.sha256(files[path]).hexdigest()}
|
|
with tarfile.open(out/'chrony-runtime.tar','x') as archive:
|
|
for path,data in sorted(files.items()):
|
|
item=tarfile.TarInfo(path.lstrip('/'));item.size=len(data);item.mode=0o755
|
|
archive.addfile(item,io.BytesIO(data))
|
|
addresses={name:sorted({str(ipaddress.IPv4Address(row[4][0])) for row in
|
|
socket.getaddrinfo(name,4460,family=socket.AF_INET,type=socket.SOCK_STREAM)})
|
|
for name in ('time.cloudflare.com','nts.netnod.se')}
|
|
rules=['table inet c01_nts {',
|
|
'chain input { type filter hook input priority 0; policy drop; iifname "lo" accept; ct state established,related accept; }',
|
|
'chain forward { type filter hook forward priority 0; policy drop; }',
|
|
'chain output { type filter hook output priority 0; policy drop; oifname "lo" accept; ct state established,related accept;']
|
|
for address in sorted(set(sum(addresses.values(),[]))):
|
|
rules += [f'ip daddr {address} tcp dport 4460 accept;',f'ip daddr {address} udp dport 123 accept;']
|
|
for address in ('1.1.1.1','1.0.0.1'):
|
|
rules += [f'ip daddr {address} udp dport 53 accept;',f'ip daddr {address} tcp dport 53 accept;']
|
|
rules += ['}','}']
|
|
(out/'nts-egress.nft').write_text('\n'.join(rules)+'\n')
|
|
(out/'nts-addresses.json').write_text(json.dumps(addresses,indent=2)+'\n')
|
|
(out/'chrony-artifact.json').write_text(json.dumps(manifest,indent=2,sort_keys=True)+'\n')
|