# DMARC Lab — Email Authentication Demo System

A Docker-based virtual mail lab for demonstrating **SPF**, **DKIM**, and **DMARC** in action.  

---

## Architecture

```
┌─────────────────────────────────────────────────────────┐
│               Docker Network: mailnet (10.0.0.0/24)     │
│                                                         │
│  ┌──────────────┐  ┌────────────────┐  ┌─────────────┐  │
│  │     dns      │  │   sender-mta   │  │ receiver-mta│  │
│  │  10.0.0.2    │  │   10.0.0.10    │  │  10.0.0.20  │  │
│  │              │  │                │  │             │  │
│  │  BIND9       │  │  Postfix       │  │  Postfix    │  │
│  │  - SPF TXT   │  │  + OpenDKIM    │  │  + OpenDKIM │  │
│  │  - DKIM TXT  │  │    (signing)   │  │    (verify) │  │
│  │  - DMARC TXT │  │                │  │  + OpenDMARC│  │
│  │  - MX / A    │  │  swaks         │  │  + SPF-pol. │  │
│  └──────────────┘  └────────────────┘  └─────────────┘  │
│         ▲              │  SMTP :25         ▲            │
│         │              └──────────────────►│            │
│     DNS queries from both MTAs             │            │
└─────────────────────────────────────────────────────────┘
```

### Domains

| Domain | Role | SPF | DKIM | DMARC |
|---|---|---|---|---|
| `legit.lab` | Legitimate sender | ✅ `ip4:10.0.0.10 -all` | ✅ `selector1` | ✅ `p=reject` (strict) |
| `receiver.lab` | Mail receiver | — | — | — |
| `thirdparty.lab` | Third-party relay | ✅ `ip4:10.0.0.10 -all` | ✅ `selector1` | ✅ `p=none` |
| `misconf.lab` | Misconfigured sender | ✅ `ip4:10.0.0.10 -all` | ✅ `selector1` | ✅ `p=none` |
| `evil.lab` | Attacker | ❌ none | ❌ none | ❌ none |

---

## Prerequisites

- **Docker** ≥ 20.10 and **Docker Compose** v2
- **OpenSSL** (for DKIM key generation in setup.sh)
- Linux host (tested on Ubuntu 22.04)

---

## Quick Start

```bash
# 1. Clone / navigate to the project
cd /path/to/mailsystem_proj

# 2. One-time setup: generate DKIM keys and DNS zone files
chmod +x setup.sh
./setup.sh

# 3. Build and start all containers
docker compose up --build -d

# 4. Wait for services to initialize (~15 seconds)
sleep 15

# 5. Run the demo scenarios
docker compose exec sender-mta bash /scripts/scenario-a.sh   # Legit (PASS)
docker compose exec sender-mta bash /scripts/scenario-b.sh   # Spoof (REJECT)
docker compose exec sender-mta bash /scripts/scenario-c.sh   # Partial bypass (REJECT)
docker compose exec sender-mta bash /scripts/scenario-d.sh   # Misconfig (DELIVERED)

# 6. Inspect results
docker compose exec receiver-mta bash /scripts/check-logs.sh
docker compose exec receiver-mta bash /scripts/check-headers.sh
docker compose exec receiver-mta bash /scripts/forensic-report.sh

# 7. Teardown
docker compose down -v
```

---

## Demo Scenarios

### Scenario A — Legitimate Email (Full PASS) ✅

```
Envelope:  MAIL FROM:<alice@legit.lab>
Header:    From: Alice <alice@legit.lab>
DKIM:      signed d=legit.lab s=selector1
```

**Flow:** `swaks --> sender-mta (Postfix + OpenDKIM signs) --> receiver-mta`

| Check | Result | Why |
|---|---|---|
| SPF | **pass** | IP 10.0.0.10 is in legit.lab SPF |
| DKIM | **pass** | Valid signature, d=legit.lab |
| DMARC | **pass** | From aligned with both SPF and DKIM |

**What to look for:**
- `Authentication-Results` header showing all three PASS
- Postfix log: `status=sent`
- OpenDKIM log: signature verified
- Mail delivered to `/var/mail/bob`

