diff --git a/extensions/common/context.cc b/extensions/common/context.cc index 41ca68adc35..494834b797b 100644 --- a/extensions/common/context.cc +++ b/extensions/common/context.cc @@ -363,5 +363,16 @@ void populateTCPRequestInfo(bool outbound, RequestInfo* request_info, request_info->request_protocol = kProtocolTCP; } +bool getAuditPolicy() { + bool shouldAudit = false; + if (!getValue( + {"metadata", "filter_metadata", "envoy.common", "access_log_hint"}, + &shouldAudit)) { + return false; + } + + return shouldAudit; +} + } // namespace Common } // namespace Wasm diff --git a/extensions/common/context.h b/extensions/common/context.h index 3a28f463858..861580f190d 100644 --- a/extensions/common/context.h +++ b/extensions/common/context.h @@ -235,5 +235,9 @@ void populateExtendedRequestInfo(RequestInfo* request_info); void populateTCPRequestInfo(bool outbound, RequestInfo* request_info, const std::string& destination_namespace); +// Read value of 'access_log_hint' key from envoy dynamic metadata which +// determines whether to audit a request or not. +bool getAuditPolicy(); + } // namespace Common } // namespace Wasm diff --git a/extensions/stackdriver/config/v1alpha1/stackdriver_plugin_config.proto b/extensions/stackdriver/config/v1alpha1/stackdriver_plugin_config.proto index 2085a8a492f..caa8e740b2b 100644 --- a/extensions/stackdriver/config/v1alpha1/stackdriver_plugin_config.proto +++ b/extensions/stackdriver/config/v1alpha1/stackdriver_plugin_config.proto @@ -29,7 +29,7 @@ import "google/protobuf/wrappers.proto"; // next id: 12 message PluginConfig { - // Types of Access logs to export. + // Types of Access logs to export. Does not affect audit logging. enum AccessLogging { // No Logs. NONE = 0; @@ -45,6 +45,9 @@ message PluginConfig { // This is deprecated in favor of AccessLogging enum. bool disable_server_access_logging = 1 [deprecated = true]; + // Optional. Controls whether to export audit log. + bool enable_audit_log = 11; + // Optional. FQDN of destination service that the request routed to, e.g. // productpage.default.svc.cluster.local. If not provided, request host header // will be used instead diff --git a/extensions/stackdriver/log/logger.cc b/extensions/stackdriver/log/logger.cc index d178ab66c01..25b4af40da1 100644 --- a/extensions/stackdriver/log/logger.cc +++ b/extensions/stackdriver/log/logger.cc @@ -70,22 +70,27 @@ void setMonitoredResource( log_entries_request->mutable_resource()->CopyFrom(monitored_resource); } -// Helper methods to fill destination Labels. +// Helper methods to fill destination Labels. Which labels are filled depends on +// if the entry is audit or not. void fillDestinationLabels( const ::Wasm::Common::FlatNode& destination_node_info, - google::protobuf::Map* label_map) { - (*label_map)["destination_name"] = - flatbuffers::GetString(destination_node_info.name()); + google::protobuf::Map* label_map, bool audit) { (*label_map)["destination_workload"] = flatbuffers::GetString(destination_node_info.workload_name()); (*label_map)["destination_namespace"] = flatbuffers::GetString(destination_node_info.namespace_()); + // Don't set if audit request + if (!audit) { + (*label_map)["destination_name"] = + flatbuffers::GetString(destination_node_info.name()); + } + // Add destination app and version label if exist. const auto local_labels = destination_node_info.labels(); if (local_labels) { auto version_iter = local_labels->LookupByKey("version"); - if (version_iter) { + if (version_iter && !audit) { (*label_map)["destination_version"] = flatbuffers::GetString(version_iter->value()); } @@ -107,11 +112,15 @@ void fillDestinationLabels( } } -// Helper methods to fill source Labels. +// Helper methods to fill source Labels. The labels filled depends on whether +// the log entry is audit or not. void fillSourceLabels( const ::Wasm::Common::FlatNode& source_node_info, - google::protobuf::Map* label_map) { - (*label_map)["source_name"] = flatbuffers::GetString(source_node_info.name()); + google::protobuf::Map* label_map, bool audit) { + if (!audit) { + (*label_map)["source_name"] = + flatbuffers::GetString(source_node_info.name()); + } (*label_map)["source_workload"] = flatbuffers::GetString(source_node_info.workload_name()); (*label_map)["source_namespace"] = @@ -120,7 +129,7 @@ void fillSourceLabels( const auto local_labels = source_node_info.labels(); if (local_labels) { auto version_iter = local_labels->LookupByKey("version"); - if (version_iter) { + if (version_iter && !audit) { (*label_map)["source_version"] = flatbuffers::GetString(version_iter->value()); } @@ -150,18 +159,26 @@ constexpr char kServerAccessLogName[] = "server-accesslog-stackdriver"; // Name of the client access log. constexpr char kClientAccessLogName[] = "client-accesslog-stackdriver"; +// Name of the server audit access log. +constexpr char kServerAuditLogName[] = "server-istio-audit-log"; +// Name of the client audit access log. +constexpr char kClientAuditLogName[] = "client-istio-audit-log"; + void Logger::initializeLogEntryRequest( const flatbuffers::Vector>* platform_metadata, - const ::Wasm::Common::FlatNode& local_node_info, bool outbound) { - auto log_entry_type = GetLogEntryType(outbound); + const ::Wasm::Common::FlatNode& local_node_info, bool outbound, + bool audit) { + LogEntryType log_entry_type = GetLogEntryType(outbound, audit); log_entries_request_map_[log_entry_type]->request = std::make_unique(); log_entries_request_map_[log_entry_type]->size = 0; auto log_entries_request = log_entries_request_map_[log_entry_type]->request.get(); const std::string& log_name = - outbound ? kClientAccessLogName : kServerAccessLogName; + audit ? (outbound ? kClientAuditLogName : kServerAuditLogName) + : (outbound ? kClientAccessLogName : kServerAccessLogName); + log_entries_request->set_log_name("projects/" + project_id_ + "/logs/" + log_name); @@ -178,10 +195,14 @@ void Logger::initializeLogEntryRequest( setMonitoredResource(local_node_info, resource_type, log_entries_request); auto label_map = log_entries_request->mutable_labels(); - (*label_map)["mesh_uid"] = flatbuffers::GetString(local_node_info.mesh_id()); - // Set common destination labels shared by all inbound/server entries. - outbound ? fillSourceLabels(local_node_info, label_map) - : fillDestinationLabels(local_node_info, label_map); + if (!audit) { + (*label_map)["mesh_uid"] = + flatbuffers::GetString(local_node_info.mesh_id()); + } + + // Set common labels shared by all client entries or server entries + outbound ? fillSourceLabels(local_node_info, label_map, audit) + : fillDestinationLabels(local_node_info, label_map, audit); } Logger::Logger(const ::Wasm::Common::FlatNode& local_node_info, @@ -195,14 +216,22 @@ Logger::Logger(const ::Wasm::Common::FlatNode& local_node_info, } // Initalize the current WriteLogEntriesRequest for client/server - log_entries_request_map_[Logger::LogEntryType::Client] = + log_entries_request_map_[LogEntryType::Client] = std::make_unique(); initializeLogEntryRequest(platform_metadata, local_node_info, - true /* outbound */); + true /*outbound */, false /* audit */); log_entries_request_map_[Logger::LogEntryType::Server] = std::make_unique(); initializeLogEntryRequest(platform_metadata, local_node_info, - false /* outbound */); + false /* outbound */, false /* audit */); + log_entries_request_map_[LogEntryType::ClientAudit] = + std::make_unique(); + initializeLogEntryRequest(platform_metadata, local_node_info, + true /*outbound */, true /* audit */); + log_entries_request_map_[Logger::LogEntryType::ServerAudit] = + std::make_unique(); + initializeLogEntryRequest(platform_metadata, local_node_info, + false /* outbound */, true /* audit */); log_request_size_limit_ = log_request_size_limit; exporter_ = std::move(exporter); @@ -210,24 +239,26 @@ Logger::Logger(const ::Wasm::Common::FlatNode& local_node_info, void Logger::addTcpLogEntry(const ::Wasm::Common::RequestInfo& request_info, const ::Wasm::Common::FlatNode& peer_node_info, - long int log_time, bool outbound) { + long int log_time, bool outbound, bool audit) { // create a new log entry - auto* log_entries = log_entries_request_map_[GetLogEntryType(outbound)] + auto* log_entries = log_entries_request_map_[GetLogEntryType(outbound, audit)] ->request->mutable_entries(); auto* new_entry = log_entries->Add(); *new_entry->mutable_timestamp() = google::protobuf::util::TimeUtil::NanosecondsToTimestamp(log_time); - addTCPLabelsToLogEntry(request_info, peer_node_info, outbound, new_entry); - fillAndFlushLogEntry(request_info, peer_node_info, outbound, new_entry); + addTCPLabelsToLogEntry(request_info, peer_node_info, new_entry, outbound, + audit); + fillAndFlushLogEntry(request_info, peer_node_info, new_entry, outbound, + audit); } void Logger::addLogEntry(const ::Wasm::Common::RequestInfo& request_info, const ::Wasm::Common::FlatNode& peer_node_info, - bool outbound) { + bool outbound, bool audit) { // create a new log entry - auto* log_entries = log_entries_request_map_[GetLogEntryType(outbound)] + auto* log_entries = log_entries_request_map_[GetLogEntryType(outbound, audit)] ->request->mutable_entries(); auto* new_entry = log_entries->Add(); @@ -235,40 +266,44 @@ void Logger::addLogEntry(const ::Wasm::Common::RequestInfo& request_info, google::protobuf::util::TimeUtil::NanosecondsToTimestamp( request_info.start_time); fillHTTPRequestInLogEntry(request_info, new_entry); - fillAndFlushLogEntry(request_info, peer_node_info, outbound, new_entry); + fillAndFlushLogEntry(request_info, peer_node_info, new_entry, outbound, + audit); } void Logger::fillAndFlushLogEntry( const ::Wasm::Common::RequestInfo& request_info, - const ::Wasm::Common::FlatNode& peer_node_info, bool outbound, - google::logging::v2::LogEntry* new_entry) { + const ::Wasm::Common::FlatNode& peer_node_info, + google::logging::v2::LogEntry* new_entry, bool outbound, bool audit) { new_entry->set_severity(::google::logging::type::INFO); auto label_map = new_entry->mutable_labels(); if (outbound) { - fillDestinationLabels(peer_node_info, label_map); + fillDestinationLabels(peer_node_info, label_map, audit); } else { - fillSourceLabels(peer_node_info, label_map); + fillSourceLabels(peer_node_info, label_map, audit); } (*label_map)["destination_service_host"] = request_info.destination_service_host; - (*label_map)["response_flag"] = request_info.response_flag; (*label_map)["destination_principal"] = request_info.destination_principal; (*label_map)["source_principal"] = request_info.source_principal; - (*label_map)["service_authentication_policy"] = - std::string(::Wasm::Common::AuthenticationPolicyString( - request_info.service_auth_policy)); - (*label_map)["protocol"] = request_info.request_protocol; - (*label_map)["log_sampled"] = request_info.log_sampled ? "true" : "false"; - (*label_map)["connection_id"] = std::to_string(request_info.connection_id); - (*label_map)["route_name"] = request_info.route_name; - (*label_map)["upstream_host"] = request_info.upstream_host; - (*label_map)["upstream_cluster"] = request_info.upstream_cluster; - (*label_map)["requested_server_name"] = request_info.request_serever_name; - (*label_map)["x-envoy-original-path"] = request_info.x_envoy_original_path; - (*label_map)["x-envoy-original-dst-host"] = - request_info.x_envoy_original_dst_host; + + if (!audit) { + (*label_map)["response_flag"] = request_info.response_flag; + (*label_map)["service_authentication_policy"] = + std::string(::Wasm::Common::AuthenticationPolicyString( + request_info.service_auth_policy)); + (*label_map)["protocol"] = request_info.request_protocol; + (*label_map)["log_sampled"] = request_info.log_sampled ? "true" : "false"; + (*label_map)["connection_id"] = std::to_string(request_info.connection_id); + (*label_map)["route_name"] = request_info.route_name; + (*label_map)["upstream_host"] = request_info.upstream_host; + (*label_map)["upstream_cluster"] = request_info.upstream_cluster; + (*label_map)["requested_server_name"] = request_info.request_serever_name; + (*label_map)["x-envoy-original-path"] = request_info.x_envoy_original_path; + (*label_map)["x-envoy-original-dst-host"] = + request_info.x_envoy_original_dst_host; + } // Insert trace headers, if exist. if (request_info.b3_trace_sampled) { @@ -278,9 +313,9 @@ void Logger::fillAndFlushLogEntry( new_entry->set_trace_sampled(request_info.b3_trace_sampled); } + LogEntryType log_entry_type = GetLogEntryType(outbound, audit); // Accumulate estimated size of the request. If the current request exceeds // the size limit, flush the request out. - auto log_entry_type = GetLogEntryType(outbound); log_entries_request_map_[log_entry_type]->size += new_entry->ByteSizeLong(); if (log_entries_request_map_[log_entry_type]->size > log_request_size_limit_) { @@ -288,7 +323,7 @@ void Logger::fillAndFlushLogEntry( } } -void Logger::flush(Logger::LogEntryType log_entry_type) { +void Logger::flush(LogEntryType log_entry_type) { auto request = log_entries_request_map_[log_entry_type]->request.get(); std::unique_ptr cur = std::make_unique(); @@ -331,43 +366,34 @@ bool Logger::exportLogEntry(bool is_on_done) { void Logger::addTCPLabelsToLogEntry( const ::Wasm::Common::RequestInfo& request_info, - const ::Wasm::Common::FlatNode& peer_node_info, bool outbound, - google::logging::v2::LogEntry* log_entry) { + const ::Wasm::Common::FlatNode& peer_node_info, + google::logging::v2::LogEntry* log_entry, bool outbound, bool audit) { + const auto& entries_request = + log_entries_request_map_[GetLogEntryType(outbound, audit)]->request; auto label_map = log_entry->mutable_labels(); std::string source, destination; - auto log_entry_type = GetLogEntryType(outbound); if (outbound) { setDestinationCanonicalService(peer_node_info, label_map); auto source_cs_iter = - log_entries_request_map_[log_entry_type]->request->labels().find( - "source_canonical_service"); + entries_request->labels().find("source_canonical_service"); auto destination_cs_iter = label_map->find("destination_canonical_service"); - source = - source_cs_iter != log_entries_request_map_[log_entry_type] - ->request->labels() - .end() - ? source_cs_iter->second - : log_entries_request_map_[log_entry_type]->request->labels().at( - "source_workload"); + source = source_cs_iter != entries_request->labels().end() + ? source_cs_iter->second + : entries_request->labels().at("source_workload"); destination = destination_cs_iter != label_map->end() ? destination_cs_iter->second : request_info.destination_service_name; } else { setSourceCanonicalService(peer_node_info, label_map); auto source_cs_iter = label_map->find("source_canonical_service"); - auto log_entry_type = GetLogEntryType(outbound); auto destination_cs_iter = - log_entries_request_map_[log_entry_type]->request->labels().find( - "destination_canonical_service"); + entries_request->labels().find("destination_canonical_service"); source = source_cs_iter != label_map->end() ? source_cs_iter->second : flatbuffers::GetString(peer_node_info.workload_name()); - destination = - destination_cs_iter != log_entries_request_map_[log_entry_type] - ->request->labels() - .end() - ? destination_cs_iter->second - : request_info.destination_service_name; + destination = destination_cs_iter != entries_request->labels().end() + ? destination_cs_iter->second + : request_info.destination_service_name; } log_entry->set_text_payload(absl::StrCat(source, " --> ", destination)); (*label_map)["source_ip"] = request_info.source_address; diff --git a/extensions/stackdriver/log/logger.h b/extensions/stackdriver/log/logger.h index 5c0ca447dd4..3baee4a8827 100644 --- a/extensions/stackdriver/log/logger.h +++ b/extensions/stackdriver/log/logger.h @@ -41,16 +41,40 @@ class Logger { std::unique_ptr exporter, int log_request_size_limit = 4000000 /* 4 Mb */); + // Type of log entry. + enum LogEntryType { Client, ClientAudit, Server, ServerAudit }; + // Add a new log entry based on the given request information and peer node - // information. + // information. The type of entry that is added depends on outbound and audit + // arguments. + // + // Audit labels: + // - destination_canonical_revision + // - destination_canonical_service + // - destination_namespace + // - destination_principal + // - destination_service_host + // - destination_app + // - destination_workload + // - request_id + // - source_app + // - source_canonical_revision + // - source_canonical_service + // - source_namespace + // - source_workload + // - source_principal + // void addLogEntry(const ::Wasm::Common::RequestInfo& request_info, const ::Wasm::Common::FlatNode& peer_node_info, - bool outbound); + bool outbound, bool audit); + // Add a new tcp log entry based on the given request information and peer - // node information. + // node information. The type of entry that is added depends on outbound and + // audit arguments. void addTcpLogEntry(const ::Wasm::Common::RequestInfo& request_info, const ::Wasm::Common::FlatNode& peer_node_info, - long int log_time, bool outbound); + long int log_time, bool outbound, bool audit); + // Export and clean the buffered WriteLogEntriesRequests. Returns true if // async call is made to export log entry, otherwise returns false if nothing // exported. @@ -65,43 +89,54 @@ class Logger { int size; }; - // Type of log Entry. - enum LogEntryType { Client, Server }; - // Flush rotates the current WriteLogEntriesRequest. This will be triggered // either by a timer or by request size limit. Returns false if there is no // log entry to be exported. bool flush(); - void flush(Logger::LogEntryType log_entry_type); + void flush(LogEntryType log_entry_type); - // Add TCP Specific labels to LogEntry. + // Add TCP Specific labels to LogEntry. Which labels are set depends on if + // the entry is an audit entry or not void addTCPLabelsToLogEntry(const ::Wasm::Common::RequestInfo& request_info, const ::Wasm::Common::FlatNode& peer_node_info, - bool outbound, - google::logging::v2::LogEntry* log_entry); + google::logging::v2::LogEntry* log_entry, + bool outbound, bool audit); // Fill Http_Request entry in LogEntry. void fillHTTPRequestInLogEntry( const ::Wasm::Common::RequestInfo& request_info, google::logging::v2::LogEntry* log_entry); - // Generic method to fill log entry and flush it. + // Generic method to fill the log entry. The WriteLogEntriesRequest + // containing the log entry is flushed if the request exceeds the configured + // maximum size. Which request should be flushed is determined by the outbound + // and audit arguments. void fillAndFlushLogEntry(const ::Wasm::Common::RequestInfo& request_info, const ::Wasm::Common::FlatNode& peer_node_info, - bool outbound, - google::logging::v2::LogEntry* new_entry); + google::logging::v2::LogEntry* new_entry, + bool outbound, bool audit); - // Helper method to initialize log entry request. + // Helper method to initialize log entry request. The type of log entry is + // determined by the oubound and audit arguments. void initializeLogEntryRequest( const flatbuffers::Vector>* platform_metadata, - const ::Wasm::Common::FlatNode& local_node_info, bool outbound); + const ::Wasm::Common::FlatNode& local_node_info, bool outbound, + bool audit); // Helper method to get Log Entry Type. - Logger::LogEntryType GetLogEntryType(bool outbound) { + Logger::LogEntryType GetLogEntryType(bool outbound, bool audit) const { if (outbound) { + if (audit) { + return Logger::LogEntryType::ClientAudit; + } return Logger::LogEntryType::Client; } + + if (audit) { + return Logger::LogEntryType::ServerAudit; + } + return Logger::LogEntryType::Server; } diff --git a/extensions/stackdriver/log/logger_test.cc b/extensions/stackdriver/log/logger_test.cc index 1a2dfb2f744..731c2596e4c 100644 --- a/extensions/stackdriver/log/logger_test.cc +++ b/extensions/stackdriver/log/logger_test.cc @@ -134,6 +134,52 @@ ::Wasm::Common::RequestInfo requestInfo() { return request_info; } +std::string write_audit_request_json = R"({ + "logName":"projects/test_project/logs/server-istio-audit-log", + "resource":{ + "type":"k8s_container", + "labels":{ + "cluster_name":"test_cluster", + "pod_name":"test_pod", + "location":"test_location", + "namespace_name":"test_namespace", + "project_id":"test_project", + "container_name":"istio-proxy" + } + }, + "labels":{ + "destination_workload":"test_workload", + "destination_namespace":"test_namespace" + }, + "entries":[ + { + "httpRequest":{ + "requestMethod":"GET", + "requestUrl":"http://httpbin.org/headers", + "userAgent":"chrome", + "remoteIp":"1.1.1.1", + "referer":"www.google.com", + "serverIp":"2.2.2.2", + "latency":"10s", + "protocol":"HTTP" + }, + "timestamp":"1970-01-01T00:00:00Z", + "severity":"INFO", + "labels":{ + "destination_principal":"destination_principal", + "destination_service_host":"httpbin.org", + "request_id":"123", + "source_namespace":"test_peer_namespace", + "source_principal":"source_principal", + "source_workload":"test_peer_workload", + }, + "trace":"projects/test_project/traces/123abc", + "spanId":"abc123", + "traceSampled":true + } + ] +})"; + std::string write_log_request_json = R"({ "logName":"projects/test_project/logs/server-accesslog-stackdriver", "resource":{ @@ -195,10 +241,12 @@ std::string write_log_request_json = R"({ })"; google::logging::v2::WriteLogEntriesRequest expectedRequest( - int log_entry_count) { + int log_entry_count, bool for_audit = false) { google::logging::v2::WriteLogEntriesRequest req; google::protobuf::util::JsonParseOptions options; - JsonStringToMessage(write_log_request_json, &req, options); + JsonStringToMessage( + (for_audit ? write_audit_request_json : write_log_request_json), &req, + options); for (int i = 1; i < log_entry_count; i++) { auto* new_entry = req.mutable_entries()->Add(); new_entry->CopyFrom(req.entries()[0]); @@ -213,7 +261,7 @@ TEST(LoggerTest, TestWriteLogEntry) { auto exporter_ptr = exporter.get(); flatbuffers::FlatBufferBuilder local, peer; auto logger = std::make_unique(nodeInfo(local), std::move(exporter)); - logger->addLogEntry(requestInfo(), peerNodeInfo(peer), false); + logger->addLogEntry(requestInfo(), peerNodeInfo(peer), false, false); EXPECT_CALL(*exporter_ptr, exportLogs(::testing::_, ::testing::_)) .WillOnce(::testing::Invoke( [](const std::vector(nodeInfo(local), std::move(exporter), 1200); for (int i = 0; i < 10; i++) { - logger->addLogEntry(requestInfo(), peerNodeInfo(peer), false); + logger->addLogEntry(requestInfo(), peerNodeInfo(peer), false, false); } EXPECT_CALL(*exporter_ptr, exportLogs(::testing::_, ::testing::_)) .WillOnce(::testing::Invoke( @@ -259,6 +307,65 @@ TEST(LoggerTest, TestWriteLogEntryRotation) { logger->exportLogEntry(/* is_on_done = */ false); } +TEST(LoggerTest, TestWriteAuditEntry) { + auto exporter = std::make_unique<::testing::NiceMock>(); + auto exporter_ptr = exporter.get(); + flatbuffers::FlatBufferBuilder local, peer; + auto logger = std::make_unique(nodeInfo(local), std::move(exporter)); + logger->addLogEntry(requestInfo(), peerNodeInfo(peer), false, true); + EXPECT_CALL(*exporter_ptr, exportLogs(::testing::_, ::testing::_)) + .WillOnce(::testing::Invoke( + [](const std::vector>& requests, + bool) { + for (const auto& req : requests) { + std::string diff; + MessageDifferencer differ; + differ.ReportDifferencesToString(&diff); + if (!differ.Compare(expectedRequest(1, true), *req)) { + FAIL() << "unexpected audit entry " << diff << "\n"; + } + } + })); + logger->exportLogEntry(/* is_on_done = */ false); +} + +TEST(LoggerTest, TestWriteAuditAndLogEntry) { + auto exporter = std::make_unique<::testing::NiceMock>(); + auto exporter_ptr = exporter.get(); + flatbuffers::FlatBufferBuilder local, peer; + auto logger = std::make_unique(nodeInfo(local), std::move(exporter)); + for (int i = 0; i < 5; i++) { + logger->addLogEntry(requestInfo(), peerNodeInfo(peer), false, false); + logger->addLogEntry(requestInfo(), peerNodeInfo(peer), false, true); + } + EXPECT_CALL(*exporter_ptr, exportLogs(::testing::_, ::testing::_)) + .WillOnce(::testing::Invoke( + [](const std::vector>& requests, + bool) { + bool foundAudit = false; + bool foundLog = false; + std::string diff; + EXPECT_EQ(requests.size(), 2); + for (const auto& req : requests) { + MessageDifferencer differ; + differ.ReportDifferencesToString(&diff); + if (differ.Compare(expectedRequest(5, true), *req)) { + foundAudit = true; + } + + if (differ.Compare(expectedRequest(5, false), *req)) { + foundLog = true; + } + } + if (!(foundAudit && foundLog)) { + FAIL() << "unexpected entries, last difference: " << diff << "\n"; + } + })); + logger->exportLogEntry(/* is_on_done = */ false); +} + } // namespace Log } // namespace Stackdriver } // namespace Extensions diff --git a/extensions/stackdriver/stackdriver.cc b/extensions/stackdriver/stackdriver.cc index 308dc887b75..0b00aaa13f4 100644 --- a/extensions/stackdriver/stackdriver.cc +++ b/extensions/stackdriver/stackdriver.cc @@ -363,14 +363,24 @@ void StackdriverRootContext::record() { ::Extensions::Stackdriver::Metric::record( outbound, local_node, peer_node, request_info, !config_.disable_http_size_metrics()); + bool extended_info_populated = false; if ((enableAllAccessLog() || (enableAccessLogOnError() && (request_info.response_code >= 400 || request_info.response_flag != ::Wasm::Common::NONE))) && shouldLogThisRequest(request_info)) { ::Wasm::Common::populateExtendedHTTPRequestInfo(&request_info); - logger_->addLogEntry(request_info, peer_node, outbound); + extended_info_populated = true; + logger_->addLogEntry(request_info, peer_node, outbound, false /* audit */); } + + if (enableAuditLog() && shouldAuditThisRequest()) { + if (!extended_info_populated) { + ::Wasm::Common::populateExtendedHTTPRequestInfo(&request_info); + } + logger_->addLogEntry(request_info, peer_node, outbound, true /* audit */); + } + if (enableEdgeReporting()) { std::string peer_id; if (!getPeerId(peer_id)) { @@ -430,10 +440,12 @@ bool StackdriverRootContext::recordTCP(uint32_t id) { // Record TCP Metrics. ::Extensions::Stackdriver::Metric::recordTCP(outbound, local_node, peer_node, request_info); + bool extended_info_populated = false; // Add LogEntry to Logger. Log Entries are batched and sent on timer // to Stackdriver Logging Service. if (enableAllAccessLog() || (enableAccessLogOnError() && !no_error)) { ::Wasm::Common::populateExtendedRequestInfo(&request_info); + extended_info_populated = true; // It's possible that for a short lived TCP connection, we log TCP // Connection Open log entry on connection close. if (!record_info.tcp_open_entry_logged && @@ -442,13 +454,38 @@ bool StackdriverRootContext::recordTCP(uint32_t id) { record_info.request_info->tcp_connection_state = ::Wasm::Common::TCPConnectionState::Open; logger_->addTcpLogEntry(*record_info.request_info, peer_node, - record_info.request_info->start_time, outbound); + record_info.request_info->start_time, outbound, + false /* audit */); record_info.request_info->tcp_connection_state = ::Wasm::Common::TCPConnectionState::Close; } logger_->addTcpLogEntry(request_info, peer_node, - getCurrentTimeNanoseconds(), outbound); + getCurrentTimeNanoseconds(), outbound, + false /* audit */); + } + + if (enableAuditLog() && shouldAuditThisRequest()) { + if (!extended_info_populated) { + ::Wasm::Common::populateExtendedRequestInfo(&request_info); + } + // It's possible that for a short lived TCP connection, we audit log TCP + // Connection Open log entry on connection close. + if (!record_info.tcp_open_entry_logged && + request_info.tcp_connection_state == + ::Wasm::Common::TCPConnectionState::Close) { + record_info.request_info->tcp_connection_state = + ::Wasm::Common::TCPConnectionState::Open; + logger_->addTcpLogEntry(*record_info.request_info, peer_node, + record_info.request_info->start_time, outbound, + true /* audit */); + record_info.request_info->tcp_connection_state = + ::Wasm::Common::TCPConnectionState::Close; + } + logger_->addTcpLogEntry(*record_info.request_info, peer_node, + record_info.request_info->start_time, outbound, + true /* audit */); } + if (log_open_on_timeout) { // If we logged the request on timeout, for outbound requests, we try to // populate the request info again when metadata is available. @@ -481,6 +518,10 @@ inline bool StackdriverRootContext::enableAccessLogOnError() { stackdriver::config::v1alpha1::PluginConfig::ERRORS_ONLY; } +inline bool StackdriverRootContext::enableAuditLog() { + return config_.enable_audit_log(); +} + inline bool StackdriverRootContext::enableEdgeReporting() { return config_.enable_mesh_edges_reporting() && !isOutbound(); } @@ -497,6 +538,10 @@ bool StackdriverRootContext::shouldLogThisRequest( return request_info.log_sampled; } +bool StackdriverRootContext::shouldAuditThisRequest() { + return Wasm::Common::getAuditPolicy(); +} + void StackdriverRootContext::addToTCPRequestQueue(uint32_t id) { std::unique_ptr<::Wasm::Common::RequestInfo> request_info = std::make_unique<::Wasm::Common::RequestInfo>(); diff --git a/extensions/stackdriver/stackdriver.h b/extensions/stackdriver/stackdriver.h index aa5b222d15f..e870a4dfa28 100644 --- a/extensions/stackdriver/stackdriver.h +++ b/extensions/stackdriver/stackdriver.h @@ -119,6 +119,12 @@ class StackdriverRootContext : public RootContext { bool shouldLogThisRequest(::Wasm::Common::RequestInfo& request_info); + // Indicates whether to export server audit log or not. + bool enableAuditLog(); + + // Indicates whether the request should be logged based on audit policy + bool shouldAuditThisRequest(); + // Indicates whether or not to report edges to Stackdriver. bool enableEdgeReporting(); diff --git a/test/envoye2e/inventory.go b/test/envoye2e/inventory.go index d819c5c29b8..a5ddebbd342 100644 --- a/test/envoye2e/inventory.go +++ b/test/envoye2e/inventory.go @@ -52,6 +52,7 @@ func init() { "TestStatsParallel", "TestStatsGrpc", "TestTCPMetadataExchange", + "TestStackdriverAuditLog", "TestTCPMetadataExchangeNoAlpn", "TestAttributeGen", }, diff --git a/test/envoye2e/stackdriver_plugin/stackdriver_test.go b/test/envoye2e/stackdriver_plugin/stackdriver_test.go index e3cba779f56..6452bd18ff0 100644 --- a/test/envoye2e/stackdriver_plugin/stackdriver_test.go +++ b/test/envoye2e/stackdriver_plugin/stackdriver_test.go @@ -640,3 +640,69 @@ func TestStackdriverTCPMetadataExchange(t *testing.T) { }) } } + +func TestStackdriverAuditLog(t *testing.T) { + t.Parallel() + respCode := "200" + logEntryCount := 5 + + params := driver.NewTestParams(t, map[string]string{ + "ServiceAuthenticationPolicy": "NONE", + "DirectResponseCode": respCode, + "SDLogStatusCode": respCode, + "StackdriverRootCAFile": driver.TestPath("testdata/certs/stackdriver.pem"), + "StackdriverTokenFile": driver.TestPath("testdata/certs/access-token"), + }, envoye2e.ProxyE2ETests) + + sdPort := params.Ports.Max + 1 + stsPort := params.Ports.Max + 2 + params.Vars["SDPort"] = strconv.Itoa(int(sdPort)) + params.Vars["STSPort"] = strconv.Itoa(int(stsPort)) + params.Vars["ClientMetadata"] = params.LoadTestData("testdata/client_node_metadata.json.tmpl") + params.Vars["ServerMetadata"] = params.LoadTestData("testdata/server_node_metadata.json.tmpl") + params.Vars["ClientHTTPFilters"] = driver.LoadTestData("testdata/filters/mx_outbound.yaml.tmpl") + params.Vars["ServerHTTPFilters"] = params.LoadTestData("testdata/filters/rbac_log.yaml.tmpl") + "\n" + + driver.LoadTestData("testdata/filters/stackdriver_inbound.yaml.tmpl") + "\n" + driver.LoadTestData("testdata/filters/mx_inbound.yaml.tmpl") + sd := &Stackdriver{Port: sdPort} + intRespCode, _ := strconv.Atoi(respCode) + if err := (&driver.Scenario{ + Steps: []driver.Step{ + &driver.XDS{}, + sd, + &SecureTokenService{Port: stsPort}, + &driver.Update{Node: "client", Version: "0", Listeners: []string{ + params.LoadTestData("testdata/listener/client.yaml.tmpl"), + }}, + &driver.Update{Node: "server", Version: "0", Listeners: []string{ + params.LoadTestData("testdata/listener/server.yaml.tmpl"), + }}, + &driver.Envoy{Bootstrap: params.LoadTestData("testdata/bootstrap/server.yaml.tmpl")}, + &driver.Envoy{Bootstrap: params.LoadTestData("testdata/bootstrap/client.yaml.tmpl")}, + &driver.Sleep{Duration: 1 * time.Second}, + &driver.Repeat{ + N: logEntryCount, + Step: &driver.HTTPCall{ + Port: params.Ports.ClientPort, + ResponseCode: intRespCode, + }, + }, + sd.Check(params, + nil, []SDLogEntry{ + { + LogBaseFile: "testdata/stackdriver/server_access_log.yaml.tmpl", + LogEntryFile: []string{"testdata/stackdriver/server_access_log_entry.yaml.tmpl"}, + LogEntryCount: logEntryCount, + }, + { + LogBaseFile: "testdata/stackdriver/server_audit_log.yaml.tmpl", + LogEntryFile: []string{"testdata/stackdriver/server_audit_log_entry.yaml.tmpl"}, + LogEntryCount: logEntryCount, + }, + }, + nil, true, + ), + }, + }).Run(params); err != nil { + t.Fatal(err) + } +} diff --git a/testdata/filters/rbac_log.yaml.tmpl b/testdata/filters/rbac_log.yaml.tmpl new file mode 100644 index 00000000000..c475a00c395 --- /dev/null +++ b/testdata/filters/rbac_log.yaml.tmpl @@ -0,0 +1,13 @@ +- name: envoy.filters.http.rbac + typed_config: + "@type": type.googleapis.com/udpa.type.v1.TypedStruct + type_url: envoy.extensions.filters.http.rbac.v3.RBAC + value: + rules: + action: LOG + policies: + "test": + permissions: + - any: true + principals: + - any: true \ No newline at end of file diff --git a/testdata/filters/stackdriver_inbound.yaml.tmpl b/testdata/filters/stackdriver_inbound.yaml.tmpl index f4966425e9b..4a1792182bf 100644 --- a/testdata/filters/stackdriver_inbound.yaml.tmpl +++ b/testdata/filters/stackdriver_inbound.yaml.tmpl @@ -17,4 +17,4 @@ configuration: "@type": "type.googleapis.com/google.protobuf.StringValue" value: | - {"enableMeshEdgesReporting": "true", "meshEdgesReportingDuration": "1s"} + {"enableMeshEdgesReporting": "true", "meshEdgesReportingDuration": "1s", "enable_audit_log": true} diff --git a/testdata/filters/stackdriver_outbound.yaml.tmpl b/testdata/filters/stackdriver_outbound.yaml.tmpl index 512d691a68b..48fcb6f8498 100644 --- a/testdata/filters/stackdriver_outbound.yaml.tmpl +++ b/testdata/filters/stackdriver_outbound.yaml.tmpl @@ -18,7 +18,7 @@ "@type": "type.googleapis.com/google.protobuf.StringValue" value: | {{- if .Vars.JustSendErrorClientLog }} - {"access_logging": "ERRORS_ONLY"} + {"access_logging": "ERRORS_ONLY", "enable_audit_log": true} {{- else }} - {"access_logging": "FULL"} + {"access_logging": "FULL", "enable_audit_log": true} {{- end }} diff --git a/testdata/stackdriver/server_audit_log.yaml.tmpl b/testdata/stackdriver/server_audit_log.yaml.tmpl new file mode 100644 index 00000000000..24330e25627 --- /dev/null +++ b/testdata/stackdriver/server_audit_log.yaml.tmpl @@ -0,0 +1,16 @@ +labels: + destination_namespace: default + destination_workload: ratings-v1 + destination_app: ratings + destination_canonical_service: ratings + destination_canonical_revision: version-1 +logName: projects/test-project/logs/server-istio-audit-log +resource: + labels: + cluster_name: test-cluster + container_name: server + location: us-east4-b + namespace_name: default + pod_name: ratings-v1-84975bc778-pxz2w + project_id: test-project + type: k8s_container diff --git a/testdata/stackdriver/server_audit_log_entry.yaml.tmpl b/testdata/stackdriver/server_audit_log_entry.yaml.tmpl new file mode 100644 index 00000000000..9b0bce98796 --- /dev/null +++ b/testdata/stackdriver/server_audit_log_entry.yaml.tmpl @@ -0,0 +1,16 @@ +http_request: + request_method: "GET" + request_url: "http://127.0.0.1:{{ .Ports.ClientPort }}/{{ .Vars.RequestPath }}" + server_ip: "127.0.0.1:{{ .Ports.ServerPort }}" + protocol: "http" + status: {{ .Vars.SDLogStatusCode }} +labels: + destination_principal: "{{ .Vars.DestinationPrincipal }}" + destination_service_host: server.default.svc.cluster.local + source_principal: "{{ .Vars.SourcePrincipal }}" + source_workload: productpage-v1 + source_namespace: default + source_app: productpage + source_canonical_service: productpage-v1 + source_canonical_revision: version-1 +severity: INFO \ No newline at end of file