Files
archy/scripts/os_updater_activate.py

336 lines
17 KiB
Python

#!/usr/bin/python3 -I
"""One-shot independent-admin bootstrap into the actual D09 lifecycle owner.
No application entrypoint, unsigned mode, arbitrary command, ledger reset or
financial restore. A failed/interrupted activation always requires external
reconciliation. The owner alone subsequently consumes APPLY authority.
"""
import argparse
import ast
import fcntl
import json
import os
from pathlib import Path
import pwd
import subprocess
import sys
import time
sys.path.insert(0, str(Path(__file__).resolve().parent))
import os_updater_bootstrap as bootstrap
BASE = Path('/var/lib/archipelago-os-bootstrap')
OS_ROOT = Path('/var/lib/archipelago-os-update')
LEGACY_OS_ROOT = Path('/var/lib/archipelago/os-update')
D09 = Path('/var/lib/archipelago-d09')
ADMISSION = D09 / 'incoming/admission.json'
VERIFIER = Path('/usr/libexec/archipelago-os-release-verifier')
TIMERS = ('apt-daily.timer', 'apt-daily-upgrade.timer', 'apt-daily.service',
'apt-daily-upgrade.service', 'unattended-upgrades.service')
OWNER_DROPIN = Path('/etc/systemd/system/archipelago-d09-lifecycle.service.d/55-os-bootstrap.conf')
OWNER_POLICY = (b'[Service]\n'
b'ReadWritePaths=/var/lib/dpkg/lock-frontend /var/lib/dpkg/lock '
b'/var/lib/archipelago-os-update/transactions '
b'/var/lib/archipelago-os-update/executor.lock\n')
OS_ROOTS = (Path('/var/lib/archipelago-os-update'),)
def owner_policy(root):
if root not in OS_ROOTS:
raise ValueError('unsupported protected OS state location')
return (b'[Service]\nReadWritePaths=/var/lib/dpkg/lock-frontend /var/lib/dpkg/lock '
+ str(root / 'transactions').encode() + b' '
+ str(root / 'executor.lock').encode() + b'\n')
def declared_root(package):
"""Read the pinned executor's fixed storage ABI without executing its code."""
tree = ast.parse(bootstrap.read(package / 'scripts/os_update_executor.py'))
roots = [node.value for node in tree.body if isinstance(node, ast.Assign)
and any(isinstance(t, ast.Name) and t.id == 'ROOT' for t in node.targets)]
if len(roots) != 1 or not isinstance(roots[0], ast.Call):
raise ValueError('executor ROOT declaration')
call = roots[0]
if (not isinstance(call.func, ast.Attribute) or call.func.attr != 'Path'
or not isinstance(call.func.value, ast.Name) or call.func.value.id != 'pathlib'
or len(call.args) != 1 or call.keywords):
raise ValueError('executor fixed ROOT declaration')
root = Path(ast.literal_eval(call.args[0]))
owner_policy(root)
return root
def command(*args):
result = subprocess.run(args, check=True, capture_output=True, text=True,
timeout=120, env={'PATH': '/usr/sbin:/usr/bin:/sbin:/bin',
'LC_ALL': 'C', 'PYTHONDONTWRITEBYTECODE': '1'})
return result.stdout
def protected_dir(path):
fd = bootstrap.secure(path, directory=True)
os.close(fd)
def ensure_dir(path):
if not path.exists():
ensure_dir(path.parent)
path.mkdir(mode=0o700)
bootstrap.sync(path.parent)
protected_dir(path)
def destination_custody(path):
"""Reject an application-owned ancestor before consuming activation intent."""
while not path.exists() and not path.is_symlink():
path = path.parent
protected_dir(path)
def same(path, expected):
raw = bootstrap.read(path)
if bootstrap.digest(raw) != bootstrap.pin(expected):
raise ValueError('activation pin drift: ' + str(path))
return raw
def package_inventory(root):
protected_dir(root)
result = {}
for directory, dirs, files in os.walk(root, followlinks=False):
protected_dir(Path(directory))
for name in dirs:
protected_dir(Path(directory) / name)
for name in files:
path = Path(directory) / name
result[str(path.relative_to(root))] = bootstrap.digest(bootstrap.read(path))
return result
def untouched_authority():
# Never adopt/reset state from the retired application-owned ancestry.
try:
parent = os.open(LEGACY_OS_ROOT.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
except FileNotFoundError:
parent = None
if parent is not None:
try:
try:
os.stat(LEGACY_OS_ROOT.name, dir_fd=parent, follow_symlinks=False)
except FileNotFoundError:
pass
else:
raise ValueError('legacy OS state requires explicit root migration/reconciliation')
finally:
os.close(parent)
for path in (D09 / 'ledger.json', Path('/etc/archipelago-d09/binding.json')):
if path.exists() or path.is_symlink():
raise ValueError('existing D09 authority requires reconciliation')
for root in set((*OS_ROOTS, OS_ROOT)):
transactions = root / 'transactions'
if transactions.exists() or transactions.is_symlink():
protected_dir(transactions)
if any(transactions.iterdir()):
raise ValueError('existing OS transaction requires reconciliation')
def current_exclusion():
# External administrator establishes exclusion; bootstrap only verifies it.
# Leave existing timers/accounts/keys/access policy unchanged.
for unit in TIMERS:
raw = command('/usr/bin/systemctl', 'show', unit, '--property=ActiveState,UnitFileState')
value = dict(line.split('=', 1) for line in raw.splitlines() if '=' in line)
if value.get('ActiveState') not in ('inactive', 'failed') or value.get('UnitFileState') not in ('masked', 'masked-runtime'):
raise ValueError('package writer exclusion not established: ' + unit)
raw = command('/usr/bin/systemctl', 'show', 'archipelago.service', '--property=MainPID,ActiveState')
value = dict(line.split('=', 1) for line in raw.splitlines() if '=' in line)
if value.get('MainPID') != '0' or value.get('ActiveState') not in ('inactive', 'failed'):
raise ValueError('old manager must be externally drained')
def activate(approval_path, expected):
global OS_ROOT
if os.geteuid() != 0:
raise ValueError('independent root administrator required')
approval_raw = same(approval_path, expected)
approval = bootstrap.decode(approval_raw)
if set(approval) != {'version', 'prepared_sha256', 'package_root', 'package_files',
'admission_sha256', 'os_inputs', 'helper_sha256', 'owner_dropin_sha256'} or approval['version'] != 1:
raise ValueError('activation approval fields')
# Pin the implementation as well as the package it will execute.
same(Path(__file__), approval['helper_sha256'])
protected_dir(BASE)
lock = os.open(BASE / 'lock', os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
try:
fd = bootstrap.secure(BASE / 'lock'); os.close(fd)
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
if any(BASE.glob('activation-*')):
raise ValueError('existing activation intent; external reconciliation required')
untouched_authority()
prepared = BASE / 'prepared'
raw = same(prepared / 'approval.json', approval['prepared_sha256'])
plan = bootstrap.decode(raw)
bootstrap.validate(plan)
control = bootstrap.decode(bootstrap.read(prepared / 'control.json'))
if control.get('state') != 'PREPARED_NOT_ACTIVATED' or control.get('approval_sha256') != approval['prepared_sha256']:
raise ValueError('prepared control differs')
same(Path('/etc/machine-id'), plan['machine_id_sha256'])
same(Path('/usr/local/bin/archipelago'), plan['current']['backend_sha256'])
for path, sha in plan['observations'].items():
same(Path('/') / path, sha)
for name, sha in plan['files'].items():
same(prepared / name, sha)
admin = pwd.getpwuid(plan['admin']['uid'])
service = pwd.getpwnam('archipelago')
if service.pw_uid != plan['admin']['service_uid'] or admin.pw_uid == service.pw_uid:
raise ValueError('administrator/service principal changed')
package = Path(approval['package_root'])
if not package.is_absolute() or package_inventory(package) != approval['package_files']:
raise ValueError('protected package inventory differs')
OS_ROOT = declared_root(package)
policy = owner_policy(OS_ROOT)
if approval['owner_dropin_sha256'] != bootstrap.digest(policy):
raise ValueError('owner bootstrap policy pin differs')
provision = package / 'scripts/provision-d09-lifecycle.py'
if 'scripts/provision-d09-lifecycle.py' not in approval['package_files']:
raise ValueError('missing pinned provisioner')
admission_raw = same(ADMISSION, approval['admission_sha256'])
admission = bootstrap.decode(admission_raw)
if admission['release_identity'][0] != plan['candidate']['version']:
raise ValueError('candidate version differs from authenticated D09 release')
if admission['prerequisite']['backend_sha256'] != plan['current']['backend_sha256'] or admission['artifacts']['backend'] != plan['candidate']['backend_sha256']:
raise ValueError('old/new native admission differs')
same(D09 / 'incoming/new-backend', plan['candidate']['backend_sha256'])
same(package / 'image-recipe/configs/prepare-terminal-workspace.sh', admission['helper_digest'])
os_binding = admission['os_update']
if set(os_binding) != {'version', 'plan_sha256', 'manifest_sha256', 'authority_digest', 'verifier_sha256'} or os_binding['version'] != 1:
raise ValueError('OS contract binding fields')
for field in ('plan_sha256', 'manifest_sha256', 'authority_digest', 'verifier_sha256'):
bootstrap.pin(os_binding[field])
inputs = approval['os_inputs']
if set(inputs) != {'plan.json', 'manifest.json', 'preservation.json'}:
raise ValueError('OS input set')
data = {name: same(prepared / name, sha) for name, sha in inputs.items()}
if bootstrap.digest(data['plan.json']) != os_binding['plan_sha256'] or bootstrap.digest(data['manifest.json']) != os_binding['manifest_sha256']:
raise ValueError('OS plan/manifest admission differs')
os_plan = bootstrap.decode(data['plan.json'])
preservation = bootstrap.decode(data['preservation.json'])
if bootstrap.digest(data['preservation.json']) != os_plan['preservation_sha256'] or preservation.get('authority_digest') != os_binding['authority_digest']:
raise ValueError('OS preservation authority differs')
verifier = same(VERIFIER, os_binding['verifier_sha256'])
if not verifier.startswith(b'\x7fELF'):
raise ValueError('native release verifier required')
custody = OS_ROOT / 'verifier-custody.json'
if custody.exists() or custody.is_symlink():
if bootstrap.decode(bootstrap.read(custody)) != dict(version=1,
verifier_sha256=os_binding['verifier_sha256'],
prerequisite_backend_sha256=os_binding['verifier_sha256']):
raise ValueError('existing release verifier custody differs')
elif os_binding['verifier_sha256'] != plan['current']['backend_sha256']:
raise ValueError('initial verifier must be the appointed old native artifact')
# Run the pinned production verifier before installing any owner code.
command(str(VERIFIER), 'ceremony', 'verify', str(prepared / 'manifest.json'))
current_exclusion()
destination_custody(OS_ROOT)
destination_custody(OWNER_DROPIN.parent)
# An existing plan directory, even identical, is an external reconciliation
# case. No overwrite permits stale one-use authority to be resurrected.
destination = OS_ROOT / 'plans' / os_binding['plan_sha256']
if destination.exists() or destination.is_symlink():
raise ValueError('existing protected OS plan')
if OWNER_DROPIN.exists() or OWNER_DROPIN.is_symlink():
raise ValueError('existing bootstrap owner policy')
intent = BASE / ('activation-' + expected)
intent.mkdir(mode=0o700)
bootstrap.sync(BASE)
bootstrap.write(intent / 'approval.json', approval_raw)
bootstrap.write(intent / 'INTENT', b'one-shot owner activation; no automatic retry\n')
bootstrap.sync(intent)
ensure_dir(OS_ROOT / 'plans')
ensure_dir(OS_ROOT / 'transactions')
# Same executor lock as OS stage; no D09 authority exists yet.
executor_lock = os.open(OS_ROOT / 'executor.lock', os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
try:
fd = bootstrap.secure(OS_ROOT / 'executor.lock'); os.close(fd)
fcntl.flock(executor_lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
untouched_authority()
staging = OS_ROOT / 'plans' / ('.bootstrap-' + expected)
staging.mkdir(mode=0o700)
for name, raw in data.items():
bootstrap.write(staging / name, raw)
bootstrap.sync(staging)
os.rename(staging, destination)
bootstrap.sync(destination.parent)
# Actual D09 provisioner consumes the same fixed incoming contract.
incoming = D09 / 'incoming/os-update'
if incoming.exists() or incoming.is_symlink():
raise ValueError('existing OS incoming activation requires reconciliation')
incoming.mkdir(mode=0o700)
for name, raw in data.items():
bootstrap.write(incoming / name, raw)
bootstrap.write(incoming / 'verifier', same(VERIFIER, os_binding['verifier_sha256']))
bootstrap.sync(incoming)
bootstrap.sync(incoming.parent)
python = str(Path(sys.executable).resolve())
command(python, '-I', '-B', str(provision), '--install-package')
# Bootstrap-owned additive drop-in: permit actual lock probing and
# owner postboot status only, not writable OS packages or plan inputs.
ensure_dir(OWNER_DROPIN.parent)
bootstrap.write(OWNER_DROPIN, policy)
bootstrap.sync(OWNER_DROPIN.parent)
command('/usr/bin/systemctl', 'daemon-reload')
# Recheck source and admission after installation, before granting.
if package_inventory(package) != approval['package_files']:
raise ValueError('package changed during installation')
same(ADMISSION, approval['admission_sha256'])
command(python, '-I', '-B', str(provision), '--admission', str(ADMISSION))
installed = bootstrap.decode(bootstrap.read(Path('/etc/archipelago-d09/binding.json')))
if installed.get('os_update') != os_binding:
raise ValueError('provisioner did not bind actual OS owner')
bootstrap.write(intent / 'PROVISIONED', bootstrap.read(D09 / 'ledger.json'))
finally:
os.close(executor_lock)
command('/usr/bin/systemctl', 'start', 'archipelago-d09-lifecycle.service')
# Wait for socket creation only. Never retry grant-A submission after an
# uncertain reply; the owner retains its consumed authority.
deadline = time.monotonic() + 30
while not Path('/run/archipelago-d09/owner.sock').exists():
if time.monotonic() > deadline:
raise TimeoutError('owner socket not ready')
time.sleep(.1)
started = bootstrap.decode(command(python, '-I', '-B', str(provision), '--activate-prerequisite'))
if started == {'prerequisite_started': False, 'invocation_id': None}:
# The owner submits a no-block systemd start and independently polls
# it. Observe its protected record; never submit grant A a second time.
first = bootstrap.decode(bootstrap.read(D09 / 'ledger.json'))['prerequisite']
deadline = time.monotonic() + 30
while time.monotonic() < deadline:
prior = bootstrap.decode(bootstrap.read(D09 / 'ledger.json'))['prerequisite']
if any(prior[k] != first[k] for k in ('owner_incarnation', 'boot_id', 'grant_digest')):
raise ValueError('prerequisite observation continuity changed')
if prior['state'] == 'STARTED':
started = {'prerequisite_started': True, 'invocation_id': prior['observation']['invocation_id']}
break
if prior['state'] != 'START_INTENT':
raise ValueError('prerequisite start outcome requires reconciliation')
time.sleep(.1)
if started.get('prerequisite_started') is not True or not started.get('invocation_id'):
raise ValueError('owner did not confirm prerequisite activation')
result = {'version': 1, 'state': 'D09_PREREQUISITE_ACTIVATED',
'activation_sha256': expected, 'os_update': os_binding,
'invocation_id': started['invocation_id'],
'os_transaction_applied': False}
bootstrap.write(intent / 'result.json', json.dumps(result, sort_keys=True).encode())
bootstrap.sync(intent)
return result
finally:
os.close(lock)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--approval', type=Path, required=True)
parser.add_argument('--approval-sha256', required=True)
args = parser.parse_args()
print(json.dumps(activate(args.approval, args.approval_sha256), sort_keys=True))