---

### Scenario B — Classic Spoof (DMARC → REJECT) ❌

```
Envelope:  MAIL FROM:<random@evil.lab>
Header:    From: CEO <ceo@legit.lab>    ← impersonation!
DKIM:      none (bypasses sender-mta)
```

**Flow:** `swaks --> direct to receiver-mta (bypasses OpenDKIM)`

| Check | Result | Why |
|---|---|---|
| SPF | **none** | evil.lab has no SPF record |
| DKIM | **none** | No signature present |
| DMARC | **fail** | From=legit.lab, no aligned SPF or DKIM |
| Action | **550 REJECT** | legit.lab policy p=reject |

**What to look for:**
- SMTP rejection `550 5.7.1` in swaks output
- OpenDMARC log: `fail` with reject action
- **No mail in mailbox** (rejected at SMTP level)

---

### Scenario C — Partial Bypass (SPF pass, DMARC fail) ⚠️

> **This is the key thesis demonstration**: SPF passing alone is NOT enough.

```
Envelope:  MAIL FROM:<mailer@thirdparty.lab>   (SPF pass!)
Header:    From: CEO <ceo@legit.lab>            ← still impersonation
DKIM:      none
```

**Flow:** `swaks --> direct to receiver-mta`

| Check | Result | Why |
|---|---|---|
| SPF | **pass** | IP 10.0.0.10 authorized for thirdparty.lab |
| DKIM | **none** | No signature |
| DMARC | **fail** | SPF domain ≠ From domain (alignment mismatch) |
| Action | **REJECT** | legit.lab policy p=reject |

**Key insight:** The SPF check passed because the envelope domain (thirdparty.lab) authorizes this IP. But DMARC checks **alignment** — the SPF-authenticated domain must match the `From:` header domain. They don't match → DMARC catches the spoofing attempt.

---

### Scenario D — Misconfiguration (p=none) ⚠️

> **This demonstrates what happens when DMARC is deployed but not enforced.**

```
Envelope:  MAIL FROM:<attacker@evil.lab>        (SPF fail)
Header:    From: Admin <admin@misconf.lab>      ← impersonation!
DKIM:      none
```

**Flow:** `swaks --> direct to receiver-mta`

| Check | Result | Why |
|---|---|---|
| SPF | **none** | evil.lab has no SPF record |
| DKIM | **none** | No signature |
| DMARC | **fail** | From=misconf.lab, no aligned SPF or DKIM |
| Action | **DELIVERED** | misconf.lab policy p=none |

**Key insight:** Even though the email clearly fails DMARC authentication (like Scenario B), it is **delivered** because the domain owner (misconf.lab) explicitly told receivers not to reject failing emails (`p=none`). Receivers will still send forensic/aggregate reports, but the spoofed email reaches the inbox.

---

## Forensic Tools

| Script | Purpose | Run on |
|---|---|---|
| `check-logs.sh` | Color-coded SPF/DKIM/DMARC log viewer | receiver-mta |
| `check-headers.sh` | Extract Authentication-Results from delivered mail | receiver-mta |
| `forensic-report.sh` | Full forensic dump (DNS + logs + headers + stats) | receiver-mta |

### Forensic Report includes:
1. **DNS records** for all four domains (SPF, DKIM, DMARC, MX)
2. **Mail log analysis** — SPF, DKIM, DMARC events
3. **Delivered mail headers** — full Authentication-Results
4. **OpenDMARC history** — aggregate data file
5. **Summary statistics** — pass/fail counts, deliveries, rejections

---

## How It Works

### Email Authentication Chain on receiver-mta

