Files
truf-server/tests/test_container_security.py
2026-09-30 20:30:56 +03:00

561 lines
32 KiB
Python

import copy
import hashlib
import ntpath
import os
from pathlib import Path, PurePosixPath
import posixpath
import stat
import sys
import tempfile
from types import SimpleNamespace
import unittest
from unittest import mock
APP_DIR = Path(__file__).resolve().parents[1] / 'app'
sys.path.insert(0, str(APP_DIR))
import lifecycle_authority as authority
import runtime_security as security
def platform_os(name, path=None):
# Do not mutate the shared os.name used by pathlib and the test runner.
result = SimpleNamespace(**vars(os))
result.name = name
result.path = path or os.path
return result
class NativeExecutablePolicyTests(unittest.TestCase):
def setUp(self):
self.executable = '/usr/bin/git'
self.entries = {
path: SimpleNamespace(st_uid=0, st_gid=0, st_mode=kind | 0o755)
for path, kind in (
('/', stat.S_IFDIR), ('/usr', stat.S_IFDIR),
('/usr/bin', stat.S_IFDIR), (self.executable, stat.S_IFREG),
)
}
self.writable = set()
self.noexec = set()
self.os = platform_os('posix', posixpath)
self.os.sep = '/'
self.os.geteuid = lambda: 10001
self.os.lstat = mock.Mock(side_effect=self.lstat)
self.os.stat = mock.Mock(side_effect=self.lstat)
self.os.access = mock.Mock(side_effect=self.access)
for name in ('chmod', 'chown', 'makedirs'):
setattr(self.os, name, mock.Mock(side_effect=AssertionError('unexpected filesystem mutation')))
self.enterContext(mock.patch.object(security, 'os', self.os))
self.enterContext(mock.patch.object(security, 'canonical_path', side_effect=posixpath.normpath))
def lstat(self, path, **kwargs):
try:
return self.entries[posixpath.normpath(path)]
except KeyError:
raise FileNotFoundError(path) from None
def access(self, path, mode, *, effective_ids=False):
self.assertTrue(effective_ids)
details = self.lstat(path)
shift = 6 if details.st_uid == 10001 else 3 if details.st_gid == 10001 else 0
permissions = (details.st_mode >> shift) & 0o7
if mode == os.W_OK:
return path in self.writable or bool(permissions & 0o2)
self.assertEqual(mode, os.X_OK)
return path not in self.noexec and bool(permissions & 0o1)
def test_root_owned_executable_and_traversable_parent_chain_are_accepted_without_repairs(self):
for mode, gid in ((0o755, 0), (0o555, 0), (0o750, 10001)):
with self.subTest(mode=oct(mode), gid=gid):
self.entries[self.executable].st_mode = stat.S_IFREG | mode
self.entries[self.executable].st_gid = gid
self.assertEqual(
security.require_trusted_native_executable(PurePosixPath(self.executable)),
self.executable,
)
self.assertEqual(security.require_trusted_native_executable('/usr/bin/./git'), self.executable)
self.assertIn(mock.call('/'), self.os.lstat.call_args_list)
self.os.chmod.assert_not_called()
self.os.chown.assert_not_called()
self.os.makedirs.assert_not_called()
def test_relative_empty_and_invalid_paths_fail_closed(self):
for path in ('git', './git', '', None, '/usr/bin/\x00git'):
with self.subTest(path=path), self.assertRaises(security.PrivateFileError):
security.require_trusted_native_executable(path)
self.os.lstat.assert_not_called()
def test_nonregular_or_untrusted_executables_are_rejected(self):
cases = [
(stat.S_IFREG, 0o755, 10001, 'root-owned'),
(stat.S_IFREG, 0o755, 10002, 'root-owned'),
(stat.S_IFLNK, 0o755, 0, 'link'),
]
cases.extend((kind, 0o755, 0, 'regular file') for kind in (
stat.S_IFDIR, stat.S_IFIFO, stat.S_IFSOCK, stat.S_IFCHR, stat.S_IFBLK,
))
cases.extend((stat.S_IFREG, mode, 0, 'unsafe permissions') for mode in (
0o775, 0o757, 0o777, 0o4755, 0o2755, 0o6755,
))
for kind, mode, uid, error in cases:
details = SimpleNamespace(st_mode=kind | mode, st_uid=uid, st_gid=0)
with self.subTest(kind=kind, mode=oct(mode), uid=uid), \
mock.patch.dict(self.entries, {self.executable: details}), \
self.assertRaisesRegex(security.PrivateFileError, error):
security.require_trusted_native_executable(self.executable)
def test_every_parent_including_filesystem_root_must_be_trusted(self):
for path in ('/', '/usr', '/usr/bin'):
for mode, uid in ((0o775, 0), (0o777, 0), (0o1777, 0), (0o755, 10001), (0o755, 10002)):
details = SimpleNamespace(st_mode=stat.S_IFDIR | mode, st_uid=uid, st_gid=0)
with self.subTest(path=path, mode=oct(mode), uid=uid), \
mock.patch.dict(self.entries, {path: details}), \
self.assertRaises(security.PrivateFileError):
security.require_trusted_native_executable(self.executable)
with mock.patch.dict(self.entries, {'/usr': SimpleNamespace(st_mode=stat.S_IFREG | 0o755)}), \
self.assertRaisesRegex(security.PrivateFileError, 'parent is not a directory'):
security.require_trusted_native_executable(self.executable)
def test_parent_links_cannot_be_hidden_by_canonicalization(self):
for path, executable in (
('/usr', self.executable), ('/usr/bin', self.executable),
('/usr/linked', '/usr/linked/../bin/git'),
):
details = SimpleNamespace(st_mode=stat.S_IFLNK | 0o755, st_uid=0, st_gid=0)
with self.subTest(path=path), mock.patch.dict(self.entries, {path: details}), \
self.assertRaisesRegex(security.PrivateFileError, 'link'):
security.require_trusted_native_executable(executable)
def test_execute_access_is_for_runtime_credentials_not_root_or_any_execute_bit(self):
for mode in (0o644, 0o700, 0o750):
with self.subTest(mode=oct(mode)):
self.entries[self.executable].st_mode = stat.S_IFREG | mode
with self.assertRaisesRegex(security.PrivateFileError, 'not executable/searchable'):
security.require_trusted_native_executable(self.executable)
def test_effective_access_checks_reject_writes_and_noexec_even_with_safe_modes(self):
for paths, error in ((self.writable, 'writable'), (self.noexec, 'not executable/searchable')):
for path in self.entries:
with self.subTest(path=path, error=error):
paths.add(path)
try:
with self.assertRaisesRegex(security.PrivateFileError, error):
security.require_trusted_native_executable(self.executable)
finally:
paths.remove(path)
def test_missing_inaccessible_and_unsupported_inspections_fail_closed(self):
for error in (FileNotFoundError('missing'), PermissionError('denied'), ValueError('invalid')):
with self.subTest(error=type(error).__name__), \
mock.patch.object(self.os, 'lstat', side_effect=error), \
self.assertRaises(security.PrivateFileError):
security.require_trusted_native_executable(self.executable)
with mock.patch.object(self.os, 'access', side_effect=NotImplementedError('effective IDs')), \
self.assertRaises(security.PrivateFileError):
security.require_trusted_native_executable(self.executable)
def test_private_file_and_directory_policy_remains_owner_only(self):
with mock.patch.object(security, 'reject_reparse_components', side_effect=lambda path: path):
for mode, uid, expected in ((0o400, 10001, True), (0o600, 10001, True),
(0o644, 10001, False), (0o755, 0, False), (0o600, 0, False)):
self.entries[self.executable] = SimpleNamespace(st_mode=stat.S_IFREG | mode, st_uid=uid)
with self.subTest(mode=oct(mode), uid=uid):
self.assertEqual(security.private_file_ready(self.executable), expected)
for mode, uid, expected in ((0o700, 10001, True), (0o755, 10001, False), (0o700, 0, False)):
self.entries['/usr/bin'] = SimpleNamespace(st_mode=stat.S_IFDIR | mode, st_uid=uid)
with self.subTest(directory_mode=oct(mode), uid=uid):
self.assertEqual(security.private_directory_ready('/usr/bin'), expected)
def test_posix_endpoint_lock_root_is_fixed_across_configs_and_environment(self):
identity = 'fixture-endpoint'
expected = '/run/truf/authority/endpoint-' + hashlib.sha256(identity.encode()).hexdigest() + '.lock'
with mock.patch.dict(os.environ, {
'TRUF_AUTHORITY_LOCK_ROOT': '/tmp/alternate', 'TRUF_AUTHORITY_DIR': '/tmp/other',
}):
self.assertEqual(security._cluster_endpoint_lock_root(), '/run/truf/authority')
for root in ('/opt/truf', '/tmp/alternate'):
config = {'global': {'root_dir': root, 'runtime_dir': root + '/runtime', 'authority_dir': root}}
self.assertEqual(security.cluster_endpoint_authority_lock_path(config, identity), expected)
self.assertNotEqual(security.cluster_endpoint_authority_lock_path(endpoint_identity='other'), expected)
class WindowsNativePolicyTests(unittest.TestCase):
def test_native_helper_delegates_to_existing_private_file_policy(self):
path = r'C:\private\git.exe'
with mock.patch.object(security, 'os', platform_os('nt', ntpath)), \
mock.patch.object(security, 'require_private_file', return_value=path) as private, \
mock.patch.object(security, 'canonical_path', return_value=path) as canonical:
self.assertEqual(security.require_trusted_native_executable(path), path)
private.assert_called_once_with(path)
canonical.assert_called_once_with(path)
private.side_effect = security.PrivateFileError('not private')
with self.assertRaisesRegex(security.PrivateFileError, 'not private'):
security.require_trusted_native_executable(path)
for error in (PermissionError('denied'), TypeError('invalid path'), ValueError('invalid path')):
private.side_effect = error
with self.subTest(error=type(error).__name__), self.assertRaises(security.PrivateFileError):
security.require_trusted_native_executable(path)
def test_windows_endpoint_lock_root_is_unchanged(self):
with mock.patch.object(security, 'os', platform_os('nt', ntpath)), \
mock.patch.object(security, '_windows_common_appdata', return_value=r'C:\ProgramData'):
self.assertEqual(security._cluster_endpoint_lock_root(), r'C:\ProgramData\Truf\authority')
class ContainerLifecyclePolicyTests(unittest.TestCase):
def setUp(self):
temporary = self.enterContext(tempfile.TemporaryDirectory())
self.root = Path(temporary)
self.app = self.root / 'app'
self.app.mkdir()
self.enterContext(mock.patch.object(authority, 'CODE_AUTHORITY_FILES', ('supervisor.py', 'runtime_security.py')))
for name in (*authority.CODE_AUTHORITY_FILES, *authority.EXTERNAL_CODE_AUTHORITY_FILES):
path = self.app / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text('# inert fixture\n', encoding='ascii')
self.git = self.root / 'git.exe'
self.trufflehog = self.root / 'trufflehog.exe'
self.policy = self.app / 'policy.yaml'
self.config_path = self.app / 'config.yaml'
self.secrets = self.app / 'secrets.yaml'
for path in (self.git, self.trufflehog, self.policy, self.config_path, self.secrets):
path.write_bytes(b'inert fixture\n')
self.manifest = authority.build_code_manifest(self.app, self.trufflehog, [self.policy], git_path=self.git)
self.native = {name: entry['path'] for name, entry in self.manifest['executables'].items()}
self.private = {entry['path'] for entry in self.manifest['files'].values()} | {security.canonical_path(self.policy)}
self.config = {'global': {
'root_dir': str(self.root), 'project_dir': str(self.app),
'trufflehog_path': str(self.trufflehog), 'trufflehog_config': str(self.policy),
'secrets_file': str(self.secrets),
}}
def test_external_authority_files_are_windows_only(self):
expected = (
'../runtime/check-openrouter-keys.ps1', '../start_core_runtime.ps1',
'../start_runtime.ps1', '../stop_runtime.ps1',
) if os.name == 'nt' else ()
self.assertEqual(authority.EXTERNAL_CODE_AUTHORITY_FILES, expected)
self.assertEqual(authority.verify_code_manifest(self.manifest), self.manifest)
def test_project_private_git_preference_is_windows_only(self):
private_git = self.root / 'runtime' / 'git' / 'cmd' / 'git.exe'
private_git.parent.mkdir(parents=True, exist_ok=True)
private_git.write_bytes(b'private Git fixture')
for platform in ('nt', 'posix'):
with self.subTest(platform=platform), \
mock.patch.object(authority, 'os', platform_os(platform)), \
mock.patch.object(authority.shutil, 'which', return_value=str(self.git)) as which:
resolved = authority.resolve_manifest_executable(None, name='git', app_dir=self.app)
self.assertEqual(resolved, security.canonical_path(private_git if platform == 'nt' else self.git))
if platform == 'nt':
which.assert_not_called()
else:
which.assert_called_once_with('git')
self.assertEqual(authority.resolve_manifest_executable(self.git, name='git'), self.native['git'])
def test_posix_resolution_rejects_inexact_paths_and_links_before_canonicalization(self):
with mock.patch.object(authority, 'os', platform_os('posix', posixpath)):
for path in ('./git', '/usr/bin/../bin/git'):
with self.subTest(path=path), self.assertRaisesRegex(authority.LifecycleAuthorityError, 'exact and absolute'):
authority.resolve_manifest_executable(path, name='git')
with mock.patch.object(authority, 'reject_reparse_components', side_effect=security.PrivateFileError('link')), \
mock.patch.object(authority, 'canonical_path') as canonical, \
self.assertRaisesRegex(authority.LifecycleAuthorityError, 'unsafe'):
authority.resolve_manifest_executable('/usr/bin/git', name='git')
canonical.assert_not_called()
def test_posix_manifest_splits_native_and_private_policies(self):
with mock.patch.object(authority, 'os', platform_os('posix')), \
mock.patch.object(authority, 'private_file_ready', side_effect=lambda path: path in self.private) as private, \
mock.patch.object(authority, 'require_trusted_native_executable', side_effect=lambda path: path) as native:
self.assertEqual(authority.verify_code_manifest(self.manifest, require_private_acl=True), self.manifest)
self.assertEqual({call.args[0] for call in private.call_args_list}, self.private)
self.assertEqual({call.args[0] for call in native.call_args_list}, set(self.native.values()))
def test_windows_manifest_still_requires_private_native_files(self):
with mock.patch.object(authority, 'os', platform_os('nt')), \
mock.patch.object(authority, 'private_file_ready', return_value=True) as private, \
mock.patch.object(authority, 'require_trusted_native_executable') as native:
authority.verify_code_manifest(self.manifest, require_private_acl=True)
self.assertEqual({call.args[0] for call in private.call_args_list}, self.private | set(self.native.values()))
native.assert_not_called()
private.side_effect = lambda path: path != self.native['git']
with self.assertRaisesRegex(authority.LifecycleAuthorityError, 'exact-private: executable:git'):
authority.verify_code_manifest(self.manifest, require_private_acl=True)
def test_native_policy_failures_are_rejected_before_hashing(self):
for name, path in self.native.items():
def check(value):
if value == path:
raise security.PrivateFileError('unsafe fixture')
return value
with self.subTest(name=name), mock.patch.object(authority, 'os', platform_os('posix')), \
mock.patch.object(authority, 'private_file_ready', return_value=True), \
mock.patch.object(authority, 'require_trusted_native_executable', side_effect=check), \
mock.patch.object(authority, 'sha256_file', wraps=security.sha256_file) as hashed:
with self.assertRaisesRegex(authority.LifecycleAuthorityError, 'executable:' + name):
authority.verify_code_manifest(self.manifest, require_private_acl=True)
self.assertNotIn(mock.call(path), hashed.call_args_list)
def test_code_and_policy_do_not_inherit_native_exemptions(self):
for path in (security.canonical_path(self.app / 'supervisor.py'), security.canonical_path(self.policy)):
with self.subTest(path=path), mock.patch.object(authority, 'os', platform_os('posix')), \
mock.patch.object(authority, 'private_file_ready', side_effect=lambda value: value != path), \
mock.patch.object(authority, 'require_trusted_native_executable'), \
self.assertRaisesRegex(authority.LifecycleAuthorityError, 'exact-private'):
authority.verify_code_manifest(self.manifest, require_private_acl=True)
manifest = copy.deepcopy(self.manifest)
manifest['assets'][self.native['git']] = manifest['executables']['git'].copy()
with mock.patch.object(authority, 'os', platform_os('posix')), \
mock.patch.object(authority, 'private_file_ready', side_effect=lambda path: path in self.private), \
mock.patch.object(authority, 'require_trusted_native_executable'), \
self.assertRaisesRegex(authority.LifecycleAuthorityError, 'exact-private: asset:'):
authority.verify_code_manifest(manifest, require_private_acl=True)
def test_supported_native_identity_sets_are_exact_and_validated(self):
self.assertEqual(set(self.manifest['executables']), {'git', 'trufflehog'})
server_manifest = authority.build_code_manifest(
self.app, policy_paths=[self.policy], git_path=self.git,
include_trufflehog=False,
)
self.assertEqual(set(server_manifest['executables']), {'git'})
self.assertEqual(authority.verify_code_manifest(server_manifest), server_manifest)
without_trufflehog = copy.deepcopy(self.manifest)
del without_trufflehog['executables']['trufflehog']
self.assertEqual(authority.verify_code_manifest(without_trufflehog), without_trufflehog)
for label, original in (('full', self.manifest), ('server', server_manifest)):
for name in original['executables']:
for change in ('relative', 'digest', 'entry'):
manifest = copy.deepcopy(original)
if change == 'entry':
manifest['executables'][name] = None
else:
field, value = ('path', 'relative') if change == 'relative' else ('sha256', 'z' * 64)
manifest['executables'][name][field] = value
with self.subTest(label=label, name=name, change=change), \
self.assertRaises(authority.LifecycleAuthorityError):
authority.verify_code_manifest(manifest)
extra = copy.deepcopy(original)
extra['executables']['shell'] = original['executables']['git'].copy()
with self.subTest(label=label, change='extra'), \
self.assertRaises(authority.LifecycleAuthorityError):
authority.verify_code_manifest(extra)
missing_git = copy.deepcopy(original)
del missing_git['executables']['git']
with self.subTest(label=label, change='missing-git'), \
self.assertRaises(authority.LifecycleAuthorityError):
authority.verify_code_manifest(missing_git)
def test_server_preflight_requires_git_without_trufflehog(self):
config = copy.deepcopy(self.config)
config['global'].pop('trufflehog_path')
config['global'].pop('trufflehog_config')
with mock.patch.object(security, 'os', platform_os('posix')), \
mock.patch.object(security, 'require_private_directory'), \
mock.patch.object(security, 'require_private_file'), \
mock.patch.object(security, 'require_trusted_native_executable'), \
mock.patch.object(
authority, 'resolve_manifest_executable',
side_effect=lambda value, **kwargs: self.native[kwargs['name']],
) as resolve:
self.assertTrue(security.preflight_lifecycle_paths(
str(self.config_path), config, authority_profile='server',
))
self.assertEqual(
[call.kwargs['name'] for call in resolve.call_args_list],
['git'],
)
def test_both_native_hashes_detect_drift_with_unchanged_size_and_mtime(self):
for name, path in self.native.items():
original = Path(path).read_bytes()
before = os.stat(path)
Path(path).write_bytes(b'I' + original[1:])
os.utime(path, ns=(before.st_atime_ns, before.st_mtime_ns))
with self.subTest(name=name), mock.patch.object(authority, 'os', platform_os('posix')), \
mock.patch.object(authority, 'private_file_ready', return_value=True), \
mock.patch.object(authority, 'require_trusted_native_executable'):
self.assertEqual(os.stat(path).st_size, before.st_size)
self.assertEqual(os.stat(path).st_mtime_ns, before.st_mtime_ns)
for private_acl in (False, True):
with self.assertRaisesRegex(authority.LifecycleAuthorityError, 'drifted: executable:' + name):
authority.verify_code_manifest(self.manifest, require_private_acl=private_acl)
Path(path).write_bytes(original)
def test_manifest_digest_binds_exact_native_paths_even_with_identical_bytes(self):
expected = authority.code_manifest_sha256(self.manifest)
alternate = self.root / 'alternate.exe'
alternate.write_bytes(self.git.read_bytes())
manifest = copy.deepcopy(self.manifest)
manifest['executables']['git']['path'] = security.canonical_path(alternate)
with self.assertRaisesRegex(authority.LifecycleAuthorityError, 'manifest digest mismatch'):
authority.verify_code_manifest(manifest, expected_sha256=expected)
with mock.patch.object(authority, 'os', platform_os('posix')), \
mock.patch.object(authority, 'canonical_path', side_effect=lambda path: (
security.canonical_path(alternate) if path == self.native['git'] else security.canonical_path(path)
)), self.assertRaisesRegex(authority.LifecycleAuthorityError, 'path is not exact'):
authority.normalize_code_manifest(self.manifest)
def test_private_inventory_excludes_native_only_when_explicitly_requested(self):
with mock.patch.object(authority, 'resolve_manifest_executable', side_effect=AssertionError('native lookup')):
paths = authority.manifest_authority_paths(
self.app, self.trufflehog, [self.policy], existing_only=True, include_executables=False,
)
self.assertEqual(set(paths), self.private)
self.assertEqual(set(authority.manifest_authority_paths(
self.app, self.trufflehog, [self.policy], existing_only=True, git_path=self.git,
)), self.private | set(self.native.values()))
def test_preflight_splits_native_from_private_code_config_policy_and_secrets(self):
for platform in ('posix', 'nt'):
def private_check(path):
if platform == 'posix' and security.canonical_path(path) in self.native.values():
raise security.PrivateFileError('native file is not private')
return path
def native_check(path):
return security.require_private_file(path) if platform == 'nt' else path
with self.subTest(platform=platform), mock.patch.object(security, 'os', platform_os(platform)), \
mock.patch.object(security, 'require_private_directory') as directory, \
mock.patch.object(security, 'require_private_file', side_effect=private_check) as private, \
mock.patch.object(security, 'require_trusted_native_executable', side_effect=native_check) as native, \
mock.patch.object(authority, 'resolve_manifest_executable', side_effect=lambda value, **kw: self.native[kw['name']]) as resolve, \
mock.patch.object(security, 'harden_private_file') as harden_file, \
mock.patch.object(security, 'harden_private_directory') as harden_directory:
self.assertTrue(security.preflight_lifecycle_paths(str(self.config_path), self.config))
expected = self.private | {security.canonical_path(self.config_path), security.canonical_path(self.secrets)}
if platform == 'nt':
expected |= set(self.native.values())
else:
directory.assert_any_call(os.path.abspath('/run/truf/authority'), create=False)
self.assertEqual({security.canonical_path(call.args[0]) for call in private.call_args_list}, expected)
self.assertEqual({call.args[0] for call in native.call_args_list}, set(self.native.values()))
self.assertEqual(resolve.call_count, 2)
harden_file.assert_not_called()
harden_directory.assert_not_called()
def test_preflight_rejects_public_config_or_policy_even_when_native_is_trusted(self):
for target in (self.config_path, self.policy):
def private_check(path):
if security.canonical_path(path) == security.canonical_path(target):
raise security.PrivateFileError('not private')
return path
with self.subTest(path=str(target)), mock.patch.object(security, 'os', platform_os('posix')), \
mock.patch.object(security, 'require_private_directory'), \
mock.patch.object(security, 'require_private_file', side_effect=private_check), \
mock.patch.object(security, 'require_trusted_native_executable') as native, \
self.assertRaisesRegex(security.PrivateFileError, 'offline hardening'):
security.preflight_lifecycle_paths(str(self.config_path), self.config)
native.assert_not_called()
def test_preflight_requires_both_native_tools_even_without_explicit_configuration(self):
config = copy.deepcopy(self.config)
config['global'].pop('trufflehog_path')
for missing in self.native:
def resolve(value, **kwargs):
if kwargs['name'] == missing:
raise authority.LifecycleAuthorityError('missing native fixture')
return self.native[kwargs['name']]
with self.subTest(missing=missing), mock.patch.object(security, 'os', platform_os('posix')), \
mock.patch.object(security, 'require_private_directory'), \
mock.patch.object(security, 'require_private_file'), \
mock.patch.object(security, 'require_trusted_native_executable'), \
mock.patch.object(authority, 'resolve_manifest_executable', side_effect=resolve), \
self.assertRaisesRegex(security.PrivateFileError, 'missing native fixture'):
security.preflight_lifecycle_paths(str(self.config_path), config)
def test_preflight_requires_prepared_private_authority_root_without_repair(self):
def require_directory(path, create=False):
self.assertFalse(create)
if path == os.path.abspath('/run/truf/authority'):
raise security.PrivateFileError('authority directory is not private')
with mock.patch.object(security, 'os', platform_os('posix')), \
mock.patch.object(security, 'require_private_directory', side_effect=require_directory), \
mock.patch.object(security, 'require_private_file'), \
mock.patch.object(security, 'harden_private_directory') as harden, \
self.assertRaisesRegex(security.PrivateFileError, 'authority directory is not private'):
security.preflight_lifecycle_paths(str(self.config_path), self.config)
harden.assert_not_called()
@unittest.skipUnless(sys.platform.startswith('linux'), 'native Linux filesystem checks')
class LinuxFilesystemSecurityTests(unittest.TestCase):
def setUp(self):
self.root = Path(self.enterContext(tempfile.TemporaryDirectory(prefix='truf-security-')))
def test_private_paths_require_runtime_ownership_and_owner_only_modes(self):
directory = self.root / 'private'
security.ensure_private_directory(directory, reject_reparse=True)
path = directory / 'secret'
path.write_bytes(b'fake secret fixture')
for mode, expected in ((0o400, True), (0o600, True), (0o644, False), (0o755, False)):
path.chmod(mode)
with self.subTest(mode=oct(mode)):
self.assertEqual(path.stat().st_uid, os.geteuid())
self.assertEqual(stat.S_IMODE(path.stat().st_mode), mode)
self.assertEqual(security.private_file_ready(path), expected)
self.assertTrue(security.private_directory_ready(directory))
directory.chmod(0o755)
with self.assertRaises(security.PrivateFileError):
security.require_private_directory(directory)
self.assertEqual(stat.S_IMODE(directory.stat().st_mode), 0o755)
def test_runtime_owned_executable_is_not_immutable_native_code(self):
if os.geteuid() == 0:
self.skipTest('requires an unprivileged runtime user')
path = self.root / 'fake-native'
path.write_bytes(b'not executed')
path.chmod(0o700)
self.assertTrue(security.private_file_ready(path))
with self.assertRaisesRegex(security.PrivateFileError, 'root-owned'):
security.require_trusted_native_executable(path)
self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o700)
def test_installed_root_owned_native_file_is_trusted_but_not_private(self):
if os.geteuid() == 0:
self.skipTest('requires an unprivileged runtime user')
path = '/usr/bin/true'
if not os.path.isfile(path):
self.skipTest('coreutils fixture is unavailable')
before = os.stat(path)
self.assertEqual(security.require_trusted_native_executable(path), security.canonical_path(path))
self.assertFalse(security.private_file_ready(path))
after = os.stat(path)
self.assertEqual((before.st_mode, before.st_uid, before.st_mtime_ns), (after.st_mode, after.st_uid, after.st_mtime_ns))
def test_native_leaf_and_parent_links_are_rejected_without_touching_targets(self):
if os.geteuid() == 0:
self.skipTest('requires an unprivileged runtime user')
target = Path('/usr/bin/true')
if not target.is_file():
self.skipTest('coreutils fixture is unavailable')
before = target.stat()
leaf = self.root / 'native-link'
leaf.symlink_to(target)
parent = self.root / 'bin'
parent.symlink_to(target.parent, target_is_directory=True)
for path in (str(leaf), str(parent / 'true'), str(parent) + '/../bin/true'):
with self.subTest(path=path), self.assertRaisesRegex(security.PrivateFileError, 'link'):
security.require_trusted_native_executable(path)
with self.assertRaises(authority.LifecycleAuthorityError):
authority.resolve_manifest_executable(path, name='git')
after = target.stat()
self.assertEqual((before.st_mode, before.st_uid, before.st_mtime_ns), (after.st_mode, after.st_uid, after.st_mtime_ns))
if __name__ == '__main__':
unittest.main()