zentinel-proxy 0.6.11

A security-first reverse proxy built on Pingora with sleepable ops at the edge
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
# ACME Automatic Certificate Management

This document describes Zentinel's ACME (Automatic Certificate Management Environment) implementation for automatic TLS certificate issuance and renewal via Let's Encrypt.

## Overview

Zentinel supports automatic TLS certificate management using the ACME protocol (RFC 8555). This eliminates the need for manual certificate management by automatically:

- Requesting certificates from Let's Encrypt
- Completing HTTP-01 or DNS-01 domain validation challenges
- **Wildcard certificate support** via DNS-01 challenges
- Storing certificates securely on disk
- Renewing certificates before expiration
- Hot-reloading certificates without proxy restart

```
┌─────────────────────────────────────────────────────────────────────┐
│                        ACME Certificate Flow                         │
│                                                                      │
│  ┌──────────────┐     ┌──────────────┐     ┌──────────────┐        │
│  │  AcmeClient  │────▶│ Let's Encrypt│────▶│   Storage    │        │
│  │              │     │    Server    │     │              │        │
│  │ - Account    │     │              │     │ - Certs      │        │
│  │ - Orders     │◀────│ - Challenges │     │ - Keys       │        │
│  │ - CSR        │     │ - Validation │     │ - Metadata   │        │
│  └──────────────┘     └──────────────┘     └──────────────┘        │
│         │                    │                    │                 │
│         ▼                    ▼                    ▼                 │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │                    Challenge Handling                         │  │
│  │                                                               │  │
│  │  HTTP-01: Served from /.well-known/acme-challenge/            │  │
│  │  DNS-01:  TXT records via DNS provider API                    │  │
│  └──────────────────────────────────────────────────────────────┘  │
│                              │                                      │
│                              ▼                                      │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │                   RenewalScheduler                            │  │
│  │                                                               │  │
│  │  Background task checking certificates every 12 hours         │  │
│  │  Triggers renewal when within renew_before_days of expiry     │  │
│  │  Supports both HTTP-01 and DNS-01 renewal flows               │  │
│  └──────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘
```

## Module Structure

### `acme/mod.rs`

Module exports and public API:

```rust
pub use challenge::ChallengeManager;
pub use client::AcmeClient;
pub use error::AcmeError;
pub use scheduler::RenewalScheduler;
pub use storage::CertificateStorage;

// DNS-01 challenge support
pub mod dns;
```

### `acme/dns/` - DNS-01 Challenge Support

The DNS module provides DNS-01 challenge support for wildcard certificates:

```
acme/dns/
├── mod.rs           # Module exports
├── provider.rs      # DnsProvider trait and errors
├── challenge.rs     # Dns01ChallengeManager
├── propagation.rs   # DNS propagation checking
├── credentials.rs   # Secure credential loading
└── providers/
    ├── mod.rs       # Provider factory
    ├── hetzner.rs   # Hetzner DNS provider
    ├── cloudflare.rs # Cloudflare DNS provider
    └── webhook.rs   # Generic webhook provider
```

#### DNS Provider Trait

```rust
#[async_trait]
pub trait DnsProvider: Send + Sync + Debug {
    fn name(&self) -> &'static str;

    async fn create_txt_record(
        &self,
        domain: &str,
        record_name: &str,
        record_value: &str,
    ) -> DnsResult<String>;  // Returns record ID

    async fn delete_txt_record(
        &self,
        domain: &str,
        record_id: &str,
    ) -> DnsResult<()>;

    async fn supports_domain(&self, domain: &str) -> DnsResult<bool>;
}
```
#### Supported DNS Providers

| Provider | Description |
|----------|-------------|
| `hetzner` | Hetzner DNS API |
| `cloudflare` | Cloudflare DNS API v4 |
| `webhook` | Generic webhook for custom DNS integrations |

#### DNS-01 Challenge Flow
...
### Custom ACME Directory and EAB

Zentinel supports custom ACME directory URLs and External Account Binding (EAB), which is required by providers like ZeroSSL.

```kdl
acme {
    email "admin@example.com"
    domains "example.com"

    // Custom ACME directory URL
    server-url "https://acme.zerossl.com/v2/DV90"

    // External Account Binding (EAB) credentials
    eab {
        kid "your-eab-kid"
        hmac-key "your-base64url-encoded-hmac-key"
    }
}
```

### SAN (Subject Alternative Name) Certificates

Zentinel supports single certificates covering multiple domains. The renewal scheduler automatically handles this by only checking the primary domain (the first one in the list) to avoid redundant renewal requests and infinite loops.

```kdl
acme {
    email "admin@example.com"
    domains "example.com" "api.example.com" "www.example.com"
}
```

