Skip to content

Commit b761b28

Browse files
feat: new DD_LAMBDA_FIPS_MODE control for secrets and metrics
1 parent 7d21380 commit b761b28

File tree

5 files changed

+107
-13
lines changed

5 files changed

+107
-13
lines changed

datadog_lambda/api.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
import os
21
import logging
2+
import os
3+
4+
from datadog_lambda.fips import enable_fips_mode
35

46
logger = logging.getLogger(__name__)
57
KMS_ENCRYPTION_CONTEXT_KEY = "LambdaFunctionName"
68
api_key = None
79

810

911
def decrypt_kms_api_key(kms_client, ciphertext):
10-
from botocore.exceptions import ClientError
1112
import base64
1213

14+
from botocore.exceptions import ClientError
15+
1316
"""
1417
Decodes and deciphers the base64-encoded ciphertext given as a parameter using KMS.
1518
For this to work properly, the Lambda function must have the appropriate IAM permissions.
@@ -63,10 +66,9 @@ def get_api_key() -> str:
6366
DD_API_KEY = os.environ.get("DD_API_KEY", os.environ.get("DATADOG_API_KEY", ""))
6467

6568
LAMBDA_REGION = os.environ.get("AWS_REGION", "")
66-
is_gov_region = LAMBDA_REGION.startswith("us-gov-")
67-
if is_gov_region:
69+
if enable_fips_mode:
6870
logger.debug(
69-
"Govcloud region detected. Using FIPs endpoints for secrets management."
71+
"FIPS mode is enabled, using FIPS endpoints for secrets management."
7072
)
7173

7274
if DD_API_KEY_SECRET_ARN:
@@ -80,7 +82,7 @@ def get_api_key() -> str:
8082
return ""
8183
endpoint_url = (
8284
f"https://secretsmanager-fips.{secrets_region}.amazonaws.com"
83-
if is_gov_region
85+
if enable_fips_mode
8486
else None
8587
)
8688
secrets_manager_client = _boto3_client(
@@ -92,7 +94,9 @@ def get_api_key() -> str:
9294
elif DD_API_KEY_SSM_NAME:
9395
# SSM endpoints: https://docs.aws.amazon.com/general/latest/gr/ssm.html
9496
fips_endpoint = (
95-
f"https://ssm-fips.{LAMBDA_REGION}.amazonaws.com" if is_gov_region else None
97+
f"https://ssm-fips.{LAMBDA_REGION}.amazonaws.com"
98+
if enable_fips_mode
99+
else None
96100
)
97101
ssm_client = _boto3_client("ssm", endpoint_url=fips_endpoint)
98102
api_key = ssm_client.get_parameter(
@@ -101,7 +105,9 @@ def get_api_key() -> str:
101105
elif DD_KMS_API_KEY:
102106
# KMS endpoints: https://docs.aws.amazon.com/general/latest/gr/kms.html
103107
fips_endpoint = (
104-
f"https://kms-fips.{LAMBDA_REGION}.amazonaws.com" if is_gov_region else None
108+
f"https://kms-fips.{LAMBDA_REGION}.amazonaws.com"
109+
if enable_fips_mode
110+
else None
105111
)
106112
kms_client = _boto3_client("kms", endpoint_url=fips_endpoint)
107113
api_key = decrypt_kms_api_key(kms_client, DD_KMS_API_KEY)

datadog_lambda/fips.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import logging
2+
import os
3+
4+
is_gov_region = os.environ.get("AWS_REGION", "").startswith("us-gov-")
5+
6+
enable_fips_mode = (
7+
os.environ.get(
8+
"DD_LAMBDA_FIPS_MODE",
9+
"true" if is_gov_region else "false",
10+
).lower()
11+
== "true"
12+
)
13+
14+
if is_gov_region or enable_fips_mode:
15+
logger = logging.getLogger(__name__)
16+
logger.debug(
17+
"Python Lambda Layer FIPS mode is %s.",
18+
"enabled" if enable_fips_mode else "not enabled",
19+
)

datadog_lambda/metric.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import ujson as json
1313

1414
from datadog_lambda.extension import should_use_extension
15+
from datadog_lambda.fips import enable_fips_mode
1516
from datadog_lambda.tags import dd_lambda_layer_tag, get_enhanced_metrics_tags
1617

1718
logger = logging.getLogger(__name__)
@@ -21,17 +22,27 @@ class MetricsHandler(enum.Enum):
2122
EXTENSION = "extension"
2223
FORWARDER = "forwarder"
2324
DATADOG_API = "datadog_api"
25+
NO_METRICS = "no_metrics"
2426

2527

2628
def _select_metrics_handler():
2729
if should_use_extension:
2830
return MetricsHandler.EXTENSION
2931
if os.environ.get("DD_FLUSH_TO_LOG", "").lower() == "true":
3032
return MetricsHandler.FORWARDER
33+
34+
if enable_fips_mode:
35+
logger.debug(
36+
"With FIPS mode enabled, the Datadog API metrics handler is unavailable."
37+
)
38+
return MetricsHandler.NO_METRICS
39+
3140
return MetricsHandler.DATADOG_API
3241

3342

3443
metrics_handler = _select_metrics_handler()
44+
# TODO: add a metric for this so that we can see how often the DATADOG_API
45+
# metrics handler is actually used.
3546
logger.debug("identified primary metrics handler as %s", metrics_handler)
3647

3748

@@ -122,6 +133,12 @@ def lambda_metric(metric_name, value, timestamp=None, tags=None, force_async=Fal
122133
elif metrics_handler == MetricsHandler.DATADOG_API:
123134
lambda_stats.distribution(metric_name, value, tags=tags, timestamp=timestamp)
124135

136+
elif metrics_handler == MetricsHandler.NO_METRICS:
137+
logger.debug(
138+
"Metric %s cannot be submitted because the metrics handler is disabled: %s",
139+
metric_name,
140+
),
141+
125142
else:
126143
# This should be qutie impossible, but let's at least log a message if
127144
# it somehow happens.

tests/test_api.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import os
22
import unittest
3-
from unittest.mock import patch, MagicMock
3+
from unittest.mock import MagicMock, patch
44

55
import datadog_lambda.api as api
66

@@ -22,6 +22,7 @@ def setUp(self):
2222
)
2323
self.env_patcher.start()
2424

25+
@patch("datadog_lambda.api.enable_fips_mode", True)
2526
@patch("botocore.session.Session.create_client")
2627
def test_secrets_manager_fips_endpoint(self, mock_boto3_client):
2728
mock_client = MagicMock()
@@ -62,6 +63,28 @@ def test_secrets_manager_different_region(self, mock_boto3_client):
6263
)
6364
self.assertEqual(api_key, "test-api-key")
6465

66+
@patch("datadog_lambda.api.enable_fips_mode", True)
67+
@patch("botocore.session.Session.create_client")
68+
def test_secrets_manager_different_region_but_still_fips(self, mock_boto3_client):
69+
mock_client = MagicMock()
70+
mock_client.get_secret_value.return_value = {"SecretString": "test-api-key"}
71+
mock_boto3_client.return_value = mock_client
72+
73+
os.environ["AWS_REGION"] = "us-east-1"
74+
os.environ[
75+
"DD_API_KEY_SECRET_ARN"
76+
] = "arn:aws:secretsmanager:us-west-1:1234567890:secret:key-name-123ABC"
77+
78+
api_key = api.get_api_key()
79+
80+
mock_boto3_client.assert_called_with(
81+
"secretsmanager",
82+
endpoint_url="https://secretsmanager-fips.us-west-1.amazonaws.com",
83+
region_name="us-west-1",
84+
)
85+
self.assertEqual(api_key, "test-api-key")
86+
87+
@patch("datadog_lambda.api.enable_fips_mode", True)
6588
@patch("botocore.session.Session.create_client")
6689
def test_ssm_fips_endpoint(self, mock_boto3_client):
6790
mock_client = MagicMock()
@@ -80,6 +103,7 @@ def test_ssm_fips_endpoint(self, mock_boto3_client):
80103
)
81104
self.assertEqual(api_key, "test-api-key")
82105

106+
@patch("datadog_lambda.api.enable_fips_mode", True)
83107
@patch("botocore.session.Session.create_client")
84108
@patch("datadog_lambda.api.decrypt_kms_api_key")
85109
def test_kms_fips_endpoint(self, mock_decrypt_kms, mock_boto3_client):

tests/test_metric.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,15 @@
1919

2020
class TestLambdaMetric(unittest.TestCase):
2121
def setUp(self):
22-
patcher = patch("datadog_lambda.metric.lambda_stats")
23-
self.mock_metric_lambda_stats = patcher.start()
24-
self.addCleanup(patcher.stop)
22+
lambda_stats_patcher = patch("datadog_lambda.metric.lambda_stats")
23+
self.mock_metric_lambda_stats = lambda_stats_patcher.start()
24+
self.addCleanup(lambda_stats_patcher.stop)
25+
26+
stdout_metric_patcher = patch(
27+
"datadog_lambda.metric.write_metric_point_to_stdout"
28+
)
29+
self.mock_write_metric_point_to_stdout = stdout_metric_patcher.start()
30+
self.addCleanup(stdout_metric_patcher.stop)
2531

2632
def test_lambda_metric_tagged_with_dd_lambda_layer(self):
2733
lambda_metric("test", 1)
@@ -56,13 +62,26 @@ def test_select_metrics_handler_dd_api_fallback(self):
5662
self.assertEqual(MetricsHandler.DATADOG_API, _select_metrics_handler())
5763
del os.environ["DD_FLUSH_TO_LOG"]
5864

65+
@patch("datadog_lambda.metric.enable_fips_mode", True)
66+
@patch("datadog_lambda.metric.should_use_extension", False)
67+
def test_select_metrics_handler_has_no_fallback_in_fips_mode(self):
68+
os.environ["DD_FLUSH_TO_LOG"] = "False"
69+
self.assertEqual(MetricsHandler.NO_METRICS, _select_metrics_handler())
70+
del os.environ["DD_FLUSH_TO_LOG"]
71+
5972
@patch("datadog_lambda.metric.metrics_handler", MetricsHandler.EXTENSION)
60-
def test_lambda_metric_flush_to_log_with_extension(self):
73+
def test_lambda_metric_goes_to_extension_with_extension_handler(self):
6174
lambda_metric("test", 1)
6275
self.mock_metric_lambda_stats.distribution.assert_has_calls(
6376
[call("test", 1, timestamp=None, tags=[dd_lambda_layer_tag])]
6477
)
6578

79+
@patch("datadog_lambda.metric.metrics_handler", MetricsHandler.NO_METRICS)
80+
def test_lambda_metric_has_nowhere_to_go_with_no_metrics_handler(self):
81+
lambda_metric("test", 1)
82+
self.mock_metric_lambda_stats.distribution.assert_not_called()
83+
self.mock_write_metric_point_to_stdout.assert_not_called()
84+
6685
@patch("datadog_lambda.metric.metrics_handler", MetricsHandler.EXTENSION)
6786
def test_lambda_metric_timestamp_with_extension(self):
6887
delta = timedelta(minutes=1)
@@ -72,6 +91,7 @@ def test_lambda_metric_timestamp_with_extension(self):
7291
self.mock_metric_lambda_stats.distribution.assert_has_calls(
7392
[call("test_timestamp", 1, timestamp=timestamp, tags=[dd_lambda_layer_tag])]
7493
)
94+
self.mock_write_metric_point_to_stdout.assert_not_called()
7595

7696
@patch("datadog_lambda.metric.metrics_handler", MetricsHandler.EXTENSION)
7797
def test_lambda_metric_datetime_with_extension(self):
@@ -89,6 +109,7 @@ def test_lambda_metric_datetime_with_extension(self):
89109
)
90110
]
91111
)
112+
self.mock_write_metric_point_to_stdout.assert_not_called()
92113

93114
@patch("datadog_lambda.metric.metrics_handler", MetricsHandler.EXTENSION)
94115
def test_lambda_metric_invalid_timestamp_with_extension(self):
@@ -97,16 +118,21 @@ def test_lambda_metric_invalid_timestamp_with_extension(self):
97118

98119
lambda_metric("test_timestamp", 1, timestamp)
99120
self.mock_metric_lambda_stats.distribution.assert_not_called()
121+
self.mock_write_metric_point_to_stdout.assert_not_called()
100122

101123
@patch("datadog_lambda.metric.metrics_handler", MetricsHandler.FORWARDER)
102124
def test_lambda_metric_flush_to_log(self):
103125
lambda_metric("test", 1)
104126
self.mock_metric_lambda_stats.distribution.assert_not_called()
127+
self.mock_write_metric_point_to_stdout.assert_has_calls(
128+
[call("test", 1, timestamp=None, tags=[dd_lambda_layer_tag])]
129+
)
105130

106131
@patch("datadog_lambda.metric.logger.warning")
107132
def test_lambda_metric_invalid_metric_name_none(self, mock_logger_warning):
108133
lambda_metric(None, 1)
109134
self.mock_metric_lambda_stats.distribution.assert_not_called()
135+
self.mock_write_metric_point_to_stdout.assert_not_called()
110136
mock_logger_warning.assert_called_once_with(
111137
"Ignoring metric submission. Invalid metric name: %s", None
112138
)
@@ -115,6 +141,7 @@ def test_lambda_metric_invalid_metric_name_none(self, mock_logger_warning):
115141
def test_lambda_metric_invalid_metric_name_not_string(self, mock_logger_warning):
116142
lambda_metric(123, 1)
117143
self.mock_metric_lambda_stats.distribution.assert_not_called()
144+
self.mock_write_metric_point_to_stdout.assert_not_called()
118145
mock_logger_warning.assert_called_once_with(
119146
"Ignoring metric submission. Invalid metric name: %s", 123
120147
)
@@ -123,6 +150,7 @@ def test_lambda_metric_invalid_metric_name_not_string(self, mock_logger_warning)
123150
def test_lambda_metric_non_numeric_value(self, mock_logger_warning):
124151
lambda_metric("test.non_numeric", "oops")
125152
self.mock_metric_lambda_stats.distribution.assert_not_called()
153+
self.mock_write_metric_point_to_stdout.assert_not_called()
126154
mock_logger_warning.assert_called_once_with(
127155
"Ignoring metric submission for metric '%s' because the value is not numeric: %r",
128156
"test.non_numeric",

0 commit comments

Comments
 (0)