fix(security): constrain SOPS writes and custody argument handoff
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a1346b-72a4-7d40-b8bd-8b8ca346cfa2
This commit is contained in:
parent
b01a30bd49
commit
8f808577d6
8 changed files with 573 additions and 44 deletions
|
|
@ -43,18 +43,11 @@ def unique_object(pairs):
|
|||
return result
|
||||
|
||||
|
||||
def age_document(source, recipient):
|
||||
# Inspect ciphertext metadata before giving a credential to any subprocess.
|
||||
# This repository's admitted SOPS profile is age-only, not arbitrary cloud
|
||||
# KMS, key services, SSH identities or executable age plugins.
|
||||
with source.open("rb") as encrypted:
|
||||
raw = encrypted.read(8 * 1024 * 1024 + 1)
|
||||
if len(raw) > 8 * 1024 * 1024:
|
||||
raise Refused('encrypted input is too large')
|
||||
if source.suffix == '.json':
|
||||
def parse_document(raw, suffix):
|
||||
if suffix == '.json':
|
||||
data = json.loads(raw, object_pairs_hook=unique_object)
|
||||
kind = 'json'
|
||||
elif source.suffix in {'.yaml', '.yml'}:
|
||||
elif suffix in {'.yaml', '.yml'}:
|
||||
import yaml
|
||||
class UniqueLoader(yaml.SafeLoader):
|
||||
def construct_mapping(self, node, deep=False):
|
||||
|
|
@ -69,6 +62,19 @@ def age_document(source, recipient):
|
|||
kind = 'yaml'
|
||||
else:
|
||||
raise Refused('age recovery supports SOPS JSON/YAML input')
|
||||
return data, kind
|
||||
|
||||
|
||||
def age_document(source, recipient, *, raw=None):
|
||||
# Inspect ciphertext metadata before giving a credential to any subprocess.
|
||||
# This repository's admitted SOPS profile is age-only, not arbitrary cloud
|
||||
# KMS, key services, SSH identities or executable age plugins.
|
||||
if raw is None:
|
||||
with source.open("rb") as encrypted:
|
||||
raw = encrypted.read(8 * 1024 * 1024 + 1)
|
||||
if len(raw) > 8 * 1024 * 1024:
|
||||
raise Refused('encrypted input is too large')
|
||||
data, kind = parse_document(raw, source.suffix)
|
||||
metadata = data.get('sops') if isinstance(data, dict) else None
|
||||
if not isinstance(metadata, dict):
|
||||
raise Refused('SOPS metadata is missing')
|
||||
|
|
@ -96,9 +102,12 @@ def private_key_fd():
|
|||
name = os.environ.get('NETKINGDOM_CUSTODY_AGE_KEY_FILE', '')
|
||||
if not name:
|
||||
raise Refused('explicit temporary custody key required')
|
||||
fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW)
|
||||
path = Path(os.path.abspath(name))
|
||||
if any(p.is_symlink() for p in (path, *path.parents)):
|
||||
raise Refused('custody key path must not contain symlinks')
|
||||
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
||||
info = os.fstat(fd)
|
||||
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077 or info.st_size > 16384:
|
||||
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077 or info.st_nlink != 1 or info.st_size > 16384:
|
||||
os.close(fd)
|
||||
raise Refused('custody key must be an owned private regular file')
|
||||
return fd
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue