Skip to content

Commit ec35100

Browse files
committed
update
1 parent cb37025 commit ec35100

4 files changed

Lines changed: 288 additions & 3 deletions

File tree

‎guidelines/securityprinciples.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ short_title: Security Principles Checklist
77
Applying Security by Design principles is essential when building secure Python applications.
88
:::
99

10-
10+
(security-principles)=
1111
## Security Principles
1212

1313
When developing Python applications, you should apply at **minimum** the following Security by Design principles:

‎recipes/recipes_intro.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
title: Python Security Recipes
3+
short_title: Security Recipes
4+
---
5+
6+
7+
When developing secure applications in Python, clear and practical examples are indispensable. This section provides actionable, straightforward solutions to common Python security challenges.
8+
9+
Each recipe follows a consistent, easy-to-digest blueprint:
10+
11+
* **Challenge:** The development goal or requirement.
12+
* **Threat:** The risk or attack vector associated with incorrect implementation.
13+
* **Vulnerable Example:** Real-world code demonstrating the flaw.
14+
* **Secure Mitigation:** Corrected, production-ready code implementing defensive practices.
15+
* **Discussion:** An architectural breakdown of why the mitigation works and further security nuances.
16+
17+
+++ {"no-pdf": true}
18+
19+
:::{toc}
20+
:context: children
21+
:::
22+
23+
+++

