# Story 4.45: Webhook Secure Defaults

**Status:** Complete
**Priority:** High
**Depends on:** Story 4.42 (Webhook HMAC signature - completed)

---

## Context / Background

An external audit identified webhook security as a high-priority improvement. Two issues were found:

1. **Default signature mode is deprecated**: `src/fapilog/plugins/sinks/webhook.py:42` defaults to `SignatureMode.HEADER` which sends secrets in plain headers. The more secure `HMAC` mode exists but isn't the default. Currently:
   ```python
   signature_mode: SignatureMode = Field(
       default=SignatureMode.HEADER,  # Deprecated!
       description="Authentication mode: 'header' (deprecated) or 'hmac' (recommended)",
   )
   ```

2. **No replay protection**: The HMAC signature (`webhook.py:104-111`) only signs the JSON payload. An attacker who intercepts a signed request can replay it indefinitely. Industry best practice includes a timestamp and/or nonce in the signature.

Story 4.42 added HMAC support with deprecation warnings for HEADER mode. This story completes the transition by making HMAC the default and adding replay protection.

---

## Scope (In / Out)

### In Scope

- Change `signature_mode` default from `HEADER` to `HMAC`
- Add `X-Fapilog-Timestamp` header with Unix timestamp
- Include timestamp in HMAC signature computation
- Add optional `replay_tolerance_seconds` config (default: 300 seconds / 5 minutes)
- Document signature verification for webhook consumers
- Update migration guide for users relying on HEADER mode

### Out of Scope

- Nonce-based replay protection (stateful, requires server-side storage)
- Webhook retry with idempotency keys (separate concern)
- Mutual TLS authentication (enterprise feature)

---

## Acceptance Criteria

### AC1: HMAC is Default Signature Mode

**Description:** New webhook configurations default to HMAC signing, not header secrets.

**Validation:**
```python
from fapilog.plugins.sinks.webhook import WebhookSinkConfig, SignatureMode

config = WebhookSinkConfig(endpoint="https://example.com", secret="test")
assert config.signature_mode == SignatureMode.HMAC
```

### AC2: Timestamp Included in Requests

**Description:** All signed webhook requests include an `X-Fapilog-Timestamp` header with the current Unix timestamp.

**Validation:**
```python
# When sending a webhook, headers should include:
# X-Fapilog-Timestamp: 1737216000 (example Unix timestamp)
# X-Fapilog-Signature-256: sha256=<hmac of timestamp + payload>
```

### AC3: Timestamp Included in Signature

**Description:** The HMAC signature covers both timestamp and payload to prevent tampering.

**Validation:**
```python
import hmac
import hashlib

# Signature computation should be:
message = f"{timestamp}.{json_payload}".encode()
signature = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
# Header: X-Fapilog-Signature-256: sha256={signature}
```

### AC4: Verification Documentation

**Description:** Documentation explains how webhook consumers should verify signatures and reject stale requests.

**Validation:**
```python
# docs/user-guide/webhooks.md should include:
def verify_webhook(request, secret, tolerance_seconds=300):
    timestamp = int(request.headers["X-Fapilog-Timestamp"])
    if abs(time.time() - timestamp) > tolerance_seconds:
        raise ValueError("Request too old or too new")

    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.{request.body}".encode(),
        hashlib.sha256
    ).hexdigest()

    received = request.headers["X-Fapilog-Signature-256"].removeprefix("sha256=")
    if not hmac.compare_digest(expected, received):
        raise ValueError("Invalid signature")
```

### AC5: HEADER Mode Still Supported

**Description:** Users can still opt into HEADER mode explicitly for backward compatibility.

**Validation:**
```python
config = WebhookSinkConfig(
    endpoint="https://example.com",
    secret="test",
    signature_mode=SignatureMode.HEADER,  # Explicit opt-in
)
# Should work but emit DeprecationWarning
```

---

## Implementation Notes

### File Changes

```
src/fapilog/plugins/sinks/webhook.py (MODIFIED - default + timestamp)
src/fapilog/core/settings.py (MODIFIED - add replay_tolerance_seconds if needed)
docs/user-guide/webhooks.md (NEW or MODIFIED - verification guide)
tests/unit/sinks/test_webhook.py (MODIFIED - new test cases)
```

