0
# File-Based Secrets
1
2
Docker-style secret file support for reading sensitive configuration from mounted files instead of environment variables, enabling secure handling of passwords, API keys, and certificates in containerized environments.
3
4
## Capabilities
5
6
### FileAwareEnv Class
7
8
Extended environment reader that supports reading values from files when corresponding `_FILE` environment variables are set.
9
10
```python { .api }
11
class FileAwareEnv(Env):
12
def __init__(self, *, file_suffix="_FILE", eager=True, expand_vars=False, prefix=None):
13
"""
14
Initialize FileAwareEnv with file reading capabilities.
15
16
Parameters:
17
- file_suffix: str, suffix appended to variable names for file paths (default: "_FILE")
18
- eager: bool, whether to raise validation errors immediately (default: True)
19
- expand_vars: bool, whether to expand ${VAR} syntax (default: False)
20
- prefix: str, global prefix for all variable names (default: None)
21
22
Behavior:
23
When parsing variable "SECRET", first checks for "SECRET_FILE" environment variable.
24
If "SECRET_FILE" exists and points to readable file, returns file contents.
25
Otherwise falls back to standard "SECRET" environment variable.
26
"""
27
```
28
29
Usage examples:
30
31
```python
32
import os
33
from environs import FileAwareEnv
34
from pathlib import Path
35
36
# Create temporary secret files for demonstration
37
Path("/tmp/db_password").write_text("super_secret_password")
38
Path("/tmp/api_key").write_text("sk_live_123456789abcdef")
39
40
# Set up file-based environment variables
41
os.environ["DATABASE_PASSWORD_FILE"] = "/tmp/db_password"
42
os.environ["API_KEY_FILE"] = "/tmp/api_key"
43
os.environ["REGULAR_CONFIG"] = "not_from_file"
44
45
# Initialize FileAwareEnv
46
file_env = FileAwareEnv()
47
48
# Read from files
49
db_password = file_env.str("DATABASE_PASSWORD") # Reads from /tmp/db_password
50
api_key = file_env.str("API_KEY") # Reads from /tmp/api_key
51
regular = file_env.str("REGULAR_CONFIG") # Reads from environment variable
52
53
print(f"Database password: {db_password}") # => "super_secret_password"
54
print(f"API key: {api_key}") # => "sk_live_123456789abcdef"
55
print(f"Regular config: {regular}") # => "not_from_file"
56
57
# Clean up
58
Path("/tmp/db_password").unlink()
59
Path("/tmp/api_key").unlink()
60
```
61
62
### Custom File Suffix
63
64
Customize the suffix used for file environment variables to match your naming conventions.
65
66
```python
67
import os
68
from environs import FileAwareEnv
69
from pathlib import Path
70
71
# Custom suffix example
72
Path("/tmp/secret").write_text("my_secret_value")
73
os.environ["API_SECRET_PATH"] = "/tmp/secret"
74
75
# Initialize with custom suffix
76
custom_env = FileAwareEnv(file_suffix="_PATH")
77
78
# Read using custom suffix
79
secret = custom_env.str("API_SECRET") # Looks for API_SECRET_PATH
80
print(f"Secret: {secret}") # => "my_secret_value"
81
82
# Clean up
83
Path("/tmp/secret").unlink()
84
```
85
86
### Docker Secrets Integration
87
88
Integrate with Docker Swarm secrets or Kubernetes mounted secrets for production deployments.
89
90
```python
91
import os
92
from environs import FileAwareEnv
93
94
# Docker Swarm secrets are typically mounted at /run/secrets/
95
os.environ["DATABASE_PASSWORD_FILE"] = "/run/secrets/db_password"
96
os.environ["SSL_CERT_FILE"] = "/run/secrets/ssl_certificate"
97
os.environ["SSL_KEY_FILE"] = "/run/secrets/ssl_private_key"
98
99
# Kubernetes secrets mounted as files
100
os.environ["JWT_SECRET_FILE"] = "/etc/secrets/jwt_secret"
101
os.environ["OAUTH_CLIENT_SECRET_FILE"] = "/etc/secrets/oauth_client_secret"
102
103
file_env = FileAwareEnv()
104
105
# Read secrets from mounted files
106
try:
107
db_password = file_env.str("DATABASE_PASSWORD")
108
ssl_cert = file_env.str("SSL_CERT")
109
ssl_key = file_env.str("SSL_KEY")
110
jwt_secret = file_env.str("JWT_SECRET")
111
oauth_secret = file_env.str("OAUTH_CLIENT_SECRET")
112
113
print("All secrets loaded successfully from files")
114
except Exception as e:
115
print(f"Error loading secrets: {e}")
116
```
117
118
### Fallback Behavior
119
120
FileAwareEnv gracefully falls back to regular environment variables when file variables are not set.
121
122
```python
123
import os
124
from environs import FileAwareEnv
125
from pathlib import Path
126
127
file_env = FileAwareEnv()
128
129
# Set up mixed configuration
130
Path("/tmp/file_secret").write_text("from_file")
131
os.environ["SECRET_FROM_FILE_FILE"] = "/tmp/file_secret"
132
os.environ["SECRET_FROM_ENV"] = "from_environment"
133
134
# FileAwareEnv will read from file when _FILE variable exists
135
secret1 = file_env.str("SECRET_FROM_FILE") # => "from_file"
136
137
# Falls back to environment variable when no _FILE variable
138
secret2 = file_env.str("SECRET_FROM_ENV") # => "from_environment"
139
140
# Default values work as expected
141
secret3 = file_env.str("MISSING_SECRET", "default") # => "default"
142
143
print(f"From file: {secret1}")
144
print(f"From env: {secret2}")
145
print(f"Default: {secret3}")
146
147
# Clean up
148
Path("/tmp/file_secret").unlink()
149
```
150
151
### Type Casting with File Secrets
152
153
All standard environs type casting works with file-based values.
154
155
```python
156
import os
157
from environs import FileAwareEnv
158
from pathlib import Path
159
160
# Create files with different data types
161
Path("/tmp/port_number").write_text("8080")
162
Path("/tmp/debug_flag").write_text("true")
163
Path("/tmp/allowed_hosts").write_text("localhost,127.0.0.1,example.com")
164
Path("/tmp/config_json").write_text('{"timeout": 30, "retries": 3}')
165
166
# Set up file environment variables
167
os.environ["PORT_FILE"] = "/tmp/port_number"
168
os.environ["DEBUG_FILE"] = "/tmp/debug_flag"
169
os.environ["ALLOWED_HOSTS_FILE"] = "/tmp/allowed_hosts"
170
os.environ["CONFIG_FILE"] = "/tmp/config_json"
171
172
file_env = FileAwareEnv()
173
174
# Type casting works with file contents
175
port = file_env.int("PORT") # => 8080
176
debug = file_env.bool("DEBUG") # => True
177
hosts = file_env.list("ALLOWED_HOSTS") # => ["localhost", "127.0.0.1", "example.com"]
178
config = file_env.json("CONFIG") # => {"timeout": 30, "retries": 3}
179
180
print(f"Port: {port} (type: {type(port)})")
181
print(f"Debug: {debug} (type: {type(debug)})")
182
print(f"Hosts: {hosts}")
183
print(f"Config: {config}")
184
185
# Clean up
186
for file_path in ["/tmp/port_number", "/tmp/debug_flag", "/tmp/allowed_hosts", "/tmp/config_json"]:
187
Path(file_path).unlink()
188
```
189
190
### Error Handling
191
192
Handle file-related errors appropriately with detailed error messages.
193
194
```python
195
import os
196
from environs import FileAwareEnv, EnvValidationError
197
198
file_env = FileAwareEnv()
199
200
# File doesn't exist
201
os.environ["MISSING_FILE_FILE"] = "/path/that/does/not/exist"
202
try:
203
value = file_env.str("MISSING_FILE")
204
except ValueError as e:
205
print(f"File not found: {e}")
206
207
# Permission denied
208
os.environ["RESTRICTED_FILE_FILE"] = "/root/restricted_file"
209
try:
210
value = file_env.str("RESTRICTED_FILE")
211
except ValueError as e:
212
print(f"Permission error: {e}")
213
214
# Directory instead of file
215
os.environ["DIRECTORY_FILE"] = "/tmp"
216
try:
217
value = file_env.str("DIRECTORY")
218
except ValueError as e:
219
print(f"Directory error: {e}")
220
221
# Invalid file content for type casting
222
from pathlib import Path
223
Path("/tmp/invalid_int").write_text("not_a_number")
224
os.environ["INVALID_INT_FILE"] = "/tmp/invalid_int"
225
226
try:
227
value = file_env.int("INVALID_INT")
228
except EnvValidationError as e:
229
print(f"Type casting error: {e}")
230
231
# Clean up
232
Path("/tmp/invalid_int").unlink()
233
```
234
235
### Production Deployment Example
236
237
Complete example showing how to use FileAwareEnv in a production Django application with Docker secrets.
238
239
```python
240
# settings.py for production Django app
241
import os
242
from environs import FileAwareEnv
243
244
# Initialize FileAwareEnv for production secrets
245
env = FileAwareEnv()
246
247
# Load .env file if it exists (for local development)
248
env.read_env()
249
250
# Basic settings
251
DEBUG = env.bool("DEBUG", False)
252
SECRET_KEY = env.str("SECRET_KEY") # Will read from SECRET_KEY_FILE if available
253
254
# Database configuration (password from file in production)
255
DATABASES = {
256
'default': {
257
'ENGINE': 'django.db.backends.postgresql',
258
'NAME': env.str("DB_NAME"),
259
'USER': env.str("DB_USER"),
260
'PASSWORD': env.str("DB_PASSWORD"), # From DB_PASSWORD_FILE in production
261
'HOST': env.str("DB_HOST", "localhost"),
262
'PORT': env.int("DB_PORT", 5432),
263
}
264
}
265
266
# Redis configuration (password from file)
267
REDIS_PASSWORD = env.str("REDIS_PASSWORD", "") # From REDIS_PASSWORD_FILE
268
269
# Email configuration (SMTP password from file)
270
EMAIL_HOST_PASSWORD = env.str("EMAIL_HOST_PASSWORD", "")
271
272
# SSL certificate paths (certificate content from files)
273
SSL_CERTIFICATE = env.str("SSL_CERTIFICATE", "") # From SSL_CERTIFICATE_FILE
274
SSL_PRIVATE_KEY = env.str("SSL_PRIVATE_KEY", "") # From SSL_PRIVATE_KEY_FILE
275
276
# API keys (from files in production)
277
STRIPE_SECRET_KEY = env.str("STRIPE_SECRET_KEY", "")
278
AWS_SECRET_ACCESS_KEY = env.str("AWS_SECRET_ACCESS_KEY", "")
279
```
280
281
Corresponding Docker Compose configuration:
282
283
```yaml
284
# docker-compose.yml
285
version: '3.8'
286
services:
287
web:
288
image: myapp:latest
289
environment:
290
- DEBUG=false
291
- SECRET_KEY_FILE=/run/secrets/django_secret_key
292
- DB_PASSWORD_FILE=/run/secrets/db_password
293
- REDIS_PASSWORD_FILE=/run/secrets/redis_password
294
- STRIPE_SECRET_KEY_FILE=/run/secrets/stripe_secret_key
295
secrets:
296
- django_secret_key
297
- db_password
298
- redis_password
299
- stripe_secret_key
300
301
secrets:
302
django_secret_key:
303
external: true
304
db_password:
305
external: true
306
redis_password:
307
external: true
308
stripe_secret_key:
309
external: true
310
```
311
312
### Local Development with Files
313
314
Use file-based secrets in local development for consistency with production.
315
316
```python
317
# Local development setup
318
import os
319
from environs import FileAwareEnv
320
from pathlib import Path
321
322
# Create local secrets directory
323
secrets_dir = Path("./secrets")
324
secrets_dir.mkdir(exist_ok=True)
325
326
# Write development secrets to files
327
(secrets_dir / "db_password").write_text("dev_password_123")
328
(secrets_dir / "api_key").write_text("dev_api_key_456")
329
(secrets_dir / "jwt_secret").write_text("dev_jwt_secret_789")
330
331
# Set environment to use local secret files
332
os.environ["DATABASE_PASSWORD_FILE"] = str(secrets_dir / "db_password")
333
os.environ["API_KEY_FILE"] = str(secrets_dir / "api_key")
334
os.environ["JWT_SECRET_FILE"] = str(secrets_dir / "jwt_secret")
335
336
# Use FileAwareEnv as in production
337
env = FileAwareEnv()
338
339
db_password = env.str("DATABASE_PASSWORD")
340
api_key = env.str("API_KEY")
341
jwt_secret = env.str("JWT_SECRET")
342
343
print("Local development secrets loaded from files")
344
```
345
346
## Types
347
348
```python { .api }
349
from pathlib import Path
350
from typing import Union
351
352
FilePath = Union[str, Path]
353
```