Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
The `tls_client_metadata` metadata field added by TCP-based sources with `client_metadata_key`
set now includes `subject_altnames`, containing the Subject Alternative Names (DNS names, email
addresses, URIs, and IP addresses) from the client TLS certificate. Each SAN is prefixed with its
type (for example `DNS:example.com`, `email:admin@example.com`, `URI:https://example.com`, or
`IP Address:127.0.0.1`) to match the output of `openssl x509 -text`. This key is only added when
the client certificate contains Subject Alternative Names.

authors: emillen
111 changes: 110 additions & 1 deletion lib/vector-core/src/tls/incoming.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::{
collections::HashMap,
future::Future,
net::SocketAddr,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
pin::Pin,
sync::Arc,
task::{Context, Poll},
Expand Down Expand Up @@ -400,6 +400,7 @@ pub struct CertificateMetadata {
pub organization_name: Option<String>,
pub organizational_unit_name: Option<String>,
pub common_name: Option<String>,
pub subject_altnames: Vec<String>,
}

impl CertificateMetadata {
Expand All @@ -425,6 +426,10 @@ impl CertificateMetadata {
}
components.join(",")
}

pub fn subject_altnames(&self) -> String {
self.subject_altnames.join(",")
}
}

impl From<X509> for CertificateMetadata {
Expand All @@ -434,13 +439,45 @@ impl From<X509> for CertificateMetadata {
let data_string = entry.data().to_string().unwrap_or_default();
subject_metadata.insert(entry.object().to_string(), data_string);
}
let subject_altnames = cert
.subject_alt_names()
.map(|names| {
names
.iter()
.filter_map(|name| {
if let Some(dns) = name.dnsname() {
Some(format!("DNS:{dns}"))
} else if let Some(email) = name.email() {
Some(format!("email:{email}"))
} else if let Some(uri) = name.uri() {
Some(format!("URI:{uri}"))
} else {
name.ipaddress().and_then(|ip| match ip.len() {
4 => Some(format!(
"IP Address:{}",
IpAddr::V4(Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]))
)),
16 => Some(format!(
"IP Address:{}",
IpAddr::V6(Ipv6Addr::from(u128::from_be_bytes(
ip.try_into().expect("ip address length checked"),
)))
)),
_ => None,
})
}
})
.collect()
})
.unwrap_or_default();
Self {
country_name: subject_metadata.get("countryName").cloned(),
state_or_province_name: subject_metadata.get("stateOrProvinceName").cloned(),
locality_name: subject_metadata.get("localityName").cloned(),
organization_name: subject_metadata.get("organizationName").cloned(),
organizational_unit_name: subject_metadata.get("organizationalUnitName").cloned(),
common_name: subject_metadata.get("commonName").cloned(),
subject_altnames,
}
}
}
Expand Down Expand Up @@ -472,6 +509,13 @@ impl Connected for MaybeTlsIncomingStream<TcpStream> {

#[cfg(test)]
mod test {
use openssl::{
hash::MessageDigest,
pkey::PKey,
rsa::Rsa,
x509::{X509NameBuilder, extension::SubjectAlternativeName},
};

use super::*;

#[test]
Expand All @@ -483,6 +527,7 @@ mod test {
organization_name: Some("organization".to_owned()),
organizational_unit_name: Some("org_unit".to_owned()),
state_or_province_name: Some("state".to_owned()),
subject_altnames: vec!["DNS:example.com".to_owned()],
};

let expected = format!(
Expand All @@ -506,6 +551,7 @@ mod test {
organization_name: Some("organization".to_owned()),
organizational_unit_name: Some("org_unit".to_owned()),
state_or_province_name: None,
subject_altnames: vec![],
};

let expected = format!(
Expand All @@ -517,4 +563,67 @@ mod test {
);
assert_eq!(expected, example_meta.subject());
}

#[test]
fn certificate_metadata_subject_altnames() {
let example_meta = CertificateMetadata {
common_name: None,
country_name: None,
locality_name: None,
organization_name: None,
organizational_unit_name: None,
state_or_province_name: None,
subject_altnames: vec![
"DNS:example.com".to_owned(),
"IP Address:1.2.3.4".to_owned(),
],
};
assert_eq!(
"DNS:example.com,IP Address:1.2.3.4",
example_meta.subject_altnames()
);

let empty_meta = CertificateMetadata {
common_name: None,
country_name: None,
locality_name: None,
organization_name: None,
organizational_unit_name: None,
state_or_province_name: None,
subject_altnames: vec![],
};
assert_eq!("", empty_meta.subject_altnames());
}

#[test]
fn certificate_metadata_from_x509() {
let key = PKey::from_rsa(Rsa::generate(2048).unwrap()).unwrap();

let mut name = X509NameBuilder::new().unwrap();
name.append_entry_by_text("CN", "example.com").unwrap();
let name = name.build();

let mut builder = X509::builder().unwrap();
builder.set_version(2).unwrap();
builder.set_subject_name(&name).unwrap();
builder.set_issuer_name(&name).unwrap();
builder.set_pubkey(&key).unwrap();
let san = SubjectAlternativeName::new()
.dns("example.com")
.email("admin@example.com")
.uri("https://example.com")
.ip("1.2.3.4")
.build(&builder.x509v3_context(None, None))
.unwrap();
builder.append_extension(san).unwrap();
builder.sign(&key, MessageDigest::sha256()).unwrap();
let cert = builder.build();

let metadata = CertificateMetadata::from(cert);

assert_eq!(
"DNS:example.com,email:admin@example.com,URI:https://example.com,IP Address:1.2.3.4",
metadata.subject_altnames()
);
}
}
8 changes: 2 additions & 6 deletions src/sources/dnstap/tcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::{
internal_events::{SocketEventsReceived, SocketMode},
sources::util::{
framestream::{FrameHandler, TcpFrameHandler},
net::SocketListenAddr,
net::{SocketListenAddr, build_tls_client_metadata},
},
};

Expand Down Expand Up @@ -266,11 +266,7 @@ impl<T: FrameHandler + Clone> TcpFrameHandler for DnstapFrameHandler<T> {
}

fn insert_tls_client_metadata(&mut self, metadata: Option<CertificateMetadata>) {
self.tls_client_metadata = metadata.map(|c| {
let mut metadata = ObjectMap::new();
metadata.insert("subject".into(), c.subject().into());
metadata
});
self.tls_client_metadata = metadata.map(|c| build_tls_client_metadata(&c));
}

fn allowed_origins(&self) -> Option<&[IpNet]> {
Expand Down
2 changes: 1 addition & 1 deletion src/sources/util/net/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use vector_lib::configurable::configurable_component;
#[cfg(feature = "sources-utils-net-tcp")]
pub use self::tcp::{
MAX_IN_FLIGHT_EVENTS_TARGET, TcpNullAcker, TcpSource, TcpSourceAck, TcpSourceAcker,
request_limiter::RequestLimiter, try_bind_tcp_listener,
build_tls_client_metadata, request_limiter::RequestLimiter, try_bind_tcp_listener,
};
#[cfg(feature = "sources-utils-net-udp")]
pub use self::udp::try_bind_udp_socket;
Expand Down
62 changes: 60 additions & 2 deletions src/sources/util/net/tcp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,8 +383,7 @@ async fn handle_stream<T>(


if let Some(certificate_metadata) = &certificate_metadata {
let mut metadata = ObjectMap::new();
metadata.insert("subject".into(), certificate_metadata.subject().into());
let metadata = build_tls_client_metadata(certificate_metadata);
for event in &mut events {
let log = event.as_mut_log();

Expand Down Expand Up @@ -477,3 +476,62 @@ fn close_socket(socket: &MaybeTlsIncomingStream<TcpStream>) -> bool {
true
}
}

const TLS_CLIENT_METADATA_SUBJECT_KEY: &str = "subject";
const TLS_CLIENT_METADATA_SUBJECT_ALTNAMES_KEY: &str = "subject_altnames";

pub fn build_tls_client_metadata(certificate_metadata: &CertificateMetadata) -> ObjectMap {
let mut metadata = ObjectMap::new();
metadata.insert(
TLS_CLIENT_METADATA_SUBJECT_KEY.into(),
certificate_metadata.subject().into(),
);
let subject_altnames = certificate_metadata.subject_altnames();
if !subject_altnames.is_empty() {
metadata.insert(
TLS_CLIENT_METADATA_SUBJECT_ALTNAMES_KEY.into(),
subject_altnames.into(),
);
}
metadata
}

#[cfg(test)]
mod test {
use super::*;
use vrl::value::Value;

fn metadata_with_subject_altnames(subject_altnames: Vec<String>) -> CertificateMetadata {
CertificateMetadata {
country_name: None,
state_or_province_name: None,
locality_name: None,
organization_name: None,
organizational_unit_name: None,
common_name: Some("common".to_owned()),
subject_altnames,
}
}

#[test]
fn tls_client_metadata_with_subject_altnames() {
let metadata = build_tls_client_metadata(&metadata_with_subject_altnames(vec![
"DNS:example.com".to_owned(),
"IP Address:1.2.3.4".to_owned(),
]));

assert_eq!(metadata.get("subject"), Some(&Value::from("CN=common")));
assert_eq!(
metadata.get("subject_altnames"),
Some(&Value::from("DNS:example.com,IP Address:1.2.3.4"))
);
}

#[test]
fn tls_client_metadata_without_subject_altnames() {
let metadata = build_tls_client_metadata(&metadata_with_subject_altnames(vec![]));

assert_eq!(metadata.get("subject"), Some(&Value::from("CN=common")));
assert!(!metadata.contains_key("subject_altnames"));
}
}
8 changes: 8 additions & 0 deletions website/cue/reference/components/sources.cue
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,14 @@ components: sources: [Name=string]: {
examples: ["CN=localhost,OU=Vector,O=Datadog,L=New York,ST=New York,C=US"]
}
}
subject_altnames: {
description: "The Subject Alternative Names (SANs) from the client TLS certificate, as a comma-separated list. Each SAN is prefixed with its type (for example `DNS:example.com`, `email:admin@example.com`, `URI:https://example.com`, or `IP Address:127.0.0.1`) to match the output of `openssl x509 -text`. Only added if `tls.client_metadata_key` is set and the client certificate contains SANs. Key name depends on configured `client_metadata_key`"
required: false
type: string: {
default: null
examples: ["DNS:localhost,IP Address:127.0.0.1"]
}
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions website/cue/reference/components/sources/dnstap.cue
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,7 @@ components: sources: dnstap: {
}
}
}
client_metadata: fields._client_metadata
}
}

Expand Down
48 changes: 48 additions & 0 deletions website/layouts/partials/logs_output.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,54 @@
</div>
{{ end }}
{{ end }}

{{ with index $v.type "object" }}
{{ with .options }}
<div class="mt-3 border rounded flex flex-col divide-y dark:divide-gray-700 dark:border-gray-700">
{{ range $name, $opt := . }}
<div class="py-2.5 px-3.5">
<span class="flex justify-between items-center">
<span class="font-mono font-semibold">
{{ $name }}
</span>

<span class="flex space-x-1">
{{ if $opt.required }}
{{ partial "badge.html" (dict "word" "required" "color" "red") }}
{{ else }}
{{ partial "badge.html" (dict "word" "optional" "color" "blue") }}
{{ end }}

{{ range $t, $tv := $opt.type }}
{{ partial "badge.html" (dict "word" $t "color" "gray") }}
{{ end }}
</span>
</span>

{{ with $opt.description }}
<div class="mt-2 prose dark:prose-invert">
{{ . | markdownify }}
</div>
{{ end }}

{{ range $t, $tv := $opt.type }}
{{ with $tv.examples }}
<div class="mt-2">
<span> Examples </span>

<div class="mt-1.5 flex flex-col space-y-1 text-sm">
{{ range . }}
<span class="font-mono text-sm text-secondary dark:text-primary">{{ . }}</span>
{{ end }}
</div>
</div>
{{ end }}
{{ end }}
</div>
{{ end }}
</div>
{{ end }}
{{ end }}
</div>
{{ end }}
</div>
Expand Down
Loading