Skip to content

Commit 348c8ce

Browse files
committed
Merge #273: feat: [#272] Add HTTPS support with Caddy for all HTTP services
28336e0 refactor: [#272] Set X-Forwarded-For header only for HTTP trackers (Jose Celano) e464aca feat: [#272] Explicitly set X-Forwarded-For header in Caddy reverse proxy (Jose Celano) 8b6e361 docs: [#272] Add ADRs for HTTPS implementation decisions (Phase 9) (Jose Celano) 6948342 docs: [#272] Explain why test command cannot work in Docker-based E2E tests (Jose Celano) 310f93f docs: [#272] Add revised Phase 6 implementation plan for HTTPS E2E testing (Jose Celano) bf5df90 docs: [#272] Add HTTPS user documentation (Jose Celano) 2426650 refactor: [#272] Refactor ServiceEndpoint to store validated URL and server IP (Jose Celano) 3815f55 feat: [#272] Add HTTPS support to test command with ServiceEndpoint type (Jose Celano) c7e0dc4 refactor: [#272] Remove unused TlsSection and domain::tls module (Jose Celano) 01da06e refactor: [#272] Replace tls with domain+use_tls_proxy for Grafana (Jose Celano) 55dfa94 refactor: [#272] Replace tls with domain+use_tls_proxy for Health Check API (Jose Celano) 3662aff refactor: [#272] Replace tls with domain+use_tls_proxy for HTTP API (Jose Celano) 80b4b32 refactor: [#272] replace TlsSection with domain + use_tls_proxy for HTTP trackers (Jose Celano) d796db7 docs: [#272] Add reproduction steps for on_reverse_proxy issue (Jose Celano) bf73227 docs: [#272] Document tracker on_reverse_proxy global setting limitation (Jose Celano) a857e07 docs: [#272] Add subtask 7.5 for use_tls_proxy configuration refactor (Jose Celano) 5334429 feat: [#272] Handle localhost-bound services in show command and validation (Task 7.4) (Jose Celano) d21f313 docs: [#272] clarify task 7.4 implementation notes (Jose Celano) f143303 feat: [#272] add HTTPS/TLS support for health check API (Jose Celano) 1855058 docs: [#272] add tasks 7.3 and 7.4 for health check TLS and localhost handling (Jose Celano) c1c194a feat: [#272] update show command to display HTTPS-enabled services (Jose Celano) 704f153 feat: [#272] add Caddy to Docker security scan workflow (Jose Celano) c8236eb chore: [#272] add rustc-ice files to gitignore (Jose Celano) 011cd8c refactor: [#272] improve docker-compose template whitespace handling (Jose Celano) 56ad1dc refactor: [#272] add MysqlServiceConfig for MySQL service network configuration (Jose Celano) d508cc7 refactor: [#272] separate Caddy contexts for Caddyfile and docker-compose templates (Jose Celano) 526e7ab refactor: [#272] rename ports module to tracker for service consistency (Jose Celano) 02a83be refactor: [#272] move Prometheus and Grafana networks logic from Tera to Rust (Jose Celano) a93596c docs: [#272] add CLI command HTTPS compatibility phase and fix VM file locations (Jose Celano) 1b69af4 refactor: [#272] move tracker networks logic from template to Rust (Jose Celano) 180f364 refactor: [#272] use YAML anchors in docker-compose template for DRY config (Jose Celano) ef906fc refactor: [#272] simplify docker-compose template by pre-computing TLS flags in Rust (Jose Celano) 7882917 feat: [#272] add HTTPS support with Caddy for all HTTP services (Jose Celano) 3bbaea1 feat: [#272] add Caddy template rendering infrastructure (Jose Celano) df19401 feat: [#272] add Caddy templates for HTTPS support (Jose Celano) 900aeac docs: [#272] clarify context data preparation pattern for Tera templates (Jose Celano) Pull request description: ## Summary Add HTTPS support with Caddy reverse proxy for automatic TLS termination on all HTTP services (Tracker API, HTTP Trackers, Grafana, Health Check API). Closes #272 ## What's Implemented ### Phase 1: Template Creation ✅ - Created `templates/caddy/Caddyfile.tera` with conditional service blocks - Created `docs/contributing/templates/caddy.md` documenting template variables - Updated `templates/docker-compose/docker-compose.yml.tera` with Caddy service block - Registered templates in `CaddyProjectGenerator` with 14 unit tests ### Phase 2: Configuration DTOs ✅ - Added `HttpsSection` DTO with `admin_email` and `use_staging` fields - Added per-service `domain` and `use_tls_proxy` fields (replaced nested `tls` section) - Extended `HttpApiSection`, `HttpTrackerSection`, `GrafanaSection`, `HealthCheckApiSection` with TLS options - Implemented validation (`has_any_tls_configured`, https/tls consistency, uniform HTTP tracker TLS) - Added `Email` type in `src/shared/email.rs` for email format validation - Added `DomainName` type in `src/shared/domain_name.rs` for domain validation ### Phase 3: Template Rendering Integration ✅ - Created `RenderCaddyTemplatesStep` for template rendering - Created `DeployCaddyConfigStep` for Ansible deployment - Created `deploy-caddy-config.yml` Ansible playbook - Added `RenderCaddyTemplates` and `DeployCaddyConfigToRemote` to `ReleaseStep` enum - Integrated `CaddyContext` into Docker Compose template rendering - Added `CaddyConfigDeployment` error variant with actionable help text ### Phase 4: Security Workflow Updates ✅ - Added `caddy:2.10` to Docker security scan workflow matrix - Added SARIF upload step for Caddy scan results - Updated security scan documentation ### Phase 5: Documentation ✅ - Created `docs/user-guide/services/https.md` with complete HTTPS setup guide - Updated `docs/user-guide/services/README.md` with HTTPS service entry - Updated `docs/user-guide/services/grafana.md` with TLS proxy fields documentation - Regenerated JSON schema (`schemas/environment-config.json`) ### Phase 6: E2E Testing ✅ (Manual) Manual E2E testing verified: - ✅ HTTPS endpoints working for API, Grafana, and HTTP trackers - ✅ HTTP→HTTPS redirect (308 Permanent Redirect) - ✅ HTTP/2 and HTTP/3 enabled - ✅ Caddy Local CA for `.local` domains - ✅ X-Forwarded-For header explicitly set for HTTP trackers - ✅ Announce request through Caddy returns valid bencode response ### Phase 7: Schema Generation ✅ - Regenerated JSON schema with HTTPS configuration - Schema includes `https`, `domain`, `use_tls_proxy` fields ### Phase 8: ADRs (Decision Documentation) ✅ Three ADRs created: 1. **Caddy for TLS Termination** (`docs/decisions/caddy-for-tls-termination.md`) - Why Caddy was chosen over Pingoo/nginx 2. **Per-Service TLS Configuration** (`docs/decisions/per-service-tls-configuration.md`) - Why `domain + use_tls_proxy` pattern was chosen, including naming rationale 3. **Uniform HTTP Tracker TLS Requirement** (`docs/decisions/uniform-http-tracker-tls-requirement.md`) - Why all HTTP trackers must use same TLS setting ## What's Remaining - [ ] **Phase 6**: Automated E2E tests (currently only manual testing) ## Configuration Example ```json { "https": { "admin_email": "admin@example.com", "use_staging": true }, "tracker": { "http_api": { "bind_address": "0.0.0.0:1212", "admin_token": "secret", "domain": "api.tracker.local", "use_tls_proxy": true }, "http_trackers": [ { "bind_address": "0.0.0.0:7070", "domain": "http1.tracker.local", "use_tls_proxy": true } ] }, "grafana": { "admin_user": "admin", "admin_password": "admin", "domain": "grafana.tracker.local", "use_tls_proxy": true } } ``` ## Testing - All pre-commit checks pass - Manual E2E test results documented in [docs/issues/272-add-https-support-with-caddy.md](docs/issues/272-add-https-support-with-caddy.md) - X-Forwarded-For header verified on deployed environment ## Key Design Decisions 1. **X-Forwarded-For only for HTTP trackers**: The explicit `header_up X-Forwarded-For {remote_host}` is only set for HTTP trackers (not API, health check, or Grafana) because it's critical only for peer IP tracking in BitTorrent swarms. 2. **`use_tls_proxy` naming**: Named to avoid confusion with tracker's native TLS support (`TslConfig`), reserving namespace for future `use_native_tls` option. ## Notes for Reviewers Key files to review: - `templates/caddy/Caddyfile.tera` - Caddy template with explicit X-Forwarded-For for HTTP trackers - `src/application/command_handlers/create/config/https.rs` - HTTPS configuration DTOs - `src/application/command_handlers/release/handler.rs` - Release workflow integration - `src/infrastructure/templating/caddy/` - Caddy template rendering - `docs/decisions/` - Three new ADRs documenting architectural decisions ACKs for top commit: josecelano: ACK 28336e0 Tree-SHA512: f3ef875536b9b8841bc6e9e21b93cfc161c32e09ad90b073cad022da7b9e50c9ec2dbed46d6f1cde1448cfb04d652a75fe5b536278b8860fe0e10b42cbdab840
2 parents 0ffed4c + 28336e0 commit 348c8ce

File tree

106 files changed

+10713
-941
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

106 files changed

+10713
-941
lines changed

.github/workflows/docker-security-scan.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ jobs:
107107
- mysql:8.0
108108
- grafana/grafana:11.4.0
109109
- prom/prometheus:v3.0.1
110+
- caddy:2.10
110111

111112
steps:
112113
- name: Display vulnerabilities (table format)
@@ -219,3 +220,11 @@ jobs:
219220
sarif_file: sarif-third-party-prom-prometheus-v3.0.1-${{ github.run_id }}/trivy.sarif
220221
category: docker-third-party-prom-prometheus-v3.0.1
221222
continue-on-error: true
223+
224+
- name: Upload third-party caddy SARIF
225+
if: always()
226+
uses: github/codeql-action/upload-sarif@v4
227+
with:
228+
sarif_file: sarif-third-party-caddy-2.10-${{ github.run_id }}/trivy.sarif
229+
category: docker-third-party-caddy-2.10
230+
continue-on-error: true

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ repomix-output.xml
5252
# Rust build artifacts
5353
target/
5454
Cargo.lock
55+
rustc-ice-*.txt
5556

5657
# Template build directory (runtime-generated configs)
5758
build/

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ tracing-appender = "0.2"
6565
tracing-subscriber = { version = "0.3", features = [ "env-filter", "json", "fmt" ] }
6666
url = { version = "2.0", features = [ "serde" ] }
6767
uuid = { version = "1.0", features = [ "v4", "serde" ] }
68+
email_address = "0.2.9"
6869

6970
[dev-dependencies]
7071
rstest = "0.26"
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Caddy Templates
2+
3+
Documentation for Caddy reverse proxy templates used for automatic HTTPS with Let's Encrypt.
4+
5+
## Overview
6+
7+
Caddy provides automatic HTTPS termination for HTTP services. The template generates a Caddyfile
8+
based on which services have TLS configured in the environment configuration.
9+
10+
## Template Files
11+
12+
### `templates/caddy/Caddyfile.tera`
13+
14+
Dynamic Tera template that generates a Caddyfile. Only services with TLS configured
15+
will have entries in the generated file.
16+
17+
## Template Variables
18+
19+
The template receives a `CaddyContext` with the following structure:
20+
21+
| Variable | Type | Description |
22+
| --------------- | -------------------------- | --------------------------------------------------- |
23+
| `admin_email` | `String` | Admin email for Let's Encrypt notifications |
24+
| `use_staging` | `bool` | Use Let's Encrypt staging environment (for testing) |
25+
| `tracker_api` | `Option<ServiceTlsConfig>` | TLS config for Tracker API (if enabled) |
26+
| `http_trackers` | `Vec<ServiceTlsConfig>` | TLS configs for HTTP trackers (only those with TLS) |
27+
| `grafana` | `Option<ServiceTlsConfig>` | TLS config for Grafana (if enabled) |
28+
29+
### `ServiceTlsConfig` Structure
30+
31+
| Field | Type | Description |
32+
| -------- | -------- | --------------------------------------------- |
33+
| `domain` | `String` | Domain name for this service |
34+
| `port` | `u16` | Port number (pre-extracted from bind_address) |
35+
36+
## Context Data Preparation
37+
38+
Following the project's [Context Data Preparation Pattern](template-system-architecture.md#-context-data-preparation-pattern),
39+
all data is pre-processed in Rust before being passed to the template:
40+
41+
- **Ports are extracted** from `bind_address` strings (e.g., `"0.0.0.0:7070"``7070`)
42+
- **Only TLS-enabled services** are included in the context
43+
- **The template receives ready-to-use values** - no parsing required
44+
45+
### Example: Port Extraction in Rust
46+
47+
```rust
48+
// In the context builder (Rust code)
49+
let http_api_port = tracker_config.http_api.bind_address.port(); // u16
50+
51+
// Context passed to template
52+
CaddyContext {
53+
tracker_api: Some(ServiceTlsConfig {
54+
domain: "api.example.com".to_string(),
55+
port: http_api_port, // Already extracted as u16
56+
}),
57+
// ...
58+
}
59+
```
60+
61+
```tera
62+
{# In the template - receives ready-to-use port #}
63+
{{ tracker_api.domain }} {
64+
reverse_proxy tracker:{{ tracker_api.port }}
65+
}
66+
```
67+
68+
## Conditional Rendering
69+
70+
The template uses Tera conditionals to include only services with TLS configured:
71+
72+
- `{% if tracker_api %}` - Include API block only if TLS is enabled for API
73+
- `{% for http_tracker in http_trackers %}` - Iterate only over trackers with TLS
74+
- `{% if grafana %}` - Include Grafana block only if TLS is enabled
75+
76+
Services without TLS configuration remain accessible via HTTP on their configured ports.
77+
78+
## Let's Encrypt Environments
79+
80+
### Production (Default)
81+
82+
Uses the production Let's Encrypt API. Certificates are trusted by all browsers.
83+
84+
**Rate limits** (production):
85+
86+
- 50 certificates per registered domain per week
87+
- 5 duplicate certificates per week
88+
89+
### Staging
90+
91+
Set `use_staging: true` in your environment configuration for testing:
92+
93+
```json
94+
{
95+
"https": {
96+
"admin_email": "admin@example.com",
97+
"use_staging": true
98+
}
99+
}
100+
```
101+
102+
This configures Caddy to use `https://acme-staging-v02.api.letsencrypt.org/directory`.
103+
104+
**Important notes about staging**:
105+
106+
- Staging certificates will show browser warnings (not trusted by browsers)
107+
- Use staging only for testing the HTTPS flow, not for production
108+
- Staging has much higher rate limits than production
109+
110+
## Docker Compose Integration
111+
112+
When Caddy is enabled (any service has TLS configured), the following is added to `docker-compose.yml`:
113+
114+
- **Caddy service**: Runs `caddy:2.10` image with ports 80, 443, and 443/udp (HTTP/3)
115+
- **proxy_network**: Network connecting Caddy to services it proxies
116+
- **caddy_data volume**: Persists TLS certificates (critical for avoiding rate limits)
117+
- **caddy_config volume**: Persists Caddy configuration cache
118+
119+
Services with TLS enabled are automatically connected to the `proxy_network`.
120+
121+
## Caddyfile Syntax Notes
122+
123+
- **Caddy requires TABS for indentation**, not spaces
124+
- The template uses actual tab characters for proper Caddyfile formatting
125+
- Global options are enclosed in `{ }` at the top of the file
126+
- Site blocks use the format `domain.com { ... }`
127+
128+
## Related Documentation
129+
130+
- [Template System Architecture](template-system-architecture.md) - Overall template system design
131+
- [Context Data Preparation Pattern](template-system-architecture.md#-context-data-preparation-pattern) - How to prepare data for templates
132+
- [Tera Template Guidelines](tera.md) - Tera syntax and best practices
133+
- [HTTPS Setup Guide](../../user-guide/https-setup.md) - User documentation (coming soon)

docs/contributing/templates/template-system-architecture.md

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,178 @@ impl DockerComposeProjectGenerator {
245245
- Template syntax validation and error handling
246246
- Strongly typed wrappers prevent runtime template errors
247247

248+
## 🎯 Context Data Preparation Pattern
249+
250+
**Templates should receive only pre-processed, ready-to-use data.** All data transformation, parsing, and extraction must happen in Rust code when building the Context, not in the template.
251+
252+
### Core Principle
253+
254+
The Context acts as a **presentation layer** for templates:
255+
256+
- **Rust code** does the heavy lifting: parsing, validation, extraction, conversion
257+
- **Templates** only do simple variable interpolation and conditional rendering
258+
- **No custom Tera filters** for data transformation (e.g., no `extract_port` filter)
259+
260+
### Why This Matters
261+
262+
1. **Testability**: Rust transformations are unit-testable; template logic is harder to test
263+
2. **Type Safety**: Rust catches errors at compile time; template errors appear at runtime
264+
3. **Simplicity**: Templates remain simple and readable
265+
4. **Consistency**: All data preparation follows the same pattern
266+
5. **Debugging**: Errors in data preparation have clear stack traces
267+
268+
### Example: Port Extraction
269+
270+
**❌ WRONG - Processing in template:**
271+
272+
```tera
273+
{# Template tries to extract port from bind_address #}
274+
reverse_proxy tracker:{{ tracker.http_api.bind_address | extract_port }}
275+
```
276+
277+
Problems:
278+
279+
- Requires custom Tera filter registration
280+
- Error handling in templates is awkward
281+
- Template becomes coupled to data structure
282+
283+
**✅ CORRECT - Pre-processed in Rust:**
284+
285+
```rust
286+
// Context struct with ready-to-use values
287+
pub struct CaddyContext {
288+
pub http_api_port: u16, // Already extracted from bind_address
289+
pub http_api_domain: String,
290+
// ...
291+
}
292+
293+
// Port extraction happens in Rust when building context
294+
impl CaddyContext {
295+
pub fn from_config(config: &TrackerConfig) -> Self {
296+
Self {
297+
http_api_port: config.http_api.bind_address.port(), // Extraction here
298+
http_api_domain: config.http_api.tls.as_ref()
299+
.map(|tls| tls.domain.clone())
300+
.unwrap_or_default(),
301+
}
302+
}
303+
}
304+
```
305+
306+
```tera
307+
{# Template receives ready-to-use port number #}
308+
reverse_proxy tracker:{{ http_api_port }}
309+
```
310+
311+
### Example: Conditional Data
312+
313+
**❌ WRONG - Complex logic in template:**
314+
315+
```tera
316+
{% if tracker.http_api.tls is defined and tracker.http_api.tls.domain != "" %}
317+
{{ tracker.http_api.tls.domain }} {
318+
reverse_proxy tracker:{{ tracker.http_api.bind_address | extract_port }}
319+
}
320+
{% endif %}
321+
```
322+
323+
**✅ CORRECT - Rust prepares filtered list:**
324+
325+
```rust
326+
// Context contains only services that need rendering
327+
pub struct CaddyContext {
328+
pub services: Vec<CaddyService>, // Only TLS-enabled services included
329+
}
330+
331+
pub struct CaddyService {
332+
pub domain: String,
333+
pub upstream_port: u16,
334+
}
335+
336+
// Filtering happens in Rust
337+
impl CaddyContext {
338+
pub fn from_config(config: &EnvironmentConfig) -> Self {
339+
let mut services = Vec::new();
340+
341+
// Only add if TLS is configured
342+
if let Some(tls) = &config.tracker.http_api.tls {
343+
services.push(CaddyService {
344+
domain: tls.domain.clone(),
345+
upstream_port: config.tracker.http_api.bind_address.port(),
346+
});
347+
}
348+
349+
Self { services }
350+
}
351+
}
352+
```
353+
354+
```tera
355+
{# Template simply iterates pre-filtered list #}
356+
{% for service in services %}
357+
{{ service.domain }} {
358+
reverse_proxy tracker:{{ service.upstream_port }}
359+
}
360+
{% endfor %}
361+
```
362+
363+
### Data Flow Summary
364+
365+
```text
366+
┌──────────────────┐ ┌───────────────────┐ ┌──────────────────┐
367+
│ Domain Config │────▶│ Context Builder │────▶│ Template │
368+
│ (raw data) │ │ (Rust processing) │ │ (simple output) │
369+
└──────────────────┘ └───────────────────┘ └──────────────────┘
370+
371+
┌────────────┼────────────┐
372+
│ │ │
373+
Parse ports Filter by Convert types
374+
condition to strings
375+
```
376+
377+
### Guidelines for Context Design
378+
379+
1. **Flatten nested structures**: If template needs `config.tracker.http_api.bind_address.port()`, provide `http_api_port: u16`
380+
2. **Pre-filter collections**: If template only renders TLS-enabled services, filter in Rust first
381+
3. **Use primitive types**: Prefer `String`, `u16`, `bool` over complex domain types
382+
4. **Handle optionals in Rust**: Don't pass `Option<T>` to templates; provide defaults or filter out
383+
5. **Name for template clarity**: Use names like `http_api_port` not `bind_address_port_number`
384+
385+
## 📁 Templates Directory Organization
386+
387+
The `templates/` directory should contain **only template files** (`.tera` files and static configuration files). Documentation about templates should be placed in `docs/contributing/templates/`.
388+
389+
### DO ✅
390+
391+
- Place template files (`.tera`, `.yml`, `.toml`, etc.) in `templates/<service>/`
392+
- Add comments directly in template files to explain template-specific details
393+
- Create documentation in `docs/contributing/templates/<service>.md` for detailed explanations
394+
395+
### DON'T ❌
396+
397+
- ❌ Add `README.md` files in `templates/` subdirectories
398+
- ❌ Add documentation files in the `templates/` directory structure
399+
- ❌ Mix documentation with template source files
400+
401+
### Service Documentation Location
402+
403+
| Service | Templates Location | Documentation Location |
404+
| -------------- | --------------------------- | ----------------------------------------------- |
405+
| Ansible | `templates/ansible/` | `docs/contributing/templates/ansible.md` |
406+
| Caddy | `templates/caddy/` | `docs/contributing/templates/caddy.md` |
407+
| Docker Compose | `templates/docker-compose/` | `docs/contributing/templates/docker-compose.md` |
408+
| Grafana | `templates/grafana/` | `docs/contributing/templates/grafana.md` |
409+
| Prometheus | `templates/prometheus/` | `docs/contributing/templates/prometheus.md` |
410+
| Tofu | `templates/tofu/` | `docs/contributing/templates/tofu.md` |
411+
| Tracker | `templates/tracker/` | `docs/contributing/templates/tracker.md` |
412+
413+
### Rationale
414+
415+
1. **Clean separation**: Template files are source code; documentation is separate
416+
2. **Embedded templates**: The `templates/` directory is embedded in the binary - documentation files would unnecessarily increase binary size
417+
3. **Consistency**: All documentation lives in `docs/`, not scattered across the codebase
418+
4. **Discoverability**: Contributors know to look in `docs/contributing/templates/` for template documentation
419+
248420
## ⚠️ Important Behaviors
249421

250422
### Template Persistence

docs/decisions/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ This directory contains architectural decision records for the Torrust Tracker D
66

77
| Status | Date | Decision | Summary |
88
| ------------- | ---------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
9+
| ✅ Accepted | 2026-01-20 | [Caddy for TLS Termination](./caddy-for-tls-termination.md) | Use Caddy v2.10 as TLS proxy for automatic HTTPS with WebSocket support |
10+
| ✅ Accepted | 2026-01-20 | [Per-Service TLS Configuration](./per-service-tls-configuration.md) | Use domain + use_tls_proxy fields instead of nested tls section for explicit TLS opt-in |
11+
| ✅ Accepted | 2026-01-20 | [Uniform HTTP Tracker TLS Requirement](./uniform-http-tracker-tls-requirement.md) | All HTTP trackers must use same TLS setting due to tracker's global on_reverse_proxy |
912
| ✅ Accepted | 2026-01-10 | [Hetzner SSH Key Dual Injection Pattern](./hetzner-ssh-key-dual-injection.md) | Use both OpenTofu SSH key and cloud-init for debugging capability with manual hardening |
1013
| ✅ Accepted | 2026-01-10 | [Configuration and Data Directories as Secrets](./configuration-directories-as-secrets.md) | Treat envs/, data/, build/ as secrets; no env var injection; users secure via permissions |
1114
| ✅ Accepted | 2026-01-07 | [Configuration DTO Layer Placement](./configuration-dto-layer-placement.md) | Keep configuration DTOs in application layer, not domain; defer package extraction |

0 commit comments

Comments
 (0)