|
| 1 | +# Security: MySQL port 3306 is publicly exposed in Docker Compose configuration |
| 2 | + |
| 3 | +**Issue**: #277 |
| 4 | +**Parent Epic**: None (standalone security fix) |
| 5 | +**Related**: [docs/decisions/docker-ufw-firewall-security-strategy.md](../decisions/docker-ufw-firewall-security-strategy.md) |
| 6 | + |
| 7 | +## Overview |
| 8 | + |
| 9 | +The MySQL service in the Docker Compose template exposes port 3306 publicly to all network interfaces, allowing external connections to the database. This is a security risk as it enables potential brute-force attacks on MySQL credentials and exposes the database to unauthorized access. |
| 10 | + |
| 11 | +## Goals |
| 12 | + |
| 13 | +- [ ] Remove public exposure of MySQL port 3306 |
| 14 | +- [ ] Maintain MySQL healthcheck functionality |
| 15 | +- [ ] Ensure tracker can still connect to MySQL via internal Docker network |
| 16 | + |
| 17 | +## 🏗️ Architecture Requirements |
| 18 | + |
| 19 | +**DDD Layer**: Infrastructure (templates) |
| 20 | +**Module Path**: `templates/docker-compose/` |
| 21 | +**Pattern**: Template modification |
| 22 | + |
| 23 | +### Module Structure Requirements |
| 24 | + |
| 25 | +- [ ] Follow DDD layer separation (see [docs/codebase-architecture.md](../codebase-architecture.md)) |
| 26 | +- [ ] Respect dependency flow rules (dependencies flow toward domain) |
| 27 | +- [ ] Use appropriate module organization (see [docs/contributing/module-organization.md](../contributing/module-organization.md)) |
| 28 | + |
| 29 | +### Architectural Constraints |
| 30 | + |
| 31 | +- [ ] Template changes only - no Rust code changes required |
| 32 | +- [ ] Must maintain compatibility with existing environment configurations |
| 33 | + |
| 34 | +### Anti-Patterns to Avoid |
| 35 | + |
| 36 | +- ❌ Exposing internal services to public network |
| 37 | +- ❌ Breaking healthcheck functionality |
| 38 | +- ❌ Breaking inter-container communication |
| 39 | + |
| 40 | +## Specifications |
| 41 | + |
| 42 | +### Current State (Problematic) |
| 43 | + |
| 44 | +The MySQL service in `templates/docker-compose/docker-compose.yml.tera` has: |
| 45 | + |
| 46 | +```yaml |
| 47 | +mysql: |
| 48 | + # ... |
| 49 | + ports: |
| 50 | + - "3306:3306" |
| 51 | +``` |
| 52 | +
|
| 53 | +This exposes MySQL to the entire network. Anyone with network access to the VM can connect to the database using the credentials. |
| 54 | +
|
| 55 | +### Verified Issue |
| 56 | +
|
| 57 | +The issue was verified by: |
| 58 | +
|
| 59 | +1. Creating a test environment with MySQL configuration (`envs/manual-mysql-test.json`) |
| 60 | +2. Deploying the environment |
| 61 | +3. Successfully connecting to MySQL from outside the VM: |
| 62 | + |
| 63 | +```bash |
| 64 | +mysql -h 10.140.190.23 -P 3306 -u tracker_user -p tracker_password -e "SELECT 1;" |
| 65 | ++-----------------+ |
| 66 | +| connection_test | |
| 67 | ++-----------------+ |
| 68 | +| 1 | |
| 69 | ++-----------------+ |
| 70 | +``` |
| 71 | + |
| 72 | +### Desired State (Secure) |
| 73 | + |
| 74 | +Remove the `ports` section entirely. The MySQL healthcheck uses `mysqladmin ping -h localhost` which works without port exposure because it runs inside the container. |
| 75 | + |
| 76 | +Unlike Prometheus (which binds to localhost for host validation via `curl http://localhost:9090`), MySQL only needs to be accessible by the tracker container via Docker's internal `database_network`, not from the host. |
| 77 | + |
| 78 | +## Implementation Plan |
| 79 | + |
| 80 | +### Phase 1: Fix Template (5 minutes) |
| 81 | + |
| 82 | +- [ ] Task 1.1: Remove `ports: - "3306:3306"` from MySQL service in `docker-compose.yml.tera` |
| 83 | +- [ ] Task 1.2: Add security comment explaining why port is not exposed |
| 84 | + |
| 85 | +### Phase 2: Verification (15 minutes) |
| 86 | + |
| 87 | +- [ ] Task 2.1: Deploy test environment with MySQL |
| 88 | +- [ ] Task 2.2: Verify MySQL is NOT accessible from outside the VM (`nc -zv <ip> 3306` should fail) |
| 89 | +- [ ] Task 2.3: Verify healthcheck still works (container becomes healthy) |
| 90 | +- [ ] Task 2.4: Verify tracker can still connect to MySQL via internal network |
| 91 | + |
| 92 | +## Acceptance Criteria |
| 93 | + |
| 94 | +> **Note for Contributors**: These criteria define what the PR reviewer will check. Use this as your pre-review checklist before submitting the PR to minimize back-and-forth iterations. |
| 95 | + |
| 96 | +**Quality Checks**: |
| 97 | + |
| 98 | +- [ ] Pre-commit checks pass: `./scripts/pre-commit.sh` |
| 99 | + |
| 100 | +**Security Criteria**: |
| 101 | + |
| 102 | +- [ ] MySQL port 3306 is not accessible from outside the VM |
| 103 | +- [ ] MySQL healthcheck still passes (container reaches healthy state) |
| 104 | +- [ ] Tracker can still connect to MySQL via Docker internal network |
| 105 | +- [ ] E2E tests pass |
| 106 | + |
| 107 | +## Related Documentation |
| 108 | + |
| 109 | +- [Docker UFW Firewall Security Strategy](../decisions/docker-ufw-firewall-security-strategy.md) |
| 110 | +- [Docker Network Segmentation Analysis](../analysis/security/docker-network-segmentation-analysis.md) |
| 111 | +- [User Guide: Security](../user-guide/security.md) |
| 112 | + |
| 113 | +## Notes |
| 114 | + |
| 115 | +The fix is straightforward - simply remove the `ports` section from MySQL service. The healthcheck will continue to work because: |
| 116 | + |
| 117 | +1. Docker healthchecks run inside the container |
| 118 | +2. `mysqladmin ping -h localhost` connects to MySQL within the container's network namespace |
| 119 | +3. No external port binding is required for container-internal commands |
0 commit comments