fix: remove builder proxy declarations from exported package environments

This commit is contained in:
Codex
2026-10-13 04:51:58 +03:00
parent 72aa5cebfb
commit 36a91a2451
2 changed files with 69 additions and 0 deletions
+38
View File
@@ -1,5 +1,8 @@
"""Prepare only exported Gentoo payloads and configure the dedicated guest network.""" """Prepare only exported Gentoo payloads and configure the dedicated guest network."""
import argparse import argparse
import bz2
import io
import runpy
import hashlib import hashlib
import ipaddress import ipaddress
import json import json
@@ -50,6 +53,39 @@ def write(root, relative, content, mode=0o644):
return path return path
def sanitize_saved_environments(root):
"""Use Portage's parser to scrub exported declarations without evaluating them."""
database = root_path(root, 'var/db/pkg')
paths = sorted(database.glob('*/*/environment.bz2'))
result = {'files_scanned': len(paths), 'filtered_files': 0, 'variable_names': []}
if not paths:
return result
scripts = sorted(root_path(root, 'usr/lib/portage').glob('*/filter-bash-environment.py'))
if not scripts:
raise ImageError('Installed Portage environment filter is required for export')
script = root_path(root, scripts[0].relative_to(root))
api = runpy.run_path(str(script))
pattern = re.compile(rb'(?i:(?:HTTP|HTTPS|FTP|ALL|NO)_PROXY|.*(?:PASSWORD|PASSWD|SECRET|TOKEN|API_KEY|ACCESS_KEY|CREDENTIAL|AUTHORIZATION).*)')
declarations = re.compile(rb'^(?:declare|export|readonly)\s+(?:-[^ ]+\s+)*([A-Za-z_][A-Za-z0-9_]*)=', re.M)
names = set()
for path in paths:
path = root_path(root, path.relative_to(root))
try:
original = bz2.decompress(path.read_bytes())
except (OSError, ValueError) as error:
raise ImageError('Cannot read an exported package environment') from error
filtered = io.BytesIO()
api['filter_bash_environment'](pattern, io.BytesIO(original), filtered)
clean = filtered.getvalue()
if clean != original:
names.update(name.decode('ascii') for name in declarations.findall(original)
if pattern.fullmatch(name))
path.write_bytes(bz2.compress(clean))
result['filtered_files'] += 1
result['variable_names'] = sorted(names)
return result
def sanitize_rootfs(root): def sanitize_rootfs(root):
"""Erase builder identity/secrets in the exported copy, retaining Gentoo updates.""" """Erase builder identity/secrets in the exported copy, retaining Gentoo updates."""
for relative in ('etc/portage/gnupg', 'etc/portage/gnupg.hydra-untrusted', for relative in ('etc/portage/gnupg', 'etc/portage/gnupg.hydra-untrusted',
@@ -76,6 +112,8 @@ def sanitize_rootfs(root):
write(root, 'etc/hosts', '127.0.0.1 localhost\n::1 localhost\n127.0.1.1 hydra-os\n') write(root, 'etc/hosts', '127.0.0.1 localhost\n::1 localhost\n127.0.1.1 hydra-os\n')
remove(root, 'etc/resolv.conf') remove(root, 'etc/resolv.conf')
root_path(root, 'etc/resolv.conf', follow_leaf=False).symlink_to('/run/NetworkManager/resolv.conf') root_path(root, 'etc/resolv.conf', follow_leaf=False).symlink_to('/run/NetworkManager/resolv.conf')
audit = sanitize_saved_environments(root)
write(root, 'usr/share/hydra/environment-sanitization.json', json.dumps(audit, indent=2) + '\n')
def copy_overlay(root, overlay): def copy_overlay(root, overlay):
+31
View File
@@ -1,4 +1,5 @@
import importlib.util import importlib.util
import bz2
import json import json
import os import os
import shlex import shlex
@@ -73,6 +74,36 @@ class ImageTests(unittest.TestCase):
self.assertEqual((self.root / 'etc/hostname').read_text(), 'hydra-os\n') self.assertEqual((self.root / 'etc/hostname').read_text(), 'hydra-os\n')
self.assertTrue((self.root / 'etc/resolv.conf').is_symlink()) self.assertTrue((self.root / 'etc/resolv.conf').is_symlink())
def test_export_filters_saved_environment_proxies_but_retains_package_metadata(self):
self.assertTrue(callable(getattr(image, 'sanitize_saved_environments', None)), 'saved environment scrubber is missing')
# Portable stand-in for the external Portage filter API, not the scrubber.
helper = self.file('usr/lib/portage/python3.14/filter-bash-environment.py',
"import re\ndef filter_bash_environment(pattern, source, output):\n"
" for line in source:\n"
" match = re.match(rb'declare -x ([A-Za-z_][A-Za-z0-9_]*)=', line)\n"
" if not match or not pattern.fullmatch(match.group(1)):\n"
" output.write(line)\n")
path = self.root / 'var/db/pkg/app-emulation/qemu-9.2.4/environment.bz2'
path.parent.mkdir(parents=True)
package_metadata = b'declare -x LICENSE="GPL-2"\ndeclare -x EAPI="8"\n'
path.write_bytes(bz2.compress(package_metadata + b'declare -x HTTPS_PROXY="https://user:secret@builder.invalid"\n'
b'declare -x HTTP_PROXY="http://127.0.0.1:1234"\n'
b'declare -x NO_PROXY="localhost"\n'))
result = image.sanitize_saved_environments(self.root)
self.assertEqual(bz2.decompress(path.read_bytes()), package_metadata)
self.assertEqual(result['filtered_files'], 1)
self.assertEqual(result['variable_names'], ['HTTPS_PROXY', 'HTTP_PROXY', 'NO_PROXY'])
def test_saved_environment_filter_refuses_missing_portage_helper(self):
self.assertTrue(callable(getattr(image, 'sanitize_saved_environments', None)), 'saved environment scrubber is missing')
path = self.root / 'var/db/pkg/app-emulation/qemu-9.2.4/environment.bz2'
path.parent.mkdir(parents=True)
original = bz2.compress(b'declare -x HTTPS_PROXY="private"\n')
path.write_bytes(original)
with self.assertRaises(image.ImageError):
image.sanitize_saved_environments(self.root)
self.assertEqual(path.read_bytes(), original)
def test_sanitization_cannot_follow_exported_symlink_to_host(self): def test_sanitization_cannot_follow_exported_symlink_to_host(self):
outside = Path(self.tmp.name) / 'host' outside = Path(self.tmp.name) / 'host'
outside.mkdir() outside.mkdir()