```
Incoming SMTP connection
        │
        ▼
┌─────────────────┐
│  policyd-spf    │  Checks SPF: is the sending IP authorized
│  (Postfix       │  for the envelope sender domain?
│   policy svc)   │  Adds Received-SPF header
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  OpenDKIM       │  Verifies DKIM signature:
│  (milter, port  │  is the signature valid and matches
│   8891)         │  the claimed signing domain?
└────────┬────────┘  Adds Authentication-Results: dkim=...
         │
         ▼
┌─────────────────┐
│  OpenDMARC      │  Evaluates DMARC:
│  (milter, port  │  1. Reads From: header domain
│   8893)         │  2. Checks SPF alignment (envelope = From?)
│                 │  3. Checks DKIM alignment (d= = From?)
│                 │  4. Applies domain's DMARC policy
└────────┬────────┘  Adds Authentication-Results: dmarc=...
         │
         ▼
    ┌─────────┐
    │ DELIVER │  if DMARC pass (or p=none)
    │   or    │
    │ REJECT  │  if DMARC fail + p=reject
    └─────────┘
```

### DMARC Alignment Explained

DMARC requires **alignment** between the `From:` header and at least one of:
- **SPF alignment**: envelope sender domain must match `From:` domain
- **DKIM alignment**: DKIM `d=` domain must match `From:` domain

| Scenario | From Header | SPF Domain | DKIM Domain | Aligned? |
|---|---|---|---|---|
| A | legit.lab | legit.lab | legit.lab | ✅ Both |
| B | legit.lab | evil.lab | (none) | ❌ Neither |
| C | legit.lab | thirdparty.lab | (none) | ❌ Neither |
| D | misconf.lab | evil.lab | (none) | ❌ Neither |

---

## Project Structure

```
mailsystem_proj/
├── docker-compose.yml          # 3 containers on 10.0.0.0/24
├── setup.sh                    # One-time DKIM key generation
├── dns/
│   ├── Dockerfile              # BIND9
│   ├── named.conf              # Zone declarations
│   └── zones/
│       ├── legit.lab.zone.template     # SPF + DKIM + DMARC p=reject
│       ├── receiver.lab.zone           # MX only
│       ├── thirdparty.lab.zone.template # SPF + DKIM + DMARC p=none
│       ├── misconf.lab.zone.template   # SPF + DKIM + DMARC p=none
│       ├── evil.lab.zone               # No auth records
│       └── 0.0.10.in-addr.arpa.zone    # Reverse DNS
├── sender-mta/
│   ├── Dockerfile              # Postfix + OpenDKIM + swaks
│   ├── main.cf                 # Postfix config (relay + milter)
│   ├── opendkim.conf           # Signing mode
│   ├── signing.table           # Maps sender → key
│   ├── key.table               # Maps key → domain:selector:file
│   ├── trusted-hosts           # Lab subnet
│   ├── keys/                   # DKIM private keys (generated)
│   └── entrypoint.sh
├── receiver-mta/
│   ├── Dockerfile              # Postfix + OpenDKIM + OpenDMARC + SPF
│   ├── main.cf                 # Postfix config (milters + SPF policy)
│   ├── opendkim.conf           # Verify mode
│   ├── opendmarc.conf          # DMARC evaluation + forensic reports
│   └── entrypoint.sh
├── scripts/
│   ├── scenario-a.sh           # Legitimate email (PASS)
│   ├── scenario-b.sh           # Spoof (REJECT)
│   ├── scenario-c.sh           # Partial bypass (alignment fail)
│   ├── scenario-d.sh           # Misconfig spoof (DELIVERED, p=none)
│   ├── check-logs.sh           # Color-coded log viewer
│   ├── check-headers.sh        # Authentication-Results inspector
│   └── forensic-report.sh      # Full forensic dump
└── README.md
```

---

## Useful Commands

```bash
# View real-time logs from all containers
docker compose logs -f

# View specific container logs
docker compose logs -f receiver-mta

# Shell into a container
docker compose exec receiver-mta bash

# Check DNS records manually
docker compose exec dns dig @localhost legit.lab TXT +short
docker compose exec dns dig @localhost _dmarc.legit.lab TXT +short
docker compose exec dns dig @localhost selector1._domainkey.legit.lab TXT +short

# Read bob's mailbox
docker compose exec receiver-mta cat /var/mail/bob

# Restart the lab
docker compose down -v && docker compose up --build -d

# Clean everything (including keys)
docker compose down -v
rm -rf sender-mta/keys/ dns/zones/legit.lab.zone dns/zones/thirdparty.lab.zone
```
