Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/manifest/orchestrator/startup_runtime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ pub fn refreshServiceRuntimeBindings(
const reg = backend_registry orelse return;
const target = tlsBackendTargetForService(alloc, svc, tls.domain, record.ip_address) orelse return;
defer alloc.free(target.ip);
reg.register(tls.domain, target.ip, target.port) catch {
reg.register(tls.domain, target.ip, target.port, tls.peer) catch {
log.warn("failed to refresh backend for {s}", .{tls.domain});
return;
};
Expand Down Expand Up @@ -317,7 +317,7 @@ fn registerTlsBackends(
const target = tlsBackendTargetForService(alloc, svc, tls.domain, record.ip_address) orelse continue;
defer alloc.free(target.ip);

reg.register(tls.domain, target.ip, target.port) catch {
reg.register(tls.domain, target.ip, target.port, tls.peer) catch {
log.warn("failed to register backend for {s}", .{tls.domain});
continue;
};
Expand Down
60 changes: 50 additions & 10 deletions src/tls/backend.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@
// and the orchestrator lifecycle run on different threads.

const std = @import("std");
const spec = @import("../manifest/spec.zig");

pub const Backend = struct {
ip: []const u8,
port: u16,
/// service-to-service mTLS posture for inbound traffic to this
/// backend's service. `.off` keeps the legacy (TLS-terminate only)
/// behavior; `.warn` and `.require` flip the listener to mTLS.
peer_mode: spec.TlsConfig.PeerMode = .off,
};

pub const BackendRegistry = struct {
Expand All @@ -37,15 +42,21 @@ pub const BackendRegistry = struct {
}

/// register a backend for a domain. overwrites any existing mapping.
pub fn register(self: *BackendRegistry, domain: []const u8, ip: []const u8, port: u16) !void {
pub fn register(
self: *BackendRegistry,
domain: []const u8,
ip: []const u8,
port: u16,
peer_mode: spec.TlsConfig.PeerMode,
) !void {
self.mutex.lockUncancelable(std.Options.debug_io);
defer self.mutex.unlock(std.Options.debug_io);

// if domain already exists, free old values
if (self.backends.getEntry(domain)) |entry| {
self.allocator.free(entry.value_ptr.ip);
const new_ip = try self.allocator.dupe(u8, ip);
entry.value_ptr.* = .{ .ip = new_ip, .port = port };
entry.value_ptr.* = .{ .ip = new_ip, .port = port, .peer_mode = peer_mode };
return;
}

Expand All @@ -54,7 +65,7 @@ pub const BackendRegistry = struct {
const owned_ip = try self.allocator.dupe(u8, ip);
errdefer self.allocator.free(owned_ip);

try self.backends.put(self.allocator, owned_domain, .{ .ip = owned_ip, .port = port });
try self.backends.put(self.allocator, owned_domain, .{ .ip = owned_ip, .port = port, .peer_mode = peer_mode });
}

/// remove a backend for a domain.
Expand Down Expand Up @@ -86,6 +97,7 @@ pub const BackendRegistry = struct {
return .{
.ip = try alloc.dupe(u8, backend.ip),
.port = backend.port,
.peer_mode = backend.peer_mode,
};
}
};
Expand All @@ -97,7 +109,7 @@ test "register and lookup" {
var reg = BackendRegistry.init(alloc);
defer reg.deinit();

try reg.register("example.com", "10.42.0.5", 8080);
try reg.register("example.com", "10.42.0.5", 8080, .off);

const backend = reg.lookup("example.com");
try std.testing.expect(backend != null);
Expand All @@ -118,8 +130,8 @@ test "register overwrites existing" {
var reg = BackendRegistry.init(alloc);
defer reg.deinit();

try reg.register("example.com", "10.42.0.5", 8080);
try reg.register("example.com", "10.42.0.10", 9090);
try reg.register("example.com", "10.42.0.5", 8080, .off);
try reg.register("example.com", "10.42.0.10", 9090, .off);

const backend = reg.lookup("example.com");
try std.testing.expect(backend != null);
Expand All @@ -132,12 +144,40 @@ test "unregister removes backend" {
var reg = BackendRegistry.init(alloc);
defer reg.deinit();

try reg.register("example.com", "10.42.0.5", 8080);
try reg.register("example.com", "10.42.0.5", 8080, .off);
reg.unregister("example.com");

try std.testing.expect(reg.lookup("example.com") == null);
}

test "register and lookup carry peer_mode through" {
const alloc = std.testing.allocator;
var reg = BackendRegistry.init(alloc);
defer reg.deinit();

try reg.register("api.example", "10.0.0.1", 8443, .require);

const got = reg.lookup("api.example").?;
try std.testing.expectEqual(spec.TlsConfig.PeerMode.require, got.peer_mode);

const owned = (try reg.lookupOwned(alloc, "api.example")).?;
defer alloc.free(owned.ip);
try std.testing.expectEqual(spec.TlsConfig.PeerMode.require, owned.peer_mode);
}

test "register without peer_mode defaults to off via overwrite path" {
const alloc = std.testing.allocator;
var reg = BackendRegistry.init(alloc);
defer reg.deinit();

try reg.register("api.example", "10.0.0.1", 8443, .require);
try reg.register("api.example", "10.0.0.2", 9090, .off);

const got = reg.lookup("api.example").?;
try std.testing.expectEqual(spec.TlsConfig.PeerMode.off, got.peer_mode);
try std.testing.expectEqualStrings("10.0.0.2", got.ip);
}

test "unregister nonexistent is safe" {
const alloc = std.testing.allocator;
var reg = BackendRegistry.init(alloc);
Expand All @@ -151,8 +191,8 @@ test "multiple domains" {
var reg = BackendRegistry.init(alloc);
defer reg.deinit();

try reg.register("a.com", "10.42.0.1", 80);
try reg.register("b.com", "10.42.0.2", 443);
try reg.register("a.com", "10.42.0.1", 80, .off);
try reg.register("b.com", "10.42.0.2", 443, .off);

const a = reg.lookup("a.com");
const b = reg.lookup("b.com");
Expand All @@ -167,7 +207,7 @@ test "lookupOwned returns stable backend copy" {
var reg = BackendRegistry.init(alloc);
defer reg.deinit();

try reg.register("example.com", "10.42.0.5", 8080);
try reg.register("example.com", "10.42.0.5", 8080, .off);
const owned = (try reg.lookupOwned(alloc, "example.com")).?;
defer alloc.free(owned.ip);

Expand Down
23 changes: 23 additions & 0 deletions src/tls/proxy.zig
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const backend_mod = @import("backend.zig");
const acme_mod = @import("acme.zig");
const managed_runtime = @import("acme/managed_runtime.zig");
const runtime_wait = @import("../lib/runtime_wait.zig");
const store_mod = @import("../state/store.zig");

const max_connections: u32 = 256;
var active_connections: std.atomic.Value(u32) = std.atomic.Value(u32).init(0);
Expand Down Expand Up @@ -444,6 +445,27 @@ pub const TlsProxy = struct {
};
defer self.allocator.free(backend.ip);

// build MtlsOpts when the service has tls.peer set. the trust
// root is the cluster CA loaded from raft state. missing CA on
// a service that asked for mtls is a misconfiguration; fall
// back to plain TLS and log loudly.
var mtls_ca_rec: ?store_mod.ClusterCaRecord = null;
defer if (mtls_ca_rec) |rec| rec.deinit(self.allocator);

const mtls_opts: ?session_runtime.MtlsOpts = blk: {
if (backend.peer_mode == .off) break :blk null;
const rec = (store_mod.getClusterCa(self.allocator) catch null) orelse {
log.warn("tls.peer set for {s} but cluster CA not yet seeded; downgrading to plain TLS", .{server_name});
break :blk null;
};
mtls_ca_rec = rec;
break :blk session_runtime.MtlsOpts{
.require_client_cert = backend.peer_mode == .require,
.trust_ca_pem = rec.cert_pem,
.now_unix = std.Io.Clock.real.now(std.Options.debug_io).toSeconds(),
};
};

// perform TLS handshake and proxy traffic
session_runtime.handleTlsSession(
self.threaded_io.io(),
Expand All @@ -453,6 +475,7 @@ pub const TlsProxy = struct {
cert_result.key_pem,
backend,
&handshake_complete,
mtls_opts,
) catch |err| {
log.warn("TLS session error for {s}: {}", .{ server_name, err });
};
Expand Down
7 changes: 6 additions & 1 deletion src/tls/proxy/session_runtime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ pub fn handleTlsSession(
key_pem: []u8,
backend_info: backend_mod.Backend,
handshake_complete: *bool,
mtls_opts: ?MtlsOpts,
) !void {
var session = try acceptServerHandshake(
io,
Expand All @@ -211,11 +212,15 @@ pub fn handleTlsSession(
client_hello,
cert_pem,
key_pem,
null,
mtls_opts,
handshake_complete,
);
defer session.deinit(std.heap.page_allocator);

if (session.peer_identity) |peer| {
log.info("tls: mtls peer accepted: {s}", .{peer});
}

const selected_alpn = session.selected_alpn;
const app_keys = session.app_keys;

Expand Down
Loading