‎recipes/use_secrets.md‎

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
---
2+
title: How To Use Secrets in Python Code
3+
short_title: How To Use Secrets
4+
---
5+
6+
## The Challenge
7+
8+
Every non-trivial Python application relies on secrets: API keys for third-party services, database passwords, JWT signing keys, cloud provider credentials, and encryption passphrases. The challenge is both operational and cryptographic: you must deliver these values to your application at runtime while preventing them from leaking into logs, version control, error reports, or attacker-controlled memory dumps.
9+
10+
In practice, this means solving three distinct problems:
11+
12+
1. **Storage**: Where do you keep secrets when the application is not running?
13+
2. **Transport**: How do you securely inject secrets into the running process?
14+
3. **Lifecycle**: How do you rotate, revoke, and audit secret usage without downtime?
15+
16+
Many teams treat these as afterthoughts—hard-coding credentials during development and "fixing" them later. But always use a [Security By Design](https://nocomplexity.com/securitybydesign/) approach. Fixing security later is not possible. Start with security from the start!
17+
18+
---
19+
20+
## The Threat
21+
22+
To understand why secret management matters, think like an attacker. Secrets can be stolen or get lost in many different ways, each with different mitigation strategies:
23+
24+
**Exfiltration from Version Control**
25+
Hard-coded secrets in source code are the most common vulnerability. Attackers scan public repositories for patterns matching API keys, AWS access keys, and private keys. Even private repositories are not safe—misconfigured CI pipelines, compromised developer accounts, and insider threats all expose stored credentials.
26+
27+
**Leakage via Logging and Debug Output**
28+
Python's logging framework, exception tracebacks, and debuggers frequently output variable contents. If your code logs a request payload, a configuration dump, or an error context that includes a secret, that secret ends up in log files, monitoring systems, and potentially aggregated into centralized logging platforms with weaker access controls.
29+
30+
**Environment Variable Exposure**
31+
Environment variables are the most common delivery mechanism, but they are also visible to:
32+
- Any process running under the same user (`/proc/PID/environ` on Linux)
33+
- Child processes that inherit the environment
34+
- Debuggers and crash dumps
35+
- Container orchestration UIs that display environment variables by default
36+
37+
38+
**In-Memory Exposure**
39+
Python's object model makes it difficult to securely scrub secrets from memory. Strings are immutable—when you overwrite a secret string, the old value remains in memory until the garbage collector reclaims it. Additionally, Python's memory allocator may not return freed memory to the operating system, leaving secret fragments in process memory that could be captured via core dumps, swap files, or cold-boot attacks.
40+
41+
**Side-Channel Exposure**
42+
Secrets can leak through timing attacks (comparing secret values byte-by-byte), through error messages that reveal whether a secret matched partially, or through monitoring that records the frequency of secret usage.
43+
44+
**Supply Chain Risk**
45+
Third-party dependencies can inadvertently expose secrets through telemetry, crash reporting, or debug logs. More maliciously, a compromised dependency could exfiltrate environment variables at runtime.
46+
47+
**The Principle**: If a secret touches disk, stdout, stderr, or a log file, consider it compromised. If you cannot guarantee that a secret is ephemeral and scoped, assume it will eventually be disclosed.
48+
49+
---
50+
51+
## Vulnerable Code Example
52+
53+
This simple script demonstrates a background job querying a database. It contains severe anti-patterns: hard-coding secrets directly into the source code, performing non-constant-time string comparisons, and leaking system contexts into error outputs.
54+
55+
```python
56+
import sqlite3
57+
58+
# VULNERABILITY 1: Hard-coded production credentials in source control
59+
DB_PASSWORD = "super_secret_production_password_123"
60+
ADMIN_KEY = "admin123"
61+
62+
def authenticate_and_query(user_provided_key):
63+
# VULNERABILITY 3: Non-constant-time string comparison (==)
64+
# Python stops comparing at the first mismatched byte, introducing timing side-channels.
65+
if user_provided_key == ADMIN_KEY:
66+
try:
67+
# VULNERABILITY 2: Hard-coded credentials passed directly into the connection
68+
conn = sqlite3.connect(f"file:prod_db?password={DB_PASSWORD}", uri=True)
69+
cursor = conn.cursor()
70+
cursor.execute("SELECT * FROM sensitive_records")
71+
records = cursor.fetchall()
72+
conn.close()
73+
return records
74+
except Exception as e:
75+
# VULNERABILITY 4: Leaking raw exception details and variable state to stdout
76+
print(f"Failed to connect using password {DB_PASSWORD}. Error: {e}")
77+
return None
78+
else:
79+
print("Access denied.")
80+
return None
81+
82+
```
83+
84+
---
85+
86+
## Secure Mitigation
87+
88+
This secure implementation mitigates these risks by pulling configuration dynamically via a single, session-persistent Secret Manager client, using constant-time comparisons, and executing low-level memory zeroing before clean exit routines.
89+
90+
```python
91+
import ctypes
92+
import hmac
93+
import logging
94+
import os
95+
import sqlite3
96+
import hvac
97+
98+
# Configure safe structured logging
99+
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
100+
logger = logging.getLogger(__name__)
101+
102+
103+
class LocalVaultManager:
104+
"""
105+
Encapsulates persistent local HashiCorp Vault interactions.
106+
Reuses sessions rather than authenticating per-request.
107+
"""
108+
def __init__(self):
109+
self._client = None
110+
111+
def _get_client(self) -> hvac.Client:
112+
if self._client and self._client.is_authenticated():
113+
return self._client
114+
115+
url = os.environ.get("VAULT_ADDR")
116+
role_id = os.environ.get("VAULT_ROLE_ID")
117+
secret_id = os.environ.get("VAULT_SECRET_ID")
118+
119+
if not all([url, role_id, secret_id]):
120+
raise RuntimeError("CRITICAL: Vault orchestration credentials missing from process memory.")
121+
122+
client = hvac.Client(url=url)
123+
client.auth.approle.login(role_id=role_id, secret_id=secret_id)
124+
self._client = client
125+
return self._client
126+
127+
def fetch_secrets(self, path: str) -> dict:
128+
try:
129+
client = self._get_client()
130+
response = client.secrets.kv.v2.read_secret_version(path=path)
131+
return response["data"]["data"]
132+
except Exception as e:
133+
logger.error(f"Failed to fetch secrets securely: {type(e).__name__}")
134+
return {}
135+
136+
137+
def zero_string_buffer(s: str):
138+
"""
139+
Overwrites the underlying memory allocation buffer of a Python string
140+
with null bytes to remove traces before garbage collection triggers.
141+
"""
142+
if not isinstance(s, str) or not s:
143+
return
144+
# Trace specific memory offset for standard CPython string configurations
145+
offset = id(s) + ctypes.sizeof(ctypes.c_void_p) * 4
146+
ctypes.memset(offset, 0, len(s))
147+
148+
149+
# Global vault manager allocation handles authentication persistence
150+
vault = LocalVaultManager()
151+
152+
153+
def authenticate_and_query(user_provided_key: str):
154+
# Retrieve scoped secrets dictionary in a single network transaction
155+
secrets_package = vault.fetch_secrets("secret/data/prod/app")
156+
157+
expected_key = secrets_package.get("admin_key")
158+
db_password = secrets_package.get("password")
159+
160+
if not expected_key or not db_password:
161+
logger.error("Configuration payload generation failed.")
162+
return None
163+
164+
# MITIGATION 3: Constant-time comparison eliminates timing side-channels
165+
if not hmac.compare_digest(user_provided_key, expected_key):
166+
logger.warning("Unauthorized access attempt.")
167+
zero_string_buffer(expected_key)
168+
zero_string_buffer(db_password)
169+
return None
170+
171+
try:
172+
# MITIGATION 2 & 5: Ephemeral connection context scoped tightly
173+
with sqlite3.connect(f"file:prod_db?password={db_password}", uri=True) as conn:
174+
cursor = conn.cursor()
175+
cursor.execute("SELECT * FROM sensitive_records")
176+
return cursor.fetchall()
177+
178+
except sqlite3.Error as db_err:
179+
# MITIGATION 4: Generic logs mask underlying parameters and call-stack variables
180+
logger.error(f"Database operation failed: {type(db_err).__name__}")
181+
return None
182+
finally:
183+
# MITIGATION 5: Hard zeroing of memory locations before discarding namespaces
184+
zero_string_buffer(expected_key)
185+
zero_string_buffer(db_password)
186+
del expected_key
187+
del db_password
188+
189+
```
190+
191+
---
192+
193+
## Discussion
194+
195+
The secure mitigation presented above demonstrates how fundamental security principles translate into practical Python code. Managing and using secrets in Python applications requires applying these principles rigorously—they are not theoretical abstractions but actionable guidelines that directly inform every design decision. Below, we examine how each principle maps to our implementation, alongside the trade-offs and residual risks that remain.
196+
197+
### Operational Considerations (Design for Secure Updates)
198+
199+
> **Systems must safely apply patches. Update ability is a security feature.**
200+
201+
**Secret Rotation**: The secure version pulls secrets programmatically at runtime from local storage managers. Because keys are pulled inside local variable contexts, standard key updates inside Vault do not require code changes or system rollouts.
202+
203+
**Incident Response**: When a secret is compromised, you need to:
204+
205+
1. Immediately revoke the token leases inside the Vault controller registry.
206+
2. Rotate underlying application and target master credentials.
207+
3. Check access metrics over Vault audit pipelines.
208+
209+
---
210+
211+
### Principles in Practice when managing and using secrets
212+
213+
| Principle | Summary | How We Implemented It |
214+
| --- | --- | --- |
215+
| **Minimise attack surface area** | Remove unnecessary features | Handled configuration data using minimal scripts, removing exposed parameters. |
216+
| **Establish secure defaults** | Deny by default | Verification checks drop immediately back to a closed state (`return None`). |
217+
| **Least privilege** | Minimum permissions | Vault tokens bound uniquely to the specific application path read scope. |
218+
| **Separation of duties** | Split critical functions | Application reads credential metadata, but lacks rights to manage infrastructure targets. |
219+
| **Defence in depth** | Layer independent controls | Single-path fetch protocols combined with constant-time comparison layers. |
220+
| **Fail securely** | Never fail open | Captured processing failures cleanly, preventing raw execution traces from outputting. |
221+
| **Complete mediation** | Every access checked | Constant-time comparisons check input authenticity on every operational iteration. |
222+
| **Economy of mechanism** | Keep it simple | Session caching avoids complex internal management systems or background handlers. |
223+
| **Open design** | No security by obscurity | Design safely shifts assumptions; security lives in the token, not hidden code branches. |
224+
| **Zero Trust** | Verify everything | Application strictly fetches current runtime values rather than caching ambient fields. |
225+
| **Compartmentalisation** | Isolate components | Function instances manage connection scope properties safely apart from peripheral execution loops. |
226+
| **Protect data everywhere** | Encrypt everywhere | Realized through zeroing structures and native local transport loop parameters. |
227+
| **Design for secure updates** | Safe patching | Runtime polling infrastructure accepts dynamic rotations seamlessly. |
228+
229+
---
230+
231+
:::{caution}
232+
Secrets are not source code. They are operational data that should be treated with the same care as encryption keys. Your application must be designed so that secrets can be changed without redeploying, and the system must gracefully handle secret rotation without downtime.
233+
:::
234+
235+
## Dangerous Solutions (Anti Patterns)
236+
237+
:::{danger} Dangerous Anti-Pattern: `.env` Files and `python-dotenv`
238+
A pervasive anti-pattern in Python tutorials is the recommendation to use `python-dotenv` for managing secrets. This advice is dangerously misguided.
239+
:::
240+
241+
### 1. Persistent Disk Exposure
242+
243+
`.env` files are plain-text files stored on disk. They are vulnerable to:
244+
245+
* Directory traversal attacks exposing the file
246+
* Accidental commits to version control
247+
* Backup and snapshot exposure
248+
249+
### 2. Inadequate Security Controls
250+
251+
Security audits of `python-dotenv` reveal it fails on crucial [security validations](#security-principles):
252+
253+
* **Lack of Access Auditing:** Plain-text files can be copied or read by any rogue dependency without logging access events.
254+
* **Cleartext Lifetime:** Storing passwords in cleartext on standard storage blocks violates core regulatory standards (e.g., SOC2, PCI-DSS).
255+
* **No Dynamic Rotation Support:** File states cannot accept rapid programmatic cryptographic changes without introducing operational instability.
256+
257+
### 3. The ".env Environment" Fallacy
258+
259+
`python-dotenv` loads secrets from disk into `os.environ`, creating a false sense of security. True environment variables are set by the operating system or orchestration platform and never touch disk.
260+
261+
So **do not** use `python-dotenv` or `.env` files for managing secrets in Python applications. They are a dangerous anti-pattern that introduces unnecessary risk without providing any security benefit. Use environment variables, secret managers, or orchestration-native solutions instead.

‎toc.yml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,14 @@ project:
6868
- file: guidelines/validatetips.md
6969
- file: guidelines/securityprinciples.md
7070

71-
- title: Python Security Recipes
72-
children:
71+
- file: recipes/recipes_intro.md
72+
children: # drop down
7373
- file: recipes/openfile.md
7474
- file: recipes/traversalattacks.md
7575
- file: recipes/path_module.md
7676
- file: recipes/assert_use.md
7777
- file: recipes/data_egress.md
78+
- file: recipes/use_secrets.md
7879

7980
- title: Appendices #section header, no page
8081
children:

0 commit comments

Comments
 (0)