From 7ec92619787d168c5be876b913524bfe839b136b Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Mon, 30 Oct 2023 21:47:01 +0100 Subject: [PATCH] feat: automate AppSec enablement setup (e.g: `AWS_LAMBDA_RUNTIME_API`) (#143) * feat: honor AWS_LAMBDA_EXEC_WRAPPER when AWS Lambda does not In order to simplify onboarding & make it more uniform across languages, inspect the value of the `AWS_LAMBDA_EXEC_WRAPPER` environment variable and apply select environment variable changes it perofrms upon decorating a handler. This is necessary/useful because that environment variable is not honored by custom runtimes (`provided`, `provided.al2`) as well as the `go1.x` runtime (which is a glorified provided runtime). The datadog Lambda wrapper starts a proxy to inject ASM functionality directly on the Lambda runtime API instead of having to manually instrument each and every lambda handler/application, and modifies `AWS_LAMBDA_RUNTIME_API` to instruct Lambda language runtime client libraries to go through it instead of directly interacting with the Lambda control plane. APPSEC-11534 * pivot to a different, cheaper strategy * typo fix * PR feedback * minor fixups * add warning in go1.x runtime if lambda.norpc build tag was not enabled --- awslambdanorpc.go | 14 ++++++++++ awslambdawithrpc.go | 14 ++++++++++ ddlambda.go | 59 ++++++++++++++++++++++++++++++++++++++++++ internal/logger/log.go | 36 ++++++++++++++++---------- 4 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 awslambdanorpc.go create mode 100644 awslambdawithrpc.go diff --git a/awslambdanorpc.go b/awslambdanorpc.go new file mode 100644 index 00000000..287a285d --- /dev/null +++ b/awslambdanorpc.go @@ -0,0 +1,14 @@ +//go:build lambda.norpc +// +build lambda.norpc + +/* + * Unless explicitly stated otherwise all files in this repository are licensed + * under the Apache License Version 2.0. + * + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2021 Datadog, Inc. + */ + +package ddlambda + +const awsLambdaRpcSupport = false diff --git a/awslambdawithrpc.go b/awslambdawithrpc.go new file mode 100644 index 00000000..994629a1 --- /dev/null +++ b/awslambdawithrpc.go @@ -0,0 +1,14 @@ +//go:build !lambda.norpc +// +build !lambda.norpc + +/* +* Unless explicitly stated otherwise all files in this repository are licensed +* under the Apache License Version 2.0. +* +* This product includes software developed at Datadog (https://www.datadoghq.com/). +* Copyright 2021 Datadog, Inc. + */ + +package ddlambda + +const awsLambdaRpcSupport = true diff --git a/ddlambda.go b/ddlambda.go index a848f49a..1782bcb2 100644 --- a/ddlambda.go +++ b/ddlambda.go @@ -96,11 +96,27 @@ const ( DefaultSite = "datadoghq.com" // DefaultEnhancedMetrics enables enhanced metrics by default. DefaultEnhancedMetrics = true + + // serverlessAppSecEnabledEnvVar is the environment variable used to activate Serverless ASM through the use of an + // AWS Lambda runtime API proxy. + serverlessAppSecEnabledEnvVar = "DD_SERVERLESS_APPSEC_ENABLED" + // awsLambdaRuntimeApiEnvVar is the environment variable used to redirect AWS Lambda runtime API calls to the proxy. + awsLambdaRuntimeApiEnvVar = "AWS_LAMBDA_RUNTIME_API" + // datadogAgentUrl is the URL of the agent and proxy started by the Datadog lambda extension. + datadogAgentUrl = "127.0.0.1:9000" + // ddExtensionFilePath is the path on disk of the datadog lambda extension. + ddExtensionFilePath = "/opt/extensions/datadog-agent" + + // awsLambdaServerPortEnvVar is the environment variable set by the go1.x Lambda Runtime to indicate which port the + // RCP server should listen on. This is used as a sign that a warning should be printed if customers want to enable + // ASM support, but did not enable the lambda.norpc build taf. + awsLambdaServerPortEnvVar = "_LAMBDA_SERVER_PORT" ) // WrapLambdaHandlerInterface is used to instrument your lambda functions. // It returns a modified handler that can be passed directly to the lambda.StartHandler function from aws-lambda-go. func WrapLambdaHandlerInterface(handler lambda.Handler, cfg *Config) lambda.Handler { + setupAppSec() listeners := initializeListeners(cfg) return wrapper.WrapHandlerInterfaceWithListeners(handler, listeners...) } @@ -108,6 +124,7 @@ func WrapLambdaHandlerInterface(handler lambda.Handler, cfg *Config) lambda.Hand // WrapFunction is used to instrument your lambda functions. // It returns a modified handler that can be passed directly to the lambda.Start function from aws-lambda-go. func WrapFunction(handler interface{}, cfg *Config) interface{} { + setupAppSec() listeners := initializeListeners(cfg) return wrapper.WrapHandlerWithListeners(handler, listeners...) } @@ -289,3 +306,45 @@ func (cfg *Config) toMetricsConfig(isExtensionRunning bool) metrics.Config { return mc } + +// setupAppSec checks if DD_SERVERLESS_APPSEC_ENABLED is set (to true) and when that +// is the case, redirects `AWS_LAMBDA_RUNTIME_API` to the agent extension, and turns +// on universal instrumentation unless it was already configured by the customer, so +// that the HTTP context (invocation details span tags) is available on AppSec traces. +func setupAppSec() { + enabled := false + if env := os.Getenv(serverlessAppSecEnabledEnvVar); env != "" { + if on, err := strconv.ParseBool(env); err == nil { + enabled = on + } + } + + if !enabled { + return + } + + if _, err := os.Stat(ddExtensionFilePath); os.IsNotExist(err) { + logger.Debug(fmt.Sprintf("%s is enabled, but the Datadog extension was not found at %s", serverlessAppSecEnabledEnvVar, ddExtensionFilePath)) + return + } + + if awsLambdaRpcSupport { + if port := os.Getenv(awsLambdaServerPortEnvVar); port != "" { + logger.Warn(fmt.Sprintf("%s activation with the go1.x AWS Lambda runtime requires setting the `lambda.norpc` go build tag", serverlessAppSecEnabledEnvVar)) + } + } + + if err := os.Setenv(awsLambdaRuntimeApiEnvVar, datadogAgentUrl); err != nil { + logger.Debug(fmt.Sprintf("failed to set %s=%s: %v", awsLambdaRuntimeApiEnvVar, datadogAgentUrl, err)) + } else { + logger.Debug(fmt.Sprintf("successfully set %s=%s", awsLambdaRuntimeApiEnvVar, datadogAgentUrl)) + } + + if val := os.Getenv(UniversalInstrumentation); val == "" { + if err := os.Setenv(UniversalInstrumentation, "1"); err != nil { + logger.Debug(fmt.Sprintf("failed to set %s=%d: %v", UniversalInstrumentation, 1, err)) + } else { + logger.Debug(fmt.Sprintf("successfully set %s=%d", UniversalInstrumentation, 1)) + } + } +} diff --git a/internal/logger/log.go b/internal/logger/log.go index 44b8c7eb..c2eec046 100644 --- a/internal/logger/log.go +++ b/internal/logger/log.go @@ -14,12 +14,12 @@ type LogLevel int const ( // LevelDebug logs all information LevelDebug LogLevel = iota - // LevelError only logs errors - LevelError LogLevel = iota + // LevelWarn only logs warnings and errors + LevelWarn LogLevel = iota ) var ( - logLevel = LevelError + logLevel = LevelWarn output io.Writer = os.Stdout ) @@ -36,12 +36,6 @@ func SetOutput(w io.Writer) { // Error logs a structured error message to stdout func Error(err error) { - - type logStructure struct { - Status string `json:"status"` - Message string `json:"message"` - } - finalMessage := logStructure{ Status: "error", Message: fmt.Sprintf("datadog: %s", err.Error()), @@ -56,10 +50,6 @@ func Debug(message string) { if logLevel > LevelDebug { return } - type logStructure struct { - Status string `json:"status"` - Message string `json:"message"` - } finalMessage := logStructure{ Status: "debug", Message: fmt.Sprintf("datadog: %s", message), @@ -70,7 +60,27 @@ func Debug(message string) { log.Println(string(result)) } +// Warn logs a structured log message to stdout +func Warn(message string) { + if logLevel > LevelWarn { + return + } + finalMessage := logStructure{ + Status: "warning", + Message: fmt.Sprintf("datadog: %s", message), + } + + result, _ := json.Marshal(finalMessage) + + log.Println(string(result)) +} + // Raw prints a raw message to the logs. func Raw(message string) { fmt.Fprintln(output, message) } + +type logStructure struct { + Status string `json:"status"` + Message string `json:"message"` +}