Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
900aeac
docs: [#272] clarify context data preparation pattern for Tera templates
josecelano Jan 13, 2026
df19401
feat: [#272] add Caddy templates for HTTPS support
josecelano Jan 13, 2026
3bbaea1
feat: [#272] add Caddy template rendering infrastructure
josecelano Jan 13, 2026
7882917
feat: [#272] add HTTPS support with Caddy for all HTTP services
josecelano Jan 13, 2026
ef906fc
refactor: [#272] simplify docker-compose template by pre-computing TL…
josecelano Jan 14, 2026
180f364
refactor: [#272] use YAML anchors in docker-compose template for DRY …
josecelano Jan 14, 2026
1b69af4
refactor: [#272] move tracker networks logic from template to Rust
josecelano Jan 14, 2026
a93596c
docs: [#272] add CLI command HTTPS compatibility phase and fix VM fil…
josecelano Jan 14, 2026
02a83be
refactor: [#272] move Prometheus and Grafana networks logic from Tera…
josecelano Jan 14, 2026
526e7ab
refactor: [#272] rename ports module to tracker for service consistency
josecelano Jan 14, 2026
d508cc7
refactor: [#272] separate Caddy contexts for Caddyfile and docker-com…
josecelano Jan 14, 2026
56ad1dc
refactor: [#272] add MysqlServiceConfig for MySQL service network con…
josecelano Jan 14, 2026
011cd8c
refactor: [#272] improve docker-compose template whitespace handling
josecelano Jan 14, 2026
c8236eb
chore: [#272] add rustc-ice files to gitignore
josecelano Jan 14, 2026
704f153
feat: [#272] add Caddy to Docker security scan workflow
josecelano Jan 14, 2026
c1c194a
feat: [#272] update show command to display HTTPS-enabled services
josecelano Jan 15, 2026
1855058
docs: [#272] add tasks 7.3 and 7.4 for health check TLS and localhost…
josecelano Jan 15, 2026
f143303
feat: [#272] add HTTPS/TLS support for health check API
josecelano Jan 15, 2026
d21f313
docs: [#272] clarify task 7.4 implementation notes
josecelano Jan 15, 2026
5334429
feat: [#272] Handle localhost-bound services in show command and vali…
josecelano Jan 15, 2026
a857e07
docs: [#272] Add subtask 7.5 for use_tls_proxy configuration refactor
josecelano Jan 16, 2026
bf73227
docs: [#272] Document tracker on_reverse_proxy global setting limitation
josecelano Jan 16, 2026
d796db7
docs: [#272] Add reproduction steps for on_reverse_proxy issue
josecelano Jan 16, 2026
80b4b32
refactor: [#272] replace TlsSection with domain + use_tls_proxy for H…
josecelano Jan 19, 2026
3662aff
refactor: [#272] Replace tls with domain+use_tls_proxy for HTTP API
josecelano Jan 19, 2026
55dfa94
refactor: [#272] Replace tls with domain+use_tls_proxy for Health Che…
josecelano Jan 19, 2026
01da06e
refactor: [#272] Replace tls with domain+use_tls_proxy for Grafana
josecelano Jan 19, 2026
c7e0dc4
refactor: [#272] Remove unused TlsSection and domain::tls module
josecelano Jan 19, 2026
3815f55
feat: [#272] Add HTTPS support to test command with ServiceEndpoint type
josecelano Jan 19, 2026
2426650
refactor: [#272] Refactor ServiceEndpoint to store validated URL and …
josecelano Jan 19, 2026
bf5df90
docs: [#272] Add HTTPS user documentation
josecelano Jan 19, 2026
310f93f
docs: [#272] Add revised Phase 6 implementation plan for HTTPS E2E te…
josecelano Jan 20, 2026
6948342
docs: [#272] Explain why test command cannot work in Docker-based E2E…
josecelano Jan 20, 2026
8b6e361
docs: [#272] Add ADRs for HTTPS implementation decisions (Phase 9)
josecelano Jan 20, 2026
e464aca
feat: [#272] Explicitly set X-Forwarded-For header in Caddy reverse p…
josecelano Jan 20, 2026
28336e0
refactor: [#272] Set X-Forwarded-For header only for HTTP trackers
josecelano Jan 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/docker-security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ jobs:
- mysql:8.0
- grafana/grafana:11.4.0
- prom/prometheus:v3.0.1
- caddy:2.10

steps:
- name: Display vulnerabilities (table format)
Expand Down Expand Up @@ -219,3 +220,11 @@ jobs:
sarif_file: sarif-third-party-prom-prometheus-v3.0.1-${{ github.run_id }}/trivy.sarif
category: docker-third-party-prom-prometheus-v3.0.1
continue-on-error: true

- name: Upload third-party caddy SARIF
if: always()
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: sarif-third-party-caddy-2.10-${{ github.run_id }}/trivy.sarif
category: docker-third-party-caddy-2.10
continue-on-error: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ repomix-output.xml
# Rust build artifacts
target/
Cargo.lock
rustc-ice-*.txt

# Template build directory (runtime-generated configs)
build/
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = [ "env-filter", "json", "fmt" ] }
url = { version = "2.0", features = [ "serde" ] }
uuid = { version = "1.0", features = [ "v4", "serde" ] }
email_address = "0.2.9"

[dev-dependencies]
rstest = "0.26"
Expand Down
133 changes: 133 additions & 0 deletions docs/contributing/templates/caddy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Caddy Templates

Documentation for Caddy reverse proxy templates used for automatic HTTPS with Let's Encrypt.

## Overview

Caddy provides automatic HTTPS termination for HTTP services. The template generates a Caddyfile
based on which services have TLS configured in the environment configuration.

## Template Files

### `templates/caddy/Caddyfile.tera`

Dynamic Tera template that generates a Caddyfile. Only services with TLS configured
will have entries in the generated file.

## Template Variables

The template receives a `CaddyContext` with the following structure:

| Variable | Type | Description |
| --------------- | -------------------------- | --------------------------------------------------- |
| `admin_email` | `String` | Admin email for Let's Encrypt notifications |
| `use_staging` | `bool` | Use Let's Encrypt staging environment (for testing) |
| `tracker_api` | `Option<ServiceTlsConfig>` | TLS config for Tracker API (if enabled) |
| `http_trackers` | `Vec<ServiceTlsConfig>` | TLS configs for HTTP trackers (only those with TLS) |
| `grafana` | `Option<ServiceTlsConfig>` | TLS config for Grafana (if enabled) |

### `ServiceTlsConfig` Structure

| Field | Type | Description |
| -------- | -------- | --------------------------------------------- |
| `domain` | `String` | Domain name for this service |
| `port` | `u16` | Port number (pre-extracted from bind_address) |

## Context Data Preparation

Following the project's [Context Data Preparation Pattern](template-system-architecture.md#-context-data-preparation-pattern),
all data is pre-processed in Rust before being passed to the template:

- **Ports are extracted** from `bind_address` strings (e.g., `"0.0.0.0:7070"` → `7070`)
- **Only TLS-enabled services** are included in the context
- **The template receives ready-to-use values** - no parsing required

### Example: Port Extraction in Rust

```rust
// In the context builder (Rust code)
let http_api_port = tracker_config.http_api.bind_address.port(); // u16

// Context passed to template
CaddyContext {
tracker_api: Some(ServiceTlsConfig {
domain: "api.example.com".to_string(),
port: http_api_port, // Already extracted as u16
}),
// ...
}
```

```tera
{# In the template - receives ready-to-use port #}
{{ tracker_api.domain }} {
reverse_proxy tracker:{{ tracker_api.port }}
}
```

## Conditional Rendering

The template uses Tera conditionals to include only services with TLS configured:

- `{% if tracker_api %}` - Include API block only if TLS is enabled for API
- `{% for http_tracker in http_trackers %}` - Iterate only over trackers with TLS
- `{% if grafana %}` - Include Grafana block only if TLS is enabled

Services without TLS configuration remain accessible via HTTP on their configured ports.

## Let's Encrypt Environments

### Production (Default)

Uses the production Let's Encrypt API. Certificates are trusted by all browsers.

**Rate limits** (production):

- 50 certificates per registered domain per week
- 5 duplicate certificates per week

### Staging

Set `use_staging: true` in your environment configuration for testing:

```json
{
"https": {
"admin_email": "admin@example.com",
"use_staging": true
}
}
```

This configures Caddy to use `https://acme-staging-v02.api.letsencrypt.org/directory`.

**Important notes about staging**:

- Staging certificates will show browser warnings (not trusted by browsers)
- Use staging only for testing the HTTPS flow, not for production
- Staging has much higher rate limits than production

## Docker Compose Integration

When Caddy is enabled (any service has TLS configured), the following is added to `docker-compose.yml`:

- **Caddy service**: Runs `caddy:2.10` image with ports 80, 443, and 443/udp (HTTP/3)
- **proxy_network**: Network connecting Caddy to services it proxies
- **caddy_data volume**: Persists TLS certificates (critical for avoiding rate limits)
- **caddy_config volume**: Persists Caddy configuration cache

Services with TLS enabled are automatically connected to the `proxy_network`.

## Caddyfile Syntax Notes

- **Caddy requires TABS for indentation**, not spaces
- The template uses actual tab characters for proper Caddyfile formatting
- Global options are enclosed in `{ }` at the top of the file
- Site blocks use the format `domain.com { ... }`

## Related Documentation

- [Template System Architecture](template-system-architecture.md) - Overall template system design
- [Context Data Preparation Pattern](template-system-architecture.md#-context-data-preparation-pattern) - How to prepare data for templates
- [Tera Template Guidelines](tera.md) - Tera syntax and best practices
- [HTTPS Setup Guide](../../user-guide/https-setup.md) - User documentation (coming soon)
172 changes: 172 additions & 0 deletions docs/contributing/templates/template-system-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,178 @@ impl DockerComposeProjectGenerator {
- Template syntax validation and error handling
- Strongly typed wrappers prevent runtime template errors

## 🎯 Context Data Preparation Pattern

**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.

### Core Principle

The Context acts as a **presentation layer** for templates:

- **Rust code** does the heavy lifting: parsing, validation, extraction, conversion
- **Templates** only do simple variable interpolation and conditional rendering
- **No custom Tera filters** for data transformation (e.g., no `extract_port` filter)

### Why This Matters

1. **Testability**: Rust transformations are unit-testable; template logic is harder to test
2. **Type Safety**: Rust catches errors at compile time; template errors appear at runtime
3. **Simplicity**: Templates remain simple and readable
4. **Consistency**: All data preparation follows the same pattern
5. **Debugging**: Errors in data preparation have clear stack traces

### Example: Port Extraction

**❌ WRONG - Processing in template:**

```tera
{# Template tries to extract port from bind_address #}
reverse_proxy tracker:{{ tracker.http_api.bind_address | extract_port }}
```

Problems:

- Requires custom Tera filter registration
- Error handling in templates is awkward
- Template becomes coupled to data structure

**✅ CORRECT - Pre-processed in Rust:**

```rust
// Context struct with ready-to-use values
pub struct CaddyContext {
pub http_api_port: u16, // Already extracted from bind_address
pub http_api_domain: String,
// ...
}

// Port extraction happens in Rust when building context
impl CaddyContext {
pub fn from_config(config: &TrackerConfig) -> Self {
Self {
http_api_port: config.http_api.bind_address.port(), // Extraction here
http_api_domain: config.http_api.tls.as_ref()
.map(|tls| tls.domain.clone())
.unwrap_or_default(),
}
}
}
```

```tera
{# Template receives ready-to-use port number #}
reverse_proxy tracker:{{ http_api_port }}
```

### Example: Conditional Data

**❌ WRONG - Complex logic in template:**

```tera
{% if tracker.http_api.tls is defined and tracker.http_api.tls.domain != "" %}
{{ tracker.http_api.tls.domain }} {
reverse_proxy tracker:{{ tracker.http_api.bind_address | extract_port }}
}
{% endif %}
```

**✅ CORRECT - Rust prepares filtered list:**

```rust
// Context contains only services that need rendering
pub struct CaddyContext {
pub services: Vec<CaddyService>, // Only TLS-enabled services included
}

pub struct CaddyService {
pub domain: String,
pub upstream_port: u16,
}

// Filtering happens in Rust
impl CaddyContext {
pub fn from_config(config: &EnvironmentConfig) -> Self {
let mut services = Vec::new();

// Only add if TLS is configured
if let Some(tls) = &config.tracker.http_api.tls {
services.push(CaddyService {
domain: tls.domain.clone(),
upstream_port: config.tracker.http_api.bind_address.port(),
});
}

Self { services }
}
}
```

```tera
{# Template simply iterates pre-filtered list #}
{% for service in services %}
{{ service.domain }} {
reverse_proxy tracker:{{ service.upstream_port }}
}
{% endfor %}
```

### Data Flow Summary

```text
┌──────────────────┐ ┌───────────────────┐ ┌──────────────────┐
│ Domain Config │────▶│ Context Builder │────▶│ Template │
│ (raw data) │ │ (Rust processing) │ │ (simple output) │
└──────────────────┘ └───────────────────┘ └──────────────────┘
┌────────────┼────────────┐
│ │ │
Parse ports Filter by Convert types
condition to strings
```

### Guidelines for Context Design

1. **Flatten nested structures**: If template needs `config.tracker.http_api.bind_address.port()`, provide `http_api_port: u16`
2. **Pre-filter collections**: If template only renders TLS-enabled services, filter in Rust first
3. **Use primitive types**: Prefer `String`, `u16`, `bool` over complex domain types
4. **Handle optionals in Rust**: Don't pass `Option<T>` to templates; provide defaults or filter out
5. **Name for template clarity**: Use names like `http_api_port` not `bind_address_port_number`

## 📁 Templates Directory Organization

The `templates/` directory should contain **only template files** (`.tera` files and static configuration files). Documentation about templates should be placed in `docs/contributing/templates/`.

### DO ✅

- Place template files (`.tera`, `.yml`, `.toml`, etc.) in `templates/<service>/`
- Add comments directly in template files to explain template-specific details
- Create documentation in `docs/contributing/templates/<service>.md` for detailed explanations

### DON'T ❌

- ❌ Add `README.md` files in `templates/` subdirectories
- ❌ Add documentation files in the `templates/` directory structure
- ❌ Mix documentation with template source files

### Service Documentation Location

| Service | Templates Location | Documentation Location |
| -------------- | --------------------------- | ----------------------------------------------- |
| Ansible | `templates/ansible/` | `docs/contributing/templates/ansible.md` |
| Caddy | `templates/caddy/` | `docs/contributing/templates/caddy.md` |
| Docker Compose | `templates/docker-compose/` | `docs/contributing/templates/docker-compose.md` |
| Grafana | `templates/grafana/` | `docs/contributing/templates/grafana.md` |
| Prometheus | `templates/prometheus/` | `docs/contributing/templates/prometheus.md` |
| Tofu | `templates/tofu/` | `docs/contributing/templates/tofu.md` |
| Tracker | `templates/tracker/` | `docs/contributing/templates/tracker.md` |

### Rationale

1. **Clean separation**: Template files are source code; documentation is separate
2. **Embedded templates**: The `templates/` directory is embedded in the binary - documentation files would unnecessarily increase binary size
3. **Consistency**: All documentation lives in `docs/`, not scattered across the codebase
4. **Discoverability**: Contributors know to look in `docs/contributing/templates/` for template documentation

## ⚠️ Important Behaviors

### Template Persistence
Expand Down
3 changes: 3 additions & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ This directory contains architectural decision records for the Torrust Tracker D

| Status | Date | Decision | Summary |
| ------------- | ---------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| ✅ 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 |
| ✅ 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 |
| ✅ 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 |
| ✅ 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 |
| ✅ 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 |
| ✅ Accepted | 2026-01-07 | [Configuration DTO Layer Placement](./configuration-dto-layer-placement.md) | Keep configuration DTOs in application layer, not domain; defer package extraction |
Expand Down
Loading
Loading