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
80 changes: 80 additions & 0 deletions src/http_module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ struct MainConf : MainConfBase {
std::map<StrView, StrView> resourceAttrs;
bool ssl;
std::string trustedCert;
std::string clientKey;
std::string clientCert;
Target::HeaderVec headers;
};

Expand All @@ -50,6 +52,8 @@ char* setExporter(ngx_conf_t* cf, ngx_command_t* cmd, void* conf);
char* addResourceAttr(ngx_conf_t* cf, ngx_command_t* cmd, void* conf);
char* addSpanAttr(ngx_conf_t* cf, ngx_command_t* cmd, void* conf);
char* setTrustedCertificate(ngx_conf_t* cf, ngx_command_t* cmd, void* conf);
char* setClientCertificateChain(ngx_conf_t* cf, ngx_command_t* cmd, void* conf);
char* setClientPrivateKey(ngx_conf_t* cf, ngx_command_t* cmd, void* conf);
char* addExporterHeader(ngx_conf_t* cf, ngx_command_t* cmd, void* conf);

namespace Propagation {
Expand Down Expand Up @@ -122,6 +126,14 @@ ngx_command_t gExporterCommands[] = {
NGX_CONF_TAKE1,
setTrustedCertificate },

{ ngx_string("client_private_key"),
NGX_CONF_TAKE1,
setClientPrivateKey },

{ ngx_string("client_certificate_chain"),
NGX_CONF_TAKE1,
setClientCertificateChain },

{ ngx_string("header"),
NGX_CONF_TAKE2,
addExporterHeader },
Expand Down Expand Up @@ -586,6 +598,8 @@ ngx_int_t initWorkerProcess(ngx_cycle_t* cycle)
target.endpoint = std::string(toStrView(mcf->endpoint));
target.ssl = mcf->ssl;
target.trustedCert = mcf->trustedCert;
target.clientKey = mcf->clientKey;
target.clientCert = mcf->clientCert;
target.headers = mcf->headers;

gExporter.reset(new BatchExporter(
Expand Down Expand Up @@ -754,6 +768,72 @@ char* setTrustedCertificate(ngx_conf_t* cf, ngx_command_t* cmd, void* conf)
return NGX_CONF_OK;
}

char* setClientCertificateChain(ngx_conf_t* cf, ngx_command_t* cmd, void* conf)
{
auto path = ((ngx_str_t*)cf->args->elts)[1];
auto mcf = getMainConf(cf);

if (ngx_get_full_name(cf->pool, &cf->cycle->conf_prefix, &path) != NGX_OK) {
return (char*)NGX_CONF_ERROR;
}

try {
std::ifstream file{(const char*)path.data, std::ios::binary};
if (!file.is_open()) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, ngx_errno,
"failed to open \"%V\"", &path);
return (char*)NGX_CONF_ERROR;
}
file.exceptions(std::ios::failbit | std::ios::badbit);
file.peek(); // trigger early error for dirs

size_t size = file.seekg(0, std::ios::end).tellg();
file.seekg(0);

mcf->clientCert.resize(size);
file.read(&mcf->clientCert[0], size);
} catch (const std::exception& e) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"failed to read \"%V\": %s", &path, e.what());
return (char*)NGX_CONF_ERROR;
}

return NGX_CONF_OK;
}

char* setClientPrivateKey(ngx_conf_t* cf, ngx_command_t* cmd, void* conf)
{
auto path = ((ngx_str_t*)cf->args->elts)[1];
auto mcf = getMainConf(cf);

if (ngx_get_full_name(cf->pool, &cf->cycle->conf_prefix, &path) != NGX_OK) {
return (char*)NGX_CONF_ERROR;
}

try {
std::ifstream file{(const char*)path.data, std::ios::binary};
if (!file.is_open()) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, ngx_errno,
"failed to open \"%V\"", &path);
return (char*)NGX_CONF_ERROR;
}
file.exceptions(std::ios::failbit | std::ios::badbit);
file.peek(); // trigger early error for dirs

size_t size = file.seekg(0, std::ios::end).tellg();
file.seekg(0);

mcf->clientKey.resize(size);
file.read(&mcf->clientKey[0], size);
} catch (const std::exception& e) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"failed to read \"%V\": %s", &path, e.what());
return (char*)NGX_CONF_ERROR;
}

return NGX_CONF_OK;
}

char* addExporterHeader(ngx_conf_t* cf, ngx_command_t* cmd, void* conf)
{
auto args = (ngx_str_t*)cf->args->elts;
Expand Down
4 changes: 4 additions & 0 deletions src/trace_service_client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ struct Target {
std::string endpoint;
bool ssl;
std::string trustedCert;
std::string clientKey;
std::string clientCert;
HeaderVec headers;

static bool validateHeaderName(StrView name)
Expand Down Expand Up @@ -44,6 +46,8 @@ class TraceServiceClient {
if (target.ssl) {
grpc::SslCredentialsOptions options;
options.pem_root_certs = target.trustedCert;
options.pem_private_key = target.clientKey;
options.pem_cert_chain = target.clientCert;

creds = grpc::SslCredentials(options);
} else {
Expand Down
20 changes: 20 additions & 0 deletions tests/test_otel.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,3 +329,23 @@ def test_tls_export(client, trace_service):
assert client.get("http://127.0.0.1:18080/ok").status_code == 200

assert trace_service.get_span().name == "/ok"


@pytest.mark.parametrize(
"nginx_config",
[
{
"endpoint": "https://localhost:14319",
"exporter_opts": """
trusted_certificate localhost.crt;
client_private_key localhost.key;
client_certificate_chain localhost.crt;
""",
}
],
indirect=True,
)
def test_mtls_export(client, trace_service):
assert client.get("https://127.0.0.1:18080/ok").status_code == 200

assert trace_service.get_span().name == "/ok"
8 changes: 7 additions & 1 deletion tests/trace_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,15 @@ def trace_service(request, pytestconfig, logger, cert):
listen_addr = f"127.0.0.1:{24317 if trace_service.use_otelcol else 14317}"
server.add_insecure_port(listen_addr)
if not trace_service.use_otelcol:
# TLS (server-only auth)
creds = grpc.ssl_server_credentials([cert])
server.add_secure_port("127.0.0.1:14318", creds)
listen_addr += " and 127.0.0.1:14318"
# mTLS (require client cert)
mtls_creds = grpc.ssl_server_credentials(
[cert], root_certificates=cert[1], require_client_auth=True
)
server.add_secure_port("127.0.0.1:14319", mtls_creds)
listen_addr += " and 127.0.0.1:14318 and 127.0.0.1:14319"
logger.info(f"Starting trace service at {listen_addr}...")
server.start()
yield trace_service
Expand Down