Skip to content

Commit 7de3319

Browse files
committed
Add explicit connection context to tracing events
Record source identity, source peer, destination authority, gateway endpoint, and tunnel outcomes directly on gateway and sidecar events so logs are self-contained.
1 parent 01afb9a commit 7de3319

9 files changed

Lines changed: 565 additions & 93 deletions

File tree

sidecar/src/bridge.rs

Lines changed: 283 additions & 20 deletions
Large diffs are not rendered by default.

sidecar/src/identity.rs

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use tss_esapi::structures::{
1919
};
2020
use tss_esapi::tcti_ldr::TctiNameConf;
2121
use tss_esapi::{Context, tss2_esys};
22+
use x509_parser::oid_registry::Oid;
2223
use x509_parser::prelude::*;
2324

2425
const PERSISTENT_HANDLE_START: u32 = 0x8100_0000;
@@ -60,17 +61,23 @@ pub fn parse_tpm_key_handle(value: &str) -> anyhow::Result<u32> {
6061

6162
pub struct TpmClientIdentity {
6263
cert_chain: Vec<CertificateDer<'static>>,
64+
source_identity: String,
6365
signing_key: Arc<TpmSigningKey>,
6466
}
6567

6668
impl TpmClientIdentity {
67-
pub fn load(tcti: &str, key_handle: u32, cert_path: &Path) -> anyhow::Result<Self> {
69+
pub fn load(
70+
tcti: &str,
71+
key_handle: u32,
72+
cert_path: &Path,
73+
identity_ext_oid: &str,
74+
) -> anyhow::Result<Self> {
6875
let cert_chain = load_certs(cert_path)?;
69-
let leaf_spki = certificate_spki(
70-
cert_chain
71-
.first()
72-
.context("client certificate chain must contain a leaf certificate")?,
73-
)?;
76+
let leaf_cert = cert_chain
77+
.first()
78+
.context("client certificate chain must contain a leaf certificate")?;
79+
let leaf_spki = certificate_spki(leaf_cert)?;
80+
let source_identity = certificate_identity(leaf_cert, identity_ext_oid)?;
7481

7582
let signing_key = Arc::new(TpmSigningKey::load(tcti, key_handle)?);
7683
ensure!(
@@ -80,10 +87,15 @@ impl TpmClientIdentity {
8087

8188
Ok(Self {
8289
cert_chain,
90+
source_identity,
8391
signing_key,
8492
})
8593
}
8694

95+
pub fn source_identity(&self) -> &str {
96+
&self.source_identity
97+
}
98+
8799
pub fn resolver(self) -> Arc<dyn ResolvesClientCert> {
88100
Arc::new(StaticTpmClientCert {
89101
certified_key: Arc::new(CertifiedKey::new(self.cert_chain, self.signing_key)),
@@ -253,6 +265,29 @@ fn certificate_spki(cert: &CertificateDer<'_>) -> anyhow::Result<Vec<u8>> {
253265
Ok(cert.tbs_certificate.subject_pki.raw.to_vec())
254266
}
255267

268+
fn certificate_identity(
269+
cert: &CertificateDer<'_>,
270+
identity_ext_oid: &str,
271+
) -> anyhow::Result<String> {
272+
let oid = Oid::from_str(identity_ext_oid)
273+
.map_err(|e| anyhow::anyhow!("invalid client identity extension OID: {e:?}"))?;
274+
let (_, cert) =
275+
X509Certificate::from_der(cert.as_ref()).context("parsing client certificate")?;
276+
let ext = cert
277+
.tbs_certificate
278+
.extensions()
279+
.iter()
280+
.find(|ext| ext.oid == oid)
281+
.with_context(|| format!("missing client identity extension {identity_ext_oid}"))?;
282+
let (remaining, value) = x509_parser::asn1_rs::Utf8String::from_der(ext.value)
283+
.context("decoding client identity extension as UTF8String")?;
284+
ensure!(
285+
remaining.is_empty(),
286+
"client identity extension contains trailing bytes"
287+
);
288+
Ok(value.string().to_owned())
289+
}
290+
256291
fn p256_spki_from_tpm_public(public: &Public) -> anyhow::Result<Vec<u8>> {
257292
let Public::Ecc {
258293
parameters, unique, ..

sidecar/src/main.rs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ use tracing::{error, info, warn};
1717
use crate::bridge::GatewayConnector;
1818
use crate::identity::{TpmClientIdentity, parse_tpm_key_handle};
1919

20+
const DEFAULT_CLIENT_IDENTITY_EXT_OID: &str = "1.3.6.1.4.1.57264.1.1";
21+
2022
#[derive(Parser)]
2123
#[command(
2224
name = "agent_gateway_sidecar",
@@ -35,6 +37,10 @@ struct Cli {
3537
#[arg(long)]
3638
client_cert: PathBuf,
3739

40+
/// Client certificate extension OID that contains the source identity
41+
#[arg(long, default_value = DEFAULT_CLIENT_IDENTITY_EXT_OID)]
42+
client_identity_ext_oid: String,
43+
3844
/// TCTI string for the simulated TPM
3945
#[arg(long, default_value = "swtpm:host=127.0.0.1,port=2321")]
4046
tpm_tcti: String,
@@ -71,10 +77,17 @@ async fn main() -> anyhow::Result<()> {
7177
}
7278

7379
let tpm_key_handle = parse_tpm_key_handle(&cli.tpm_key_handle)?;
74-
let identity = TpmClientIdentity::load(&cli.tpm_tcti, tpm_key_handle, &cli.client_cert)?;
80+
let identity = TpmClientIdentity::load(
81+
&cli.tpm_tcti,
82+
tpm_key_handle,
83+
&cli.client_cert,
84+
&cli.client_identity_ext_oid,
85+
)?;
86+
let source_identity = identity.source_identity().to_owned();
7587
let tls_config = build_client_config(identity, &cli.ca_cert)?;
7688

7789
let (gateway_host, gateway_port) = parse_gateway_addr(&cli.gateway)?;
90+
let gateway_endpoint = format_endpoint(&gateway_host, gateway_port);
7891
let gateway_sni = ServerName::try_from(gateway_host.clone())
7992
.context("gateway hostname is not a valid SNI value")?;
8093

@@ -83,10 +96,17 @@ async fn main() -> anyhow::Result<()> {
8396
gateway_sni,
8497
gateway_host,
8598
gateway_port,
99+
gateway_endpoint.clone(),
100+
source_identity.clone(),
86101
));
87102

88103
let listener = TcpListener::bind(cli.listen).await?;
89-
info!(listen = %cli.listen, gateway = %cli.gateway, "sidecar ready");
104+
info!(
105+
source_identity = %source_identity,
106+
listen = %cli.listen,
107+
gateway_endpoint = %gateway_endpoint,
108+
"sidecar ready"
109+
);
90110

91111
loop {
92112
let (stream, peer) = match listener.accept().await {
@@ -100,8 +120,8 @@ async fn main() -> anyhow::Result<()> {
100120

101121
let connector = connector.clone();
102122
tokio::spawn(async move {
103-
if let Err(e) = bridge::serve_connection(stream, connector).await {
104-
error!(%peer, error = %e, "connection error");
123+
if let Err(e) = bridge::serve_connection(stream, peer, connector).await {
124+
error!(source_peer_addr = %peer, error = %e, "connection error");
105125
}
106126
});
107127
}
@@ -158,6 +178,14 @@ fn parse_gateway_addr(addr: &str) -> anyhow::Result<(String, u16)> {
158178
}
159179
}
160180

181+
fn format_endpoint(host: &str, port: u16) -> String {
182+
if host.contains(':') {
183+
format!("[{host}]:{port}")
184+
} else {
185+
format!("{host}:{port}")
186+
}
187+
}
188+
161189
#[cfg(test)]
162190
mod tests {
163191
use super::*;
@@ -183,6 +211,12 @@ mod tests {
183211
assert_eq!(port, 8443);
184212
}
185213

214+
#[test]
215+
fn format_endpoint_brackets_ipv6() {
216+
assert_eq!(format_endpoint("127.0.0.1", 8443), "127.0.0.1:8443");
217+
assert_eq!(format_endpoint("::1", 8443), "[::1]:8443");
218+
}
219+
186220
#[test]
187221
fn parse_rejects_missing_port() {
188222
assert!(parse_gateway_addr("127.0.0.1").is_err());

src/main.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,21 +83,21 @@ async fn serve_loop(
8383
let tls_stream = match acceptor.accept(tcp_stream).await {
8484
Ok(s) => s,
8585
Err(e) => {
86-
error!(%peer_addr, error = %e, "TLS handshake failed");
86+
error!(source_peer_addr = %peer_addr, error = %e, "TLS handshake failed");
8787
return;
8888
}
8989
};
9090

9191
let peer_certs = proxy::extract_peer_certs(tls_stream.get_ref().1);
92-
let service = make_svc.make_service(peer_certs);
92+
let service = make_svc.make_service(peer_certs, peer_addr);
9393

9494
let io = hyper_util::rt::TokioIo::new(tls_stream);
9595
if let Err(e) = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
9696
.http2_only()
9797
.serve_connection_with_upgrades(io, service)
9898
.await
9999
{
100-
error!(%peer_addr, error = %e, "connection error");
100+
error!(source_peer_addr = %peer_addr, error = %e, "connection error");
101101
}
102102
});
103103
}

src/policy.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,13 @@ pub struct RequestContext {
1414
}
1515

1616
pub enum PolicyDecision {
17-
Allow,
18-
Deny { reason: String },
17+
Allow {
18+
source_identity: String,
19+
},
20+
Deny {
21+
source_identity: Option<String>,
22+
reason: String,
23+
},
1924
}
2025

2126
#[async_trait]
@@ -126,6 +131,7 @@ impl PolicyEngine for TomlPolicyEngine {
126131
// remaining entries are intermediates used for chain-of-trust validation.
127132
let Some(peer_cert_der) = ctx.peer_certificates.first() else {
128133
return PolicyDecision::Deny {
134+
source_identity: None,
129135
reason: "no client certificate".into(),
130136
};
131137
};
@@ -134,6 +140,7 @@ impl PolicyEngine for TomlPolicyEngine {
134140
Ok((_, cert)) => cert,
135141
Err(e) => {
136142
return PolicyDecision::Deny {
143+
source_identity: None,
137144
reason: format!("failed to parse client certificate: {e}"),
138145
};
139146
}
@@ -146,6 +153,7 @@ impl PolicyEngine for TomlPolicyEngine {
146153
.find(|e| e.oid == self.client_ext_oid)
147154
else {
148155
return PolicyDecision::Deny {
156+
source_identity: None,
149157
reason: format!("missing required extension {}", self.client_ext_oid),
150158
};
151159
};
@@ -154,20 +162,23 @@ impl PolicyEngine for TomlPolicyEngine {
154162
Ok((remaining, v)) => {
155163
if !remaining.is_empty() {
156164
return PolicyDecision::Deny {
165+
source_identity: None,
157166
reason: "extension value contains trailing bytes".into(),
158167
};
159168
}
160169
v.string()
161170
}
162171
Err(e) => {
163172
return PolicyDecision::Deny {
173+
source_identity: None,
164174
reason: format!("failed to decode extension as UTF8String: {e}"),
165175
};
166176
}
167177
};
168178

169179
let Some(allowed) = self.rules.get(&ext_value) else {
170180
return PolicyDecision::Deny {
181+
source_identity: Some(ext_value.to_owned()),
171182
reason: format!("no rules for extension value {ext_value:?}"),
172183
};
173184
};
@@ -176,14 +187,18 @@ impl PolicyEngine for TomlPolicyEngine {
176187
Ok(d) => d,
177188
Err(e) => {
178189
return PolicyDecision::Deny {
190+
source_identity: Some(ext_value.to_owned()),
179191
reason: format!("invalid destination: {e}"),
180192
};
181193
}
182194
};
183195
if allowed.contains(&normalized_dest) {
184-
PolicyDecision::Allow
196+
PolicyDecision::Allow {
197+
source_identity: ext_value.to_owned(),
198+
}
185199
} else {
186200
PolicyDecision::Deny {
201+
source_identity: Some(ext_value.to_owned()),
187202
reason: format!(
188203
"destination {:?} not allowed for {:?}",
189204
ctx.destination, ext_value

0 commit comments

Comments
 (0)