Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 2 additions & 13 deletions docs/analysis/security/docker-network-segmentation-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ graph LR
subgraph VM["Docker Host VM"]
subgraph PublicServices["Publicly Exposed Services"]
Tracker[Tracker<br/>Ports: 6969/udp, 7070, 1212]
Grafana[Grafana<br/>Port: 3100]
Grafana[Grafana<br/>Port: 3000]
end

subgraph InternalServices["Internal Services<br/>(No external exposure)"]
Expand Down Expand Up @@ -107,12 +107,10 @@ graph LR
**IMPORTANT**: The Tracker service stores MySQL database credentials in multiple locations:

1. **Environment Variables**:

- `MYSQL_ROOT_PASSWORD` (injected from `.env` file)
- Other database connection parameters

2. **Configuration Files**:

- `torrust-tracker.toml` or equivalent configuration may contain database credentials
- Mounted at `./storage/tracker/etc:/etc/torrust/tracker:Z`

Expand Down Expand Up @@ -265,24 +263,20 @@ ports:
**Answer**: **YES - Network segmentation still provides significant value**:

1. **Isolates Grafana from Database**:

- Grafana has NO MySQL credentials
- Compromising Grafana alone cannot access database
- Attacker must compromise BOTH Grafana AND Tracker to reach MySQL

2. **Reduces Attack Surface**:

- Only 1 service (Tracker) can access MySQL, not 3 services
- Fewer pathways to database = fewer opportunities for attackers

3. **Defense in Depth**:

- Network segmentation is ONE layer
- Even if attacker has credentials, they need network access
- Grafana compromise doesn't automatically give MySQL network access

4. **Limits Lateral Movement**:

- Compromised Grafana cannot pivot to MySQL
- Compromised Prometheus cannot pivot to MySQL
- Attack chain must go: Grafana → Tracker → MySQL (3 steps, not 2)
Expand All @@ -293,13 +287,11 @@ ports:

**Revised Risk Matrix WITH Network Segmentation**:

1. **Attacker compromises Grafana** (public service at port 3100)

1. **Attacker compromises Grafana** (public service at port 3000)
- Exploits a Grafana vulnerability (CVE in grafana/grafana image)
- Gains shell access inside Grafana container

2. **Lateral movement via shared network**

- From Grafana container: `telnet mysql 3306` - **succeeds** ❌
- Attacker can now attempt MySQL authentication attacks
- Attacker can perform network reconnaissance: `nmap -p- mysql`
Expand Down Expand Up @@ -372,7 +364,6 @@ Beyond network segmentation, implement these credential protection measures:
```

5. **Monitor Credential Access**:

- Log all MySQL connection attempts
- Alert on failed authentication
- Audit container exec commands
Expand Down Expand Up @@ -514,13 +505,11 @@ services:
### Testing Requirements

1. **Positive Tests** (these MUST work):

- Tracker can connect to MySQL
- Prometheus can scrape metrics from Tracker
- Grafana can query Prometheus

2. **Negative Tests** (these MUST fail):

- Grafana CANNOT reach MySQL: `docker exec grafana ping mysql` → fails
- Grafana CANNOT reach Tracker: `docker exec grafana curl tracker:1212` → fails
- Prometheus CANNOT reach MySQL: `docker exec prometheus ping mysql` → fails
Expand Down
1 change: 1 addition & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This directory contains architectural decision records for the Torrust Tracker D

| Status | Date | Decision | Summary |
| ------------- | ---------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| ✅ Accepted | 2026-01-20 | [Grafana Default Port 3000](./grafana-default-port-3000.md) | Use Grafana's default port 3000 instead of 3100 for dedicated VM deployments |
| ✅ 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 |
Expand Down
17 changes: 5 additions & 12 deletions docs/decisions/docker-ufw-firewall-security-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ tracker:

grafana:
ports:
- "3100:3000" # Grafana UI - public
- "3000:3000" # Grafana UI - public

# Host-accessible services - bind to localhost only
prometheus:
Expand Down Expand Up @@ -407,19 +407,16 @@ Docker's built-in security features we rely on:
**Test Strategy**:

1. **External Port Scanning**:

- Use `nmap` or `netstat` from external host to scan VM's public IP
- Verify ONLY expected ports are open (SSH, tracker UDP/HTTP, Grafana)
- Verify internal service ports (Prometheus 9090, MySQL 3306) are NOT accessible

2. **Docker Network Inspection**:

- Query `docker port <container>` to list published ports
- Parse `docker inspect` output to verify port bindings match specification
- Fail if any internal service has unexpected `HostPort` mapping

3. **Service Accessibility Tests**:

- **From external host**: Verify public services respond (tracker, Grafana)
- **From external host**: Verify internal services timeout/refuse (Prometheus, MySQL)
- **From Docker host**: Verify localhost bindings work (`curl 127.0.0.1:9090` succeeds)
Expand Down Expand Up @@ -447,7 +444,7 @@ fn it_should_block_external_access_to_prometheus_when_deployed() {
fn it_should_allow_external_access_to_grafana_when_deployed() {
// 1. Deploy environment with Grafana
// 2. Get VM public IP and configured Grafana port
// 3. Connect to http://<vm-ip>:3100
// 3. Connect to http://<vm-ip>:3000
// 4. Assert successful connection and Grafana login page
}

Expand Down Expand Up @@ -548,26 +545,22 @@ This section addresses critical security questions about the chosen approach.
**✅ Meets Best Practices**:

1. **Defense in Depth**:

- UFW firewall (host level)
- Docker port bindings (service level)
- Docker network segmentation (lateral movement prevention)
- Application authentication (Grafana, Tracker API)

2. **Principle of Least Privilege**:

- Services only have network access to what they need
- Non-root container users
- Minimal capabilities

3. **Security by Default**:

- Internal services not exposed (MySQL, Prometheus)
- Public services explicitly configured
- Localhost-only bindings for debugging services

4. **Auditability**:

- Explicit port bindings documented
- Network topology documented in templates
- Security decisions documented in ADR
Expand Down Expand Up @@ -607,7 +600,7 @@ This section addresses critical security questions about the chosen approach.
fn it_should_detect_unexpected_port_exposure() {
// Run nmap scan from external host
// Parse open ports
// Assert only expected ports open (22, 6969, 7070, 1212, 3100)
// Assert only expected ports open (22, 6969, 7070, 1212, 3000)
// Fail if MySQL (3306) or Prometheus (9090) detected
}
```
Expand All @@ -618,7 +611,7 @@ This section addresses critical security questions about the chosen approach.
#!/bin/bash
# scripts/check-port-exposure.sh

