Publish Alpine musl distroless Postfix as the sole latest image
Build and Push Docker Image / build (push) Successful in 1m25s
Build and Push Docker Image / build (push) Successful in 1m25s
This commit is contained in:
1 parent
2a9a7c6baa
commit
7ead00647b
12 files changed
+776
-1274
No files matched your search
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Assemble an Alpine-derived, shell-free runtime from an explicit ELF allowlist."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
copied = set()
|
||||
|
||||
def copy(source):
|
||||
source = Path(os.path.normpath(source))
|
||||
if source in copied:
|
||||
return
|
||||
copied.add(source)
|
||||
target = root / str(source).lstrip('/')
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
if source.is_symlink():
|
||||
link = os.readlink(source)
|
||||
if not target.is_symlink():
|
||||
target.symlink_to(link)
|
||||
# Preserve every hop, not just the final file in a multi-link chain.
|
||||
copy(link if os.path.isabs(link) else source.parent / link)
|
||||
elif source.is_dir():
|
||||
target.mkdir(exist_ok=True)
|
||||
for child in source.iterdir():
|
||||
copy(child)
|
||||
else:
|
||||
shutil.copy2(source, target)
|
||||
|
||||
for executable in sys.argv[2:]:
|
||||
result = subprocess.run(['lddtreepax', '-l', executable], check=True, text=True, capture_output=True)
|
||||
paths = result.stdout.splitlines()
|
||||
if not paths or any(not p.startswith('/') or not Path(p).exists() for p in paths):
|
||||
raise SystemExit(f'Unresolved runtime dependency for {executable}: {result.stdout}')
|
||||
copy(executable)
|
||||
for path in paths:
|
||||
copy(path)
|
||||
|
||||
# musl uses this file for nonstandard library directories (for example Lua).
|
||||
for search_path in Path('/etc').glob('ld-musl-*.path'):
|
||||
copy(search_path)
|
||||
|
||||
# Postfix loads database maps and SASL mechanisms dynamically.
|
||||
for folder in ['/usr/lib/postfix', '/usr/lib/sasl2']:
|
||||
for plugin in Path(folder).glob('*.so*'):
|
||||
result = subprocess.run(['lddtreepax', '-l', str(plugin)], check=True,
|
||||
text=True, capture_output=True,
|
||||
env={**os.environ, 'LD_LIBRARY_PATH': '/lib:/usr/lib:/usr/lib/postfix'})
|
||||
for dependency in result.stdout.splitlines():
|
||||
if not dependency.startswith('/') or not Path(dependency).exists():
|
||||
raise SystemExit(f'Unresolved plugin dependency: {dependency}')
|
||||
copy(dependency)
|
||||
copy(plugin)
|
||||
for data in ['/etc/postfix', '/etc/passwd', '/etc/group', '/etc/aliases',
|
||||
'/etc/aliases.lmdb', '/etc/ssl/certs/ca-certificates.crt', '/etc/ssl/cert.pem',
|
||||
'/etc/ssl/openssl.cnf', '/usr/lib/ossl-modules', '/usr/share/zoneinfo',
|
||||
'/usr/share/icu', '/usr/lib/icu', '/etc/services', '/etc/protocols', '/etc/alpine-release', '/etc/os-release']:
|
||||
if Path(data).exists():
|
||||
copy(data)
|
||||
for directory in ['tmp', 'root', 'dev', 'var/mail', 'var/lib/postfix', 'var/spool/postfix']:
|
||||
(root / directory).mkdir(parents=True, exist_ok=True)
|
||||
os.chmod(root / 'tmp', 0o1777)
|
||||
# Preserve the Postfix command group and setgid bits for queue submission.
|
||||
for command in ['postdrop', 'postqueue', 'postlog']:
|
||||
source = Path('/usr/sbin') / command
|
||||
target = root / str(source).lstrip('/')
|
||||
stat = source.stat()
|
||||
os.chown(target, stat.st_uid, stat.st_gid)
|
||||
os.chmod(target, stat.st_mode & 0o7777)
|
||||
for forbidden in ['bin/sh', 'bin/bash', 'bin/busybox', 'sbin/apk', 'usr/bin/apk',
|
||||
'usr/bin/python3', 'usr/bin/go', 'usr/sbin/postfix']:
|
||||
assert not (root / forbidden).exists(), f'Unexpected runtime tool: {forbidden}'
|
||||
Reference in new issue
Block a user