### `acme/client.rs`

1. **Create Order** - Request certificate with DNS-01 challenges
2. **Create TXT Records** - Provider creates `_acme-challenge.{domain}` records
3. **Wait for Propagation** - Query public DNS for record visibility
4. **Notify ACME Server** - Challenge is ready for validation
5. **Wait for Validation** - ACME server verifies records
6. **Cleanup** - Delete TXT records (always, even on failure)
7. **Finalize** - Submit CSR and retrieve certificate

### `acme/client.rs`

The `AcmeClient` wraps the `instant-acme` library and provides:

- **Account Management**: Creates or loads ACME accounts with Let's Encrypt
- **Order Creation**: Initiates certificate orders for configured domains
- **Challenge Handling**: Coordinates HTTP-01 challenge validation
- **Certificate Finalization**: Generates CSRs and retrieves issued certificates

Key methods:
- `init_account()` - Initialize or restore ACME account
- `create_order()` - Create certificate order with HTTP-01 challenges
- `create_order_dns01()` - Create certificate order with DNS-01 challenges
- `validate_challenge()` - Notify ACME server challenge is ready
- `wait_for_order_ready()` - Poll until order is validated
- `finalize_order()` - Submit CSR and get certificate
- `needs_renewal()` - Check if certificate needs renewal

### `acme/challenge.rs`

The `ChallengeManager` handles HTTP-01 challenge responses:

```rust
pub const ACME_CHALLENGE_PREFIX: &str = "/.well-known/acme-challenge/";

impl ChallengeManager {
    pub fn add_challenge(&self, token: &str, key_authorization: &str);
    pub fn get_response(&self, token: &str) -> Option<String>;
    pub fn remove_challenge(&self, token: &str);
    pub fn extract_token(path: &str) -> Option<&str>;
}
```

Uses `DashMap` for concurrent, lock-free access to active challenges.

### `acme/storage.rs`

The `CertificateStorage` manages persistent storage:

```
storage/
├── credentials.json     # Serialized AccountCredentials (opaque)
└── domains/
    └── example.com/
        ├── cert.pem     # Certificate chain
        ├── key.pem      # Private key (mode 0600)
        └── meta.json    # Expiry, issued date, domains
```

Key methods:
- `load_certificate()` / `save_certificate()` - Certificate persistence
- `load_credentials_json()` / `save_credentials_json()` - Account credentials
- `needs_renewal()` - Check if within renewal window
- `certificate_paths()` - Get paths for cert/key files

### `acme/scheduler.rs`

The `RenewalScheduler` runs as a background task:

- Default check interval: 12 hours (configurable, minimum 1 hour)
- Initial check after 10 second startup delay
- Triggers renewal when certificate expires within `renew_before_days`
- Triggers TLS hot-reload after successful renewal

### `acme/error.rs`

ACME-specific error types:

```rust
pub enum AcmeError {
    AccountCreation(String),
    NoAccount,
    OrderCreation(String),
    NoHttp01Challenge(String),
    NoDns01Challenge(String),         // DNS-01 challenge not available
    NoDnsProvider,                     // DNS-01 requested but no provider configured
    DnsProvider(DnsProviderError),     // DNS provider operation failed
    PropagationTimeout { record: String, elapsed: Duration },
    WildcardRequiresDns01 { domain: String },
    ChallengeValidation { domain: String, message: String },
    Finalization(String),
    CertificateParse(String),
    Timeout(String),
    Storage(StorageError),
}
```

## Integration Points

### HTTP-01 Challenge Handling

Challenges are handled in `early_request_filter` before any other request processing:

```rust
// In http_trait.rs
if let Some(ref challenge_manager) = self.acme_challenges {
    if let Some(token) = ChallengeManager::extract_token(path) {
        if let Some(key_authorization) = challenge_manager.get_response(token) {
            // Serve challenge response with 200 OK
            // Content-Type: text/plain
        }
    }
}
```

### TLS Hot-Reload

After successful certificate renewal, the scheduler triggers reload:

```rust
if let Some(ref resolver) = self.sni_resolver {
    resolver.reload()?;
}
```

This uses the existing `HotReloadableSniResolver` infrastructure.

### ZentinelProxy Fields

```rust
pub struct ZentinelProxy {
    // ... existing fields ...

    /// ACME challenge manager for HTTP-01 validation
    pub acme_challenges: Option<Arc<ChallengeManager>>,

    /// ACME client for certificate operations
    pub acme_client: Option<Arc<AcmeClient>>,
}
```

## Configuration

ACME is configured within the `tls {}` block of a listener.

### HTTP-01 Challenge (Default)

```kdl
listeners {
    listener "https" address="0.0.0.0:443" {
        tls {
            acme {
                email "admin@example.com"
                domains "example.com" "www.example.com"
                staging false
                storage "/var/lib/zentinel/acme"
                renew-before-days 30
            }
        }
    }
}
```

### DNS-01 Challenge (For Wildcard Certificates)

```kdl
listeners {
    listener "https" address="0.0.0.0:443" {
        tls {
            acme {
                email "admin@example.com"
                domains "example.com" "*.example.com"
                staging false
                storage "/var/lib/zentinel/acme"
                renew-before-days 30
                challenge-type "dns-01"

                dns-provider {
                    type "hetzner"
                    credentials-file "/etc/zentinel/secrets/hetzner-dns.json"
                    api-timeout-secs 30

                    propagation {
                        initial-delay-secs 10
                        check-interval-secs 5
                        timeout-secs 120
                        nameservers "8.8.8.8" "1.1.1.1"
                    }
                }
            }
        }
    }
}
```

### Webhook Provider (Custom DNS Integration)

```kdl
dns-provider {
    type "webhook"
    url "https://dns-api.internal/v1"
    auth-header "X-API-Key"
    credentials-file "/etc/zentinel/secrets/dns-webhook.json"
}
```

### Configuration Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `email` | string | required | Contact email for Let's Encrypt account |
| `domains` | string[] | required | Domains to include in certificate |
| `staging` | bool | `false` | Use Let's Encrypt staging environment |
| `storage` | path | `/var/lib/zentinel/acme` | Directory for certificates and credentials |
| `renew-before-days` | u32 | `30` | Days before expiry to trigger renewal |
| `challenge-type` | string | `"http-01"` | Challenge type: `http-01` or `dns-01` |
| `key-type` | string | `"ecdsa-p256"` | Certificate key type: `ecdsa-p256`, `ecdsa-p384` |
| `dns-provider` | block | - | DNS provider config (required for dns-01) |

### Certificate Key Types

Zentinel allows specifying the encryption algorithm and key size for ACME certificates.

| Value | Description |
|-------|-------------|
| `ecdsa-p256` | ECDSA with NIST P-256 curve (Default, fast and secure) |
| `ecdsa-p384` | ECDSA with NIST P-384 curve (Higher security strength) |

```kdl
acme {
    email "admin@example.com"
    domains "example.com"
    
    // Use high-strength ECDSA
    key-type "ecdsa-p384"
}
```

### DNS Provider Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `type` | string | required | Provider type: `hetzner`, `webhook` |
| `credentials-file` | path | - | Path to credentials file |
| `credentials-env` | string | - | Environment variable with credentials |
| `api-timeout-secs` | u64 | `30` | API request timeout |

### Propagation Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `initial-delay-secs` | u64 | `10` | Wait before first propagation check |
| `check-interval-secs` | u64 | `5` | Interval between checks |
| `timeout-secs` | u64 | `120` | Max time to wait for propagation |
| `nameservers` | string[] | public DNS | DNS servers to query for propagation |

### Credential File Formats

**JSON Token Format:**
```json
{"token": "your-api-token"}
```

**JSON Key/Secret Format:**
```json
{"api_key": "key", "api_secret": "secret"}
```

**Plain Text:**
```
your-api-token
```

### Validation Rules

- Email must be a valid email address
- At least one domain is required
- When `acme` is configured, `cert_file` and `key_file` are optional
- Manual certificates and ACME can coexist (manual takes precedence if both present)
- **Wildcard domains require `challenge-type "dns-01"`**
- **DNS-01 requires a `dns-provider` block**

## Security Considerations

1. **Storage Permissions**: Certificate storage directory is created with mode `0700`, private keys with mode `0600`

2. **Staging Environment**: Use `staging true` for testing to avoid rate limits

3. **Account Credentials**: The `credentials.json` file contains the ACME account private key and should be protected

4. **Challenge Tokens**: Challenge tokens are short-lived and automatically cleaned up after validation

## Dependencies

- `instant-acme` - ACME protocol implementation
- `rcgen` - CSR generation
- `x509-parser` - Certificate parsing for expiry extraction
- `dashmap` - Concurrent challenge storage
- `hickory-resolver` - DNS propagation checking (DNS-01)
- `reqwest` - HTTP client for DNS provider APIs (DNS-01)

## Future Improvements

Phase 2 (completed in v0.4.0):
- ✅ DNS-01 challenge support
- ✅ Wildcard certificates
- ✅ Hetzner DNS provider
- ✅ Generic webhook provider

Phase 3 (planned):
- Multiple certificate authorities
- Certificate transparency logging
- OCSP stapling integration
- Distributed challenge coordination
- Certificate inventory API
- Additional DNS providers (Cloudflare, Route53, etc.)