Secure¶
Public helpers in kstlib.secure: filesystem guardrails (GuardPolicy presets and the PathGuardrails
utility so mail templates, attachments, and other file-based assets stay confined to trusted directories) and
password hashing (hash_password, verify_password, needs_rehash) backed by Argon2id.
Tip
See Secure for the feature guide (filesystem guardrails and password hashing).
Quick overview¶
STRICT_POLICYenforces root auto-creation, forbids external paths, and validates POSIX permissions (≤0o700).RELAXED_POLICYstill pins everything to the root but skips permission enforcement so it fits sandboxed/Windows-friendly scenarios.PathGuardrailsresolves relative or absolute inputs, normalises them viaPath.resolve(), and raisesPathSecurityErrorwhenever a path escapes the root, points to the wrong type, or resides on a different drive (Windows).relax()clones an existing guardrail with a modifiedallow_externalflag, which helps when you need to temporarily opt into external paths for migrations.
Configuration snippet¶
mail:
filesystem:
attachments_root: "~/.cache/kstlib/mail/attachments"
inline_root: "~/.cache/kstlib/mail/inline"
templates_root: "~/.cache/kstlib/mail/templates"
allow_external_attachments: false
allow_external_templates: false
auto_create_roots: true
enforce_permissions: true
max_permission_octal: 448 # 0o700
Downstream modules instantiate guardrails using these settings so every file operation inherits the same hardening behavior.
Usage patterns¶
Guarding template directories¶
from pathlib import Path
from kstlib.secure import PathGuardrails, STRICT_POLICY
guard = PathGuardrails(Path("~/kstlib/templates"), policy=STRICT_POLICY)
template = guard.resolve_file("mailers/welcome.html")
Relaxing external access temporarily¶
from kstlib.secure import PathGuardrails
guard = PathGuardrails("/srv/kstlib", policy=STRICT_POLICY)
external_guard = guard.relax(allow_external=True)
external_guard.resolve_file("/opt/legacy/template.html") # raises if file is missing
End-to-end guardrail demo¶
1"""Show how filesystem guardrails accept safe paths and block traversal attempts."""
2
3from __future__ import annotations
4
5from pathlib import Path
6
7from kstlib.secure import RELAXED_POLICY, PathGuardrails, PathSecurityError
8
9
10def build_workspace(root: Path) -> None:
11 """Populate a sandbox with a small report file for the demo."""
12 reports = root / "reports"
13 reports.mkdir(parents=True, exist_ok=True)
14 report_file = reports / "daily.txt"
15 report_file.write_text("Daily conversions: 42", encoding="utf-8")
16
17
18def guardrails_demo() -> None:
19 """Resolve a safe file and demonstrate how traversal is denied."""
20 workspace = Path(__file__).with_name("workspace")
21 guard = PathGuardrails(workspace, policy=RELAXED_POLICY)
22 build_workspace(guard.root)
23
24 safe_report = guard.resolve_file("reports/daily.txt")
25 print(f"Report lives at: {safe_report}")
26
27 try:
28 guard.resolve_file("../etc/passwd")
29 except PathSecurityError as exc:
30 print(f"Traversal blocked: {exc}")
31
32
33if __name__ == "__main__": # pragma: no cover - manual example
34 guardrails_demo()
Module reference¶
Security helpers (filesystem guardrails, policies, password hashing, errors).
- class kstlib.secure.DirectoryPermissions[source]
Bases:
objectPOSIX directory permission constants.
- PRIVATE
Owner-only access (0o700). Use for directories containing sensitive files like tokens or secrets.
- Type:
- SHARED_READ
Owner full, group/others read+execute (0o755). Use for directories with public content.
- Type:
- PRIVATE: int = 448
- SHARED_READ: int = 493
- class kstlib.secure.FilePermissions[source]
Bases:
objectPOSIX file permission constants.
- READONLY
Owner read-only (0o400). Use for sensitive files like tokens, private keys, and secrets. File cannot be modified after creation.
- Type:
- READONLY_ALL
Read-only for all users (0o444). Use for public documents like certificates, CSRs, and public keys.
- Type:
- OWNER_RW
Owner read-write (0o600). Use for files that need to be modified, or temporarily to unlock read-only files before deletion.
- Type:
- OWNER_RWX
Owner read-write-execute (0o700). Use for directories containing sensitive files.
- Type:
- READONLY: int = 256
- READONLY_ALL: int = 292
- OWNER_RW: int = 384
- OWNER_RWX: int = 448
- class kstlib.secure.GuardPolicy(name, allow_external=False, auto_create_root=True, enforce_permissions=True, max_permission_octal=448)[source]
Bases:
objectConfiguration values defining how guardrails behave.
- name
Human-friendly label used for diagnostics.
- Type:
- allow_external
When True, paths outside the root are accepted.
- Type:
- auto_create_root
Automatically create the root directory when missing.
- Type:
- enforce_permissions
Whether POSIX permissions should be validated.
- Type:
- max_permission_octal
Maximum allowed permission mask (defaults to PRIVATE).
- Type:
Example
>>> from kstlib.secure import GuardPolicy >>> policy = GuardPolicy(name="custom", allow_external=False) >>> policy.name 'custom'
- name: str
- allow_external: bool
- auto_create_root: bool
- enforce_permissions: bool
- max_permission_octal: int
- __init__(self, name: 'str', allow_external: 'bool' = False, auto_create_root: 'bool' = True, enforce_permissions: 'bool' = True, max_permission_octal: 'int' = 448) None -> None
- exception kstlib.secure.InvalidPasswordHashError[source]
Bases:
PasswordErrorRaised when a stored value is not a valid Argon2 hash.
- exception kstlib.secure.PasswordError[source]
Bases:
KstlibError,RuntimeErrorRaised when a password hashing operation cannot be completed.
- class kstlib.secure.PathGuardrails(root, *, policy=GuardPolicy(name='strict', allow_external=False, auto_create_root=True, enforce_permissions=True, max_permission_octal=448))[source]
Bases:
objectValidate and resolve paths relative to a trusted root.
Example
>>> import tempfile >>> from kstlib.secure import PathGuardrails, RELAXED_POLICY >>> with tempfile.TemporaryDirectory() as tmpdir: ... guard = PathGuardrails(tmpdir, policy=RELAXED_POLICY) ... guard.policy.name 'relaxed'
- __init__(self, root: 'str | Path', *, policy: 'GuardPolicy' = GuardPolicy(name='strict', allow_external=False, auto_create_root=True, enforce_permissions=True, max_permission_octal=448)) 'None' -> None[source]
Initialise guardrails rooted at root while enforcing policy.
- Raises:
PathSecurityError – If root does not exist or is not a directory.
- property root: Path
Return the resolved guardrail root directory.
- property policy: GuardPolicy
Return the policy associated with the guardrails.
- resolve_file(self, candidate: 'str | Path') 'Path' -> Path[source]
Resolve candidate and ensure it points to an existing file.
- Raises:
PathSecurityError – If path is not a file or is outside root.
- resolve_directory(self, candidate: 'str | Path') 'Path' -> Path[source]
Resolve candidate and ensure it points to an existing directory.
- Raises:
PathSecurityError – If path is not a directory or is outside root.
- resolve_path(self, candidate: 'str | Path') 'Path' -> Path[source]
Resolve candidate relative to the guardrail root without type checks.
- relax(self, *, allow_external: 'bool | None' = None) 'PathGuardrails' -> PathGuardrails[source]
Return a new guardrail instance with adjusted external allowances.
- exception kstlib.secure.PathSecurityError[source]
Bases:
KstlibError,RuntimeErrorRaised when filesystem guardrails detect a security violation.
- kstlib.secure.hash_password(password: 'str | bytes', *, time_cost: 'int | None' = None, memory_cost: 'int | None' = None, parallelism: 'int | None' = None) 'str' -> str[source]
Hash a password with Argon2id and return the encoded PHC string.
The cost knobs (
time_cost,memory_cost,parallelism) resolve askwargs > kstlib config > defaultsand are clamped to the security floors (time_cost>=2,memory_cost>=19456KiB,parallelism>=1). Defaults track the argon2-cffi 25.x / OWASP recommendation (time_cost=3,memory_cost=65536KiB,parallelism=4).Output sizing (
hash_len,salt_len) is intentionally not a per-call argument: it must stay consistent across all stored hashes. Configure it once viasecure.passwords.hash_len/secure.passwords.salt_leninkstlib.conf.yml(defaultshash_len=32,salt_len=16; floorshash_len>=16,salt_len>=16).- Parameters:
password (str | bytes) – Plaintext password as
str(UTF-8 encoded) orbytes.time_cost (int | None) – Number of iterations. Defaults to the cascade value.
memory_cost (int | None) – Memory usage in KiB. Defaults to the cascade value.
parallelism (int | None) – Number of parallel lanes. Defaults to the cascade value.
- Returns:
The Argon2id hash encoded as a PHC string (
$argon2id$...).- Raises:
PasswordError – If argon2-cffi is not installed, the password is not
strorbytes, the password exceedsMAX_PASSWORD_LENGTH, or the underlying hashing operation fails.- Return type:
Example
>>> from kstlib.secure import hash_password >>> hash_password("s3cret") '$argon2id$v=19$m=65536,t=3,p=4$...'
- kstlib.secure.needs_rehash(stored_hash: 'str') 'bool' -> bool[source]
Return whether a stored hash should be recomputed with current parameters.
Compares the parameters embedded in stored_hash against the currently resolved policy (kstlib config or defaults). Returns
Truewhen the stored hash is weaker than the current parameters and should be upgraded on the next successful login.- Parameters:
stored_hash (str) – Previously stored PHC hash string.
- Returns:
Trueif the hash should be regenerated,Falseotherwise.- Raises:
PasswordError – If argon2-cffi is not installed.
InvalidPasswordHashError – If
stored_hashis not a valid Argon2 hash.
- Return type:
- kstlib.secure.verify_password(password: 'str | bytes', stored_hash: 'str') 'bool' -> bool[source]
Verify a password against a stored Argon2id hash in constant time.
- Parameters:
- Returns:
Trueif the password matches,Falseotherwise (including when the password exceedsMAX_PASSWORD_LENGTH).- Raises:
PasswordError – If argon2-cffi is not installed or the password is not
strorbytes.InvalidPasswordHashError – If
stored_hashis not a valid Argon2 hash.
- Return type:
Example
>>> from kstlib.secure import hash_password, verify_password >>> stored = hash_password("s3cret") >>> verify_password("s3cret", stored) True
Cost constants (DEFAULT_*, MIN_*, MAX_PASSWORD_LENGTH) and the hashing functions, defined in kstlib.secure.passwords:
Argon2id password hashing helpers.
This module wraps the optional argon2-cffi backend (install via
pip install kstlib[passwords]) to provide password hashing and
verification with safe, OWASP-aligned defaults.
Cost parameters follow a kwargs > kstlib config > defaults cascade. Any
resolved parameter below the security floor is clamped up and a [SECURITY]
warning is logged. Passwords and hashes are never written to the logs.
Example
>>> from kstlib.secure import hash_password, verify_password
>>> stored = hash_password("correct horse battery staple")
>>> verify_password("correct horse battery staple", stored)
True
- exception kstlib.secure.passwords.InvalidPasswordHashError[source]
Bases:
PasswordErrorRaised when a stored value is not a valid Argon2 hash.
- exception kstlib.secure.passwords.PasswordError[source]
Bases:
KstlibError,RuntimeErrorRaised when a password hashing operation cannot be completed.
- kstlib.secure.passwords.hash_password(password: 'str | bytes', *, time_cost: 'int | None' = None, memory_cost: 'int | None' = None, parallelism: 'int | None' = None) 'str' -> str[source]
Hash a password with Argon2id and return the encoded PHC string.
The cost knobs (
time_cost,memory_cost,parallelism) resolve askwargs > kstlib config > defaultsand are clamped to the security floors (time_cost>=2,memory_cost>=19456KiB,parallelism>=1). Defaults track the argon2-cffi 25.x / OWASP recommendation (time_cost=3,memory_cost=65536KiB,parallelism=4).Output sizing (
hash_len,salt_len) is intentionally not a per-call argument: it must stay consistent across all stored hashes. Configure it once viasecure.passwords.hash_len/secure.passwords.salt_leninkstlib.conf.yml(defaultshash_len=32,salt_len=16; floorshash_len>=16,salt_len>=16).- Parameters:
password (str | bytes) – Plaintext password as
str(UTF-8 encoded) orbytes.time_cost (int | None) – Number of iterations. Defaults to the cascade value.
memory_cost (int | None) – Memory usage in KiB. Defaults to the cascade value.
parallelism (int | None) – Number of parallel lanes. Defaults to the cascade value.
- Returns:
The Argon2id hash encoded as a PHC string (
$argon2id$...).- Raises:
PasswordError – If argon2-cffi is not installed, the password is not
strorbytes, the password exceedsMAX_PASSWORD_LENGTH, or the underlying hashing operation fails.- Return type:
Example
>>> from kstlib.secure import hash_password >>> hash_password("s3cret") '$argon2id$v=19$m=65536,t=3,p=4$...'
- kstlib.secure.passwords.needs_rehash(stored_hash: 'str') 'bool' -> bool[source]
Return whether a stored hash should be recomputed with current parameters.
Compares the parameters embedded in stored_hash against the currently resolved policy (kstlib config or defaults). Returns
Truewhen the stored hash is weaker than the current parameters and should be upgraded on the next successful login.- Parameters:
stored_hash (str) – Previously stored PHC hash string.
- Returns:
Trueif the hash should be regenerated,Falseotherwise.- Raises:
PasswordError – If argon2-cffi is not installed.
InvalidPasswordHashError – If
stored_hashis not a valid Argon2 hash.
- Return type:
- kstlib.secure.passwords.verify_password(password: 'str | bytes', stored_hash: 'str') 'bool' -> bool[source]
Verify a password against a stored Argon2id hash in constant time.
- Parameters:
- Returns:
Trueif the password matches,Falseotherwise (including when the password exceedsMAX_PASSWORD_LENGTH).- Raises:
PasswordError – If argon2-cffi is not installed or the password is not
strorbytes.InvalidPasswordHashError – If
stored_hashis not a valid Argon2 hash.
- Return type:
Example
>>> from kstlib.secure import hash_password, verify_password >>> stored = hash_password("s3cret") >>> verify_password("s3cret", stored) True