### Signature Format Change

```python
# Before (payload only):
body = json.dumps(payload, separators=(",", ":")).encode()
signature = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()

# After (timestamp + payload):
timestamp = int(time.time())
message = f"{timestamp}.{json.dumps(payload, separators=(',', ':'))}".encode()
signature = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
headers["X-Fapilog-Timestamp"] = str(timestamp)
headers["X-Fapilog-Signature-256"] = f"sha256={signature}"
```

---

## Tasks

### Phase 1: Core Implementation

- [ ] Change `signature_mode` default to `SignatureMode.HMAC`
- [ ] Add `X-Fapilog-Timestamp` header generation
- [ ] Update HMAC computation to include timestamp
- [ ] Add `replay_tolerance_seconds` to config (for documentation purposes)

### Phase 2: Testing

- [ ] Update existing webhook tests for new default
- [ ] Add tests for timestamp header presence
- [ ] Add tests for signature computation with timestamp
- [ ] Add tests for HEADER mode backward compatibility

### Phase 3: Documentation

- [ ] Create/update webhook verification guide
- [ ] Add migration notes for HEADER → HMAC transition
- [ ] Update CHANGELOG

---

## Tests

### Unit Tests

- `tests/unit/sinks/test_webhook.py`
  - `test_default_signature_mode_is_hmac`
  - `test_timestamp_header_present`
  - `test_signature_includes_timestamp`
  - `test_header_mode_still_works_with_warning`
  - `test_signature_verification_example`

### Integration Tests

- End-to-end webhook delivery with signature verification
- Replay rejection test (old timestamp should fail verification)

---

## Definition of Done

### Code Complete

- [ ] Default signature mode is HMAC
- [ ] Timestamp included in all signed requests
- [ ] Signature computation includes timestamp
- [ ] HEADER mode works with deprecation warning

### Quality Assurance

- [ ] Unit tests written and passing
- [ ] `ruff check` passes
- [ ] `mypy` passes
- [ ] No regression in existing tests

### Documentation

- [ ] Webhook verification guide complete
- [ ] Migration notes for HEADER users
- [ ] CHANGELOG updated

---

## Risks / Rollback

### Risks

1. **Risk:** Breaking change for webhook consumers expecting old signature format
   - **Mitigation:** Document the change clearly; old signature can be computed for comparison during transition

2. **Risk:** Clock skew between sender and receiver
   - **Mitigation:** Default 5-minute tolerance is generous; document NTP recommendation

### Rollback Plan

If issues occur:
1. Users can set `signature_mode=SignatureMode.HEADER` to revert behavior
2. Consider adding `signature_version` field for explicit format selection

---

## Related Stories

- **Depends on:** Story 4.42 - Webhook HMAC signature (foundation)
- **Related:** Story 4.46 - Fallback sink PII protection (security theme)

---

## Code Review

**Date:** 2026-01-18
**Reviewer:** Claude
**Verdict:** OK to PR

### Summary

Reviewed implementation of secure webhook defaults: HMAC as default signature mode, timestamp-based replay protection, and verification documentation updates.

### AC Verification

| Criterion | Evidence |
|-----------|----------|
| AC1: HMAC is default | `webhook.py:44` - `default=SignatureMode.HMAC` |
| AC2: Timestamp header | `webhook.py:119` - `headers["X-Fapilog-Timestamp"] = str(timestamp)` |
| AC3: Timestamp in signature | `webhook.py:112-113` - `message = f"{timestamp}.{json_body}".encode()` |
| AC4: Verification docs | `webhook-security.md:71-103` - FastAPI/Flask examples with timestamp tolerance |
| AC5: HEADER mode works | `test_webhook_signature.py:143-150` - explicit opt-in with deprecation warning |

### Quality Gates

- [x] ruff check passed
- [x] ruff format passed
- [x] mypy passed
- [x] diff-cover >= 90% (100% on changed lines)
- [x] No weak assertions
- [x] No dead code (vulture whitelist for backward-compat items)

---

## Change Log

| Date | Change | Author |
|------|--------|--------|
| 2026-01-18 | Initial draft from audit findings | Claude |
| 2026-01-18 | Implementation complete, code review passed | Claude |