EXPECTED_PORTS="22,6969,7070,1212,3100"
EXPECTED_PORTS="22,6969,7070,1212,3000"
FORBIDDEN_PORTS="3306,9090"

# Check docker ps for published ports
Expand Down Expand Up @@ -651,7 +644,7 @@ This section addresses critical security questions about the chosen approach.
# 6969 (UDP tracker)
# 7070 (HTTP tracker)
# 1212 (REST API)
# 3100 (Grafana)
# 3000 (Grafana)

# Unexpected = security issue
```
Expand Down
100 changes: 100 additions & 0 deletions docs/decisions/grafana-default-port-3000.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Decision: Use Grafana's Default Port 3000 Instead of 3100

## Status

Accepted

## Date

2026-01-20

## Context

The Grafana service configuration was originally copied from the [Torrust Demo project](https://github.com/torrust/torrust-demo), which uses port 3100 on the host to avoid conflicts with other services commonly using port 3000 (like Node.js development servers that typically run on port 3000).

In the Torrust Demo environment, this port offset was necessary because:

- The demo runs multiple services that might conflict with development tools
- Users often have Node.js applications running on port 3000
- The demo is frequently run on developer machines with other services active

However, in the Torrust Tracker Deployer context:

- Deployments target dedicated VM instances, not developer machines
- No other services in our stack use port 3000
- Users expect Grafana to be on its default port
- The port offset (3100 vs 3000) causes confusion when consulting Grafana documentation

## Decision

Change the Grafana host port from `3100` to `3000`, matching Grafana's internal default port.

**Before:**

```yaml
grafana:
ports:
- "3100:3000" # Host:Container
```

**After:**

```yaml
grafana:
ports:
- "3000:3000" # Host:Container (using Grafana's default port)
```

## Rationale

1. **Simplicity**: Using `3000:3000` is more intuitive than `3100:3000` - host and container ports match
2. **No Conflict**: In our deployment context (dedicated VMs), there's no service using port 3000
3. **Documentation Alignment**: Grafana's official documentation uses port 3000; matching this reduces confusion
4. **User Expectations**: Users familiar with Grafana expect it on port 3000
5. **Reduced Cognitive Load**: One less port mapping to remember

## Consequences

### Positive

- More intuitive port configuration (same port inside and outside container)
- Better alignment with Grafana's official documentation
- Simpler mental model for users
- Consistent with how most Grafana installations are configured

### Negative

- **Breaking Change for Existing Deployments**: Users with existing deployments using port 3100 will need to update their bookmarks, scripts, and firewall rules
- **Migration Required**: Existing environments need to be re-released to apply the new port configuration

### Migration Path

For existing deployments:

1. Run `release <environment>` to regenerate docker-compose files with new port
2. Run `run <environment>` to restart services with new configuration
3. Update any bookmarks, scripts, or monitoring that reference port 3100

## Alternatives Considered

### Keep Port 3100

**Pros**: No breaking change for existing users

**Cons**: Perpetuates unnecessary complexity inherited from a different project context

**Decision**: Rejected - the benefits of using the default port outweigh the one-time migration cost

### Make Port Configurable

**Pros**: Maximum flexibility

**Cons**: Adds configuration complexity for a setting rarely needed; violates "convention over configuration" principle

**Decision**: Rejected - not worth the added complexity; users with special needs can fork/modify

## Related

- [Issue #275](https://github.com/torrust/torrust-tracker-deployer/issues/275) - Implementation tracking
- [ADR: Grafana Integration Pattern](grafana-integration-pattern.md) - Original Grafana integration decision
- [Torrust Demo](https://github.com/torrust/torrust-demo) - Original source of the 3100 port choice
Loading
Loading