feat(cli,transport): Let's Encrypt outer-cert support on TLS-443/QUIC
Server admins can now point the outer TLS layer at a real CA-signed cert
(e.g. Let's Encrypt fullchain.pem) so the on-wire HTTPS camouflage is
indistinguishable from a normal CA-trusted HTTPS server. The inner Aura
mutual-auth handshake still uses the Aura CA (necessarily — that's where
the PQ mutual auth lives).
- aura-cli config: optional [server.outer_cert] {cert_path, key_path}.
Both fields together (or neither); resolve() reads PEMs and returns
(cert, key) tuple. Absent section -> falls back to reusing the Aura
server cert (v2 behavior, fully back-compat).
- aura-transport: additive MultiServer::bind_with_outer and
TcpServer::bind_with_outer that accept an optional separate outer cert.
Old MultiServer::bind / TcpServer::bind preserved as thin wrappers
(back-compat: existing callers untouched). AuraServer::bind already
took outer cert separately.
- UDP transport doesn't have outer TLS, so outer cert is irrelevant
there — only QUIC + TCP layers benefit.
- 4 new tests (parsing, back-compat, partial-section validation, two-CA
loopback verifying inner peer_id is the inner CN). Workspace: 257 tests
passed (+4), clippy -D warnings clean, fmt clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -100,6 +100,13 @@ pub struct ServerSection {
|
||||
/// Number of accept workers (advisory in v1).
|
||||
#[serde(default = "default_workers")]
|
||||
pub workers: usize,
|
||||
/// `[server.outer_cert]` sub-section: v3 explicit outer-TLS cert/key for QUIC and TCP. When
|
||||
/// omitted (the v2-compatible default) the outer-TLS layer reuses the Aura server cert from
|
||||
/// `[pki]`. When set, a passive observer on `:443` sees a normal CA-trusted handshake (e.g.
|
||||
/// Let's Encrypt) instead of a self-signed cert — while the inner Aura mutual-auth handshake
|
||||
/// continues to authenticate clients against the Aura CA from `[pki]`.
|
||||
#[serde(default)]
|
||||
pub outer_cert: Option<ServerOuterCertSection>,
|
||||
/// `[server.pool]` sub-section: v2 per-client IP pool. Omitting it triggers a v1-compatible
|
||||
/// fallback that interprets `[tunnel] pool_cidr` as a [`PoolStrategy::DynamicOnly`] pool.
|
||||
#[serde(default)]
|
||||
@@ -154,6 +161,71 @@ pub struct RelaySection {
|
||||
pub allow_extend_to: Vec<String>,
|
||||
}
|
||||
|
||||
/// `[server.outer_cert]` section: v3 explicit outer-TLS cert/key for the QUIC and TCP transports.
|
||||
///
|
||||
/// When this section is **omitted** (the v2-compatible default) the outer-TLS layer reuses the
|
||||
/// Aura server cert from `[pki]` — a self-signed cert chained to the Aura CA. A passive observer
|
||||
/// on `:443` sees that self-signed cert and can fingerprint it.
|
||||
///
|
||||
/// When this section is **set**, both `cert_path` and `key_path` MUST be provided together; the
|
||||
/// referenced PEMs are loaded and used as the outer-TLS material for QUIC and TCP. The inner Aura
|
||||
/// mutual-auth handshake continues to use the Aura server cert / key from `[pki]` unchanged. The
|
||||
/// typical deployment points this at a Let's Encrypt fullchain.pem + privkey.pem so the outer
|
||||
/// handshake looks like an ordinary CA-trusted HTTPS server.
|
||||
///
|
||||
/// Example:
|
||||
/// ```toml
|
||||
/// [server.outer_cert]
|
||||
/// cert_path = "/etc/letsencrypt/live/vpn.example.com/fullchain.pem"
|
||||
/// key_path = "/etc/letsencrypt/live/vpn.example.com/privkey.pem"
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct ServerOuterCertSection {
|
||||
/// Path to the outer-TLS certificate PEM (e.g. Let's Encrypt `fullchain.pem`). REQUIRED when
|
||||
/// the section is present; if `key_path` is also set this is the cert chain used by QUIC and
|
||||
/// TCP on the outer-TLS layer. Path may begin with `~`.
|
||||
pub cert_path: Option<PathBuf>,
|
||||
/// Path to the outer-TLS private key PEM (e.g. Let's Encrypt `privkey.pem`). REQUIRED when
|
||||
/// `cert_path` is set. Path may begin with `~`.
|
||||
pub key_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl ServerOuterCertSection {
|
||||
/// Read the outer-TLS cert/key PEMs from disk.
|
||||
///
|
||||
/// Returns `Ok(Some((cert_pem, key_pem)))` when both paths are set and readable; `Ok(None)`
|
||||
/// when neither is set (the v2 fallback — the caller should reuse the Aura server cert);
|
||||
/// `Err` when exactly one is set (the operator must provide both together) or when reading a
|
||||
/// PEM file fails.
|
||||
pub fn resolve(&self) -> anyhow::Result<Option<(String, String)>> {
|
||||
match (&self.cert_path, &self.key_path) {
|
||||
(Some(c), Some(k)) => {
|
||||
let c_resolved = expand_tilde(&c.to_string_lossy());
|
||||
let k_resolved = expand_tilde(&k.to_string_lossy());
|
||||
let cert = fs::read_to_string(&c_resolved).with_context(|| {
|
||||
format!(
|
||||
"reading [server.outer_cert] cert_path {}",
|
||||
c_resolved.display()
|
||||
)
|
||||
})?;
|
||||
let key = fs::read_to_string(&k_resolved).with_context(|| {
|
||||
format!(
|
||||
"reading [server.outer_cert] key_path {}",
|
||||
k_resolved.display()
|
||||
)
|
||||
})?;
|
||||
Ok(Some((cert, key)))
|
||||
}
|
||||
(None, None) => Ok(None),
|
||||
_ => Err(anyhow!(
|
||||
"[server.outer_cert]: cert_path and key_path must be set together \
|
||||
(got one but not the other)"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `[server.nat]` section: v2 auto-NAT configuration. See [`crate::nat`] for the apply / rollback
|
||||
/// semantics. Optional — when the section is omitted the server makes no changes to the host's
|
||||
/// IP forwarding state, matching v1 behaviour.
|
||||
|
||||
Reference in New Issue
Block a user