diff --git a/.github/workflows/test-fips.yml b/.github/workflows/test-fips.yml new file mode 100644 index 0000000000000..663bf7b45b273 --- /dev/null +++ b/.github/workflows/test-fips.yml @@ -0,0 +1,151 @@ +name: Test FIPS E2E + +on: + workflow_dispatch: + inputs: + agent-image: + description: "Agent image to use" + required: false + type: string + target: + description: "Target to test" + required: false + type: string + pull_request: + path: + - datadog_checks_base/datadog_checks/** + schedule: + - cron: '0 0,8,16 * * *' + +defaults: + run: + shell: bash + +jobs: + run: + name: "Test FIPS" + runs-on: ["ubuntu-22.04"] + + env: + FORCE_COLOR: "1" + PYTHON_VERSION: "3.12" + DDEV_E2E_AGENT: "${{ inputs.agent-image || 'datadog/agent-dev:master-fips' }}" + # Test results for later processing + TEST_RESULTS_BASE_DIR: "test-results" + # Tracing to monitor our test suite + DD_ENV: "ci" + DD_SERVICE: "ddev-integrations-core" + DD_TAGS: "team:agent-integrations" + DD_TRACE_ANALYTICS_ENABLED: "true" + # Capture traces for a separate job to do the submission + TRACE_CAPTURE_BASE_DIR: "trace-captures" + TRACE_CAPTURE_LOG: "trace-captures/output.log" + + steps: + + - name: Set environment variables with sanitized paths + run: | + JOB_NAME="test-fips" + + echo "TEST_RESULTS_DIR=$TEST_RESULTS_BASE_DIR/$JOB_NAME" >> $GITHUB_ENV + echo "TRACE_CAPTURE_FILE=$TRACE_CAPTURE_BASE_DIR/$JOB_NAME" >> $GITHUB_ENV + + - uses: actions/checkout@v4 + + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v5 + with: + python-version: "${{ env.PYTHON_VERSION }}" + cache: 'pip' + + - name: Restore cache + uses: actions/cache/restore@v4 + with: + path: '~/.cache/pip' + key: >- + ${{ format( + 'v01-python-{0}-{1}-{2}-{3}', + env.pythonLocation, + hashFiles('datadog_checks_base/pyproject.toml'), + hashFiles('datadog_checks_dev/pyproject.toml'), + hashFiles('ddev/pyproject.toml') + )}} + restore-keys: |- + v01-python-${{ env.pythonLocation }} + + - name: Install ddev from local folder + run: |- + pip install -e ./datadog_checks_dev[cli] + pip install -e ./ddev + + - name: Configure ddev + run: |- + ddev config set repos.core . + ddev config set repo core + + - name: Prepare for testing + env: + PYTHONUNBUFFERED: "1" + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }} + ORACLE_DOCKER_USERNAME: ${{ secrets.ORACLE_DOCKER_USERNAME }} + ORACLE_DOCKER_PASSWORD: ${{ secrets.ORACLE_DOCKER_PASSWORD }} + SINGLESTORE_LICENSE: ${{ secrets.SINGLESTORE_LICENSE }} + DD_GITHUB_USER: ${{ github.actor }} + DD_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ddev ci setup ${{ inputs.target || 'tls' }} + + - name: Set up trace capturing + env: + PYTHONUNBUFFERED: "1" + run: |- + mkdir "${{ env.TRACE_CAPTURE_BASE_DIR }}" + python .ddev/ci/scripts/traces.py capture --port "8126" --record-file "${{ env.TRACE_CAPTURE_FILE }}" > "${{ env.TRACE_CAPTURE_LOG }}" 2>&1 & + + - name: Run E2E tests with FIPS disabled + env: + DD_API_KEY: "${{ secrets.DD_API_KEY }}" + run: | + ddev env test -e GOFIPS=0 --new-env --junit ${{ inputs.target || 'tls' }} -- all -m "fips_off" + + - name: Run E2E tests with FIPS enabled + env: + DD_API_KEY: "${{ secrets.DD_API_KEY }}" + run: | + ddev env test -e GOFIPS=1 --new-env --junit ${{ inputs.target || 'tls' }} -- all -k "fips_on" + + - name: View trace log + if: always() + run: cat "${{ env.TRACE_CAPTURE_LOG }}" + + - name: Upload captured traces + if: always() + uses: actions/upload-artifact@v4 + with: + name: "traces-${{ inputs.target || 'tls' }}" + path: "${{ env.TRACE_CAPTURE_FILE }}" + + - name: Finalize test results + if: always() + run: |- + mkdir -p "${{ env.TEST_RESULTS_DIR }}" + if [[ -d ${{ inputs.target || 'tls' }}/.junit ]]; then + mv ${{ inputs.target || 'tls' }}/.junit/*.xml "${{ env.TEST_RESULTS_DIR }}" + fi + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: "test-results-${{ inputs.target || 'tls' }}" + path: "${{ env.TEST_RESULTS_BASE_DIR }}" + + - name: Upload coverage data + if: > + !github.event.repository.private && + always() + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: "${{ inputs.target || 'tls' }}/coverage.xml" + flags: "${{ inputs.target || 'tls' }}" diff --git a/.github/workflows/test-target.yml b/.github/workflows/test-target.yml index 489620e36dd94..b56355a44ea66 100644 --- a/.github/workflows/test-target.yml +++ b/.github/workflows/test-target.yml @@ -225,7 +225,7 @@ jobs: run: | if [ '${{ inputs.pytest-args }}' = '-m flaky' ]; then set +e # Disable immediate exit - ddev test --cov --junit ${{ inputs.target }} -- ${{ inputs.pytest-args }} + ddev test --cov --junit ${{ inputs.target }} -- ${{ inputs.pytest-args }} -- '-k "not fips"' exit_code=$? if [ $exit_code -eq 5 ]; then # Flaky test count can be zero, this is done to avoid pipeline failure @@ -235,7 +235,7 @@ jobs: exit $exit_code fi else - ddev test --cov --junit ${{ inputs.target }} ${{ inputs.pytest-args != '' && format('-- {0}', inputs.pytest-args) || '' }} + ddev test --cov --junit ${{ inputs.target }} ${{ inputs.pytest-args != '' && format('-- {0} -k "not fips"', inputs.pytest-args) || '-- -k "not fips"' }} fi - name: Run Unit & Integration tests with minimum version of base package @@ -243,7 +243,7 @@ jobs: run: | if [ '${{ inputs.pytest-args }}' = '-m flaky' ]; then set +e # Disable immediate exit - ddev test --compat --recreate --junit ${{ inputs.target }} -- ${{ inputs.pytest-args }} + ddev test --compat --recreate --junit ${{ inputs.target }} -- ${{ inputs.pytest-args }} -k "not fips" exit_code=$? if [ $exit_code -eq 5 ]; then # Flaky test count can be zero, this is done to avoid pipeline failure @@ -253,7 +253,7 @@ jobs: exit $exit_code fi else - ddev test --compat --recreate --junit ${{ inputs.target }} ${{ inputs.pytest-args != '' && format('-- {0}', inputs.pytest-args) || '' }} + ddev test --compat --recreate --junit ${{ inputs.target }} ${{ inputs.pytest-args != '' && format('-- {0} -k "not fips"', inputs.pytest-args) || '-- -k "not fips"' }} fi - name: Run E2E tests with latest base package @@ -270,7 +270,7 @@ jobs: # by default if [ '${{ inputs.pytest-args }}' = '-m flaky' ]; then set +e # Disable immediate exit - ddev env test --base --new-env --junit ${{ inputs.target }} -- all ${{ inputs.pytest-args }} + ddev env test --base --new-env --junit ${{ inputs.target }} -- all ${{ inputs.pytest-args }} -k "not fips" exit_code=$? if [ $exit_code -eq 5 ]; then # Flaky test count can be zero, this is done to avoid pipeline failure @@ -281,7 +281,7 @@ jobs: fi elif [ '${{ inputs.pytest-args }}' = '-m "not flaky"' ]; then set +e # Disable immediate exit - ddev env test --base --new-env --junit ${{ inputs.target }} -- all ${{ inputs.pytest-args }} + ddev env test --base --new-env --junit ${{ inputs.target }} -- all ${{ inputs.pytest-args }} -k "not fips" exit_code=$? if [ $exit_code -eq 5 ]; then # Flaky test count can be zero, this is done to avoid pipeline failure @@ -291,7 +291,7 @@ jobs: exit $exit_code fi else - ddev env test --base --new-env --junit ${{ inputs.target }} ${{ inputs.pytest-args != '' && format('-- all {0}', inputs.pytest-args) || '' }} + ddev env test --base --new-env --junit ${{ inputs.target }} ${{ inputs.pytest-args != '' && format('-- all {0} -k "not fips"', inputs.pytest-args) || '-- all -k "not fips"' }} fi - name: Run E2E tests @@ -308,7 +308,7 @@ jobs: # by default if [ '${{ inputs.pytest-args }}' = '-m flaky' ]; then set +e # Disable immediate exit - ddev env test --new-env --junit ${{ inputs.target }} -- all ${{ inputs.pytest-args }} + ddev env test --new-env --junit ${{ inputs.target }} -- all ${{ inputs.pytest-args }} -k "not fips" exit_code=$? if [ $exit_code -eq 5 ]; then # Flaky test count can be zero, this is done to avoid pipeline failure @@ -319,7 +319,7 @@ jobs: fi elif [ '${{ inputs.pytest-args }}' = '-m "not flaky"' ]; then set +e # Disable immediate exit - ddev env test --new-env --junit ${{ inputs.target }} -- all ${{ inputs.pytest-args }} + ddev env test --new-env --junit ${{ inputs.target }} -- all ${{ inputs.pytest-args }} -k "not fips" exit_code=$? if [ $exit_code -eq 5 ]; then # Flaky test count can be zero, this is done to avoid pipeline failure @@ -329,7 +329,7 @@ jobs: exit $exit_code fi else - ddev env test --new-env --junit ${{ inputs.target }} ${{ inputs.pytest-args != '' && format('-- all {0}', inputs.pytest-args) || '' }} + ddev env test --new-env --junit ${{ inputs.target }} ${{ inputs.pytest-args != '' && format('-- all {0} -k "not fips"', inputs.pytest-args) || '-- all -k "not fips"' }} fi - name: Run benchmarks @@ -355,7 +355,7 @@ jobs: # by default if [ '${{ inputs.pytest-args }}' = '-m flaky' ]; then set +e # Disable immediate exit - ddev env test --base --new-env --junit ${{ inputs.target }}:latest -- all ${{ inputs.pytest-args }} + ddev env test --base --new-env --junit ${{ inputs.target }}:latest -- all ${{ inputs.pytest-args }} -k "not fips" exit_code=$? if [ $exit_code -eq 5 ]; then # Flaky test count can be zero, this is done to avoid pipeline failure @@ -376,7 +376,7 @@ jobs: exit $exit_code fi else - ddev env test --base --new-env --junit ${{ inputs.target }}:latest ${{ inputs.pytest-args != '' && format('-- all {0}', inputs.pytest-args) || '' }} + ddev env test --base --new-env --junit ${{ inputs.target }}:latest ${{ inputs.pytest-args != '' && format('-- all {0} -k "not fips"', inputs.pytest-args) || '-- all -k "not fips"' }} fi - name: View trace log diff --git a/datadog_checks_base/changelog.d/19179.security b/datadog_checks_base/changelog.d/19179.security new file mode 100644 index 0000000000000..e2819d1fafecd --- /dev/null +++ b/datadog_checks_base/changelog.d/19179.security @@ -0,0 +1 @@ +Add FIPS switch diff --git a/datadog_checks_base/datadog_checks/base/checks/base.py b/datadog_checks_base/datadog_checks/base/checks/base.py index 9508dcc518dd4..5b5bcfb386c0d 100644 --- a/datadog_checks_base/datadog_checks/base/checks/base.py +++ b/datadog_checks_base/datadog_checks/base/checks/base.py @@ -6,6 +6,7 @@ import importlib import inspect import logging +import os import re import traceback import unicodedata @@ -46,6 +47,7 @@ from ..utils.agent.utils import should_profile_memory from ..utils.common import ensure_bytes, to_native_string from ..utils.diagnose import Diagnosis +from ..utils.fips import enable_fips from ..utils.http import RequestsWrapper from ..utils.limiter import Limiter from ..utils.metadata import MetadataManager @@ -307,6 +309,9 @@ def __init__(self, *args, **kwargs): self.__formatted_tags = None self.__logs_enabled = None + if os.environ.get("GOFIPS", "0") == "1": + enable_fips() + def _create_metrics_pattern(self, metric_patterns, option_name): all_patterns = metric_patterns.get(option_name, []) diff --git a/datadog_checks_base/datadog_checks/base/utils/fips.py b/datadog_checks_base/datadog_checks/base/utils/fips.py new file mode 100644 index 0000000000000..36de6c1e3038c --- /dev/null +++ b/datadog_checks_base/datadog_checks/base/utils/fips.py @@ -0,0 +1,32 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +import os + + +def enable_fips(path_to_openssl_conf=None, path_to_openssl_modules=None): + path_to_embedded = None + if os.getenv("OPENSSL_CONF") is None: + if path_to_openssl_conf is None: + path_to_embedded = _get_embedded_path() if path_to_embedded is None else path_to_embedded + path_to_openssl_conf = path_to_embedded / "ssl" / "openssl.cnf" + if not path_to_openssl_conf.exists(): + raise RuntimeError(f'The configuration file "{path_to_openssl_conf}" does not exist') + os.environ["OPENSSL_CONF"] = str(path_to_openssl_conf) + + if os.getenv("OPENSSL_MODULES") is None: + if path_to_openssl_modules is None: + path_to_embedded = _get_embedded_path() if path_to_embedded is None else path_to_embedded + path_to_openssl_modules = path_to_embedded / "lib" / "ossl-modules" + if not path_to_openssl_conf.exists(): + raise RuntimeError(f'The directory "{path_to_openssl_modules}" does not exist') + os.environ["OPENSSL_MODULES"] = str(path_to_openssl_modules) + + +def _get_embedded_path(): + import sys + from pathlib import Path + + embedded_dir = "embedded3" if os.name == 'nt' else "embedded" + return Path(sys.executable.split("embedded")[0] + embedded_dir) diff --git a/datadog_checks_base/tests/base/checks/test_agent_check.py b/datadog_checks_base/tests/base/checks/test_agent_check.py index 2f77b049a389e..931d4dab9315d 100644 --- a/datadog_checks_base/tests/base/checks/test_agent_check.py +++ b/datadog_checks_base/tests/base/checks/test_agent_check.py @@ -5,6 +5,7 @@ # Licensed under a 3-clause BSD style license (see LICENSE) import json import logging +import os from typing import Any # noqa: F401 import mock @@ -1293,3 +1294,19 @@ def test_detect_typos_configuration_models( assert "Detected potential typo in configuration option" not in caplog.text assert typos == set(unknown_options) + + +def test_env_var_logic_default(): + with mock.patch.dict('os.environ', {'GOFIPS': '0'}): + AgentCheck() + assert os.getenv('OPENSSL_CONF', None) is None + assert os.getenv('OPENSSL_MODULES', None) is None + + +def test_env_var_logic_preset(): + preset_conf = 'path/to/openssl.cnf' + preset_modules = 'path/to/ossl-modules' + with mock.patch.dict('os.environ', {'GOFIPS': '1', 'OPENSSL_CONF': preset_conf, 'OPENSSL_MODULES': preset_modules}): + AgentCheck() + assert os.getenv('OPENSSL_CONF', None) == preset_conf + assert os.getenv('OPENSSL_MODULES', None) == preset_modules diff --git a/ddev/changelog.d/19179.security b/ddev/changelog.d/19179.security new file mode 100644 index 0000000000000..e2819d1fafecd --- /dev/null +++ b/ddev/changelog.d/19179.security @@ -0,0 +1 @@ +Add FIPS switch diff --git a/ddev/src/ddev/e2e/agent/docker.py b/ddev/src/ddev/e2e/agent/docker.py index 1821d50d1b346..206d4e2bbc1ee 100644 --- a/ddev/src/ddev/e2e/agent/docker.py +++ b/ddev/src/ddev/e2e/agent/docker.py @@ -113,7 +113,12 @@ def start(self, *, agent_build: str, local_packages: dict[Path, str], env_vars: if agent_build.startswith("datadog/"): # Add a potentially missing `py` suffix for default non-RC builds - if 'rc' not in agent_build and 'py' not in agent_build and not re.match(AGENT_VERSION_REGEX, agent_build): + if ( + 'rc' not in agent_build + and 'py' not in agent_build + and 'fips' not in agent_build + and not re.match(AGENT_VERSION_REGEX, agent_build) + ): agent_build = f'{agent_build}-py{self.python_version[0]}' if self.metadata.get('use_jmx') and not agent_build.endswith('-jmx'): diff --git a/tls/tests/conftest.py b/tls/tests/conftest.py index 12483f28b2dab..b606a64d0770e 100644 --- a/tls/tests/conftest.py +++ b/tls/tests/conftest.py @@ -29,13 +29,28 @@ } -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="function") +def clean_fips_environment(): + os.environ["GOFIPS"] = "0" + os.environ["OPENSSL_CONF"] = "" + os.environ["OPENSSL_MODULES"] = "" + yield + + +@pytest.fixture(scope='session') def dd_environment(instance_e2e, mock_local_tls_dns): with docker_run(os.path.join(HERE, 'compose', 'docker-compose.yml'), build=True, sleep=20): e2e_metadata = {'docker_volumes': ['{}:{}'.format(CA_CERT, CA_CERT_MOUNT_PATH)]} yield instance_e2e, e2e_metadata +@pytest.fixture(scope='session') +def dd_fips_environment(instance_e2e_fips, mock_local_tls_dns): + with docker_run(os.path.join(HERE, 'fips', 'docker-compose.yml'), build=True, sleep=20): + e2e_metadata = {'docker_volumes': ['{}:{}'.format(CA_CERT, CA_CERT_MOUNT_PATH)]} + yield instance_e2e_fips, instance_e2e_non_fips, e2e_metadata + + @pytest.fixture(scope='session') def mock_local_tls_dns(): with mock_local(HOSTNAME_TO_PORT_MAPPING): @@ -158,6 +173,28 @@ def instance_e2e(): } +@pytest.fixture(scope='session') +def instance_e2e_fips(): + return { + 'server': 'https://localhost', + 'port': 8443, + 'tls_ca_cert': CA_CERT_MOUNT_PATH, + 'tls_verify': False, + 'tls_validate_hostname': False, + } + + +@pytest.fixture(scope='session') +def instance_e2e_non_fips(): + return { + 'server': 'https://localhost', + 'port': 9443, + 'tls_ca_cert': CA_CERT_MOUNT_PATH, + 'tls_verify': False, + 'tls_validate_hostname': False, + } + + @pytest.fixture def instance_remote_ok_ip(): return {'server': '1.1.1.1', 'tls_validate_hostname': False, 'days_warning': 1, 'days_critical': 1} diff --git a/tls/tests/fips/Dockerfile b/tls/tests/fips/Dockerfile new file mode 100644 index 0000000000000..dbf827d8f9d66 --- /dev/null +++ b/tls/tests/fips/Dockerfile @@ -0,0 +1,11 @@ +FROM alpine:3.18 + +# Install OpenSSL and necessary tools +RUN apk add --no-cache openssl bash + +COPY start-server.sh /usr/local/bin/start-server.sh +COPY ca.* /tmp/ +RUN chmod +x /usr/local/bin/start-server.sh + +# Expose port 443 +EXPOSE 443 diff --git a/tls/tests/fips/ca.crt b/tls/tests/fips/ca.crt new file mode 100644 index 0000000000000..189746cb2a227 --- /dev/null +++ b/tls/tests/fips/ca.crt @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFwzCCA6ugAwIBAgIULsStz9lSmEN4m9GOJbAcvBD26qMwDQYJKoZIhvcNAQEL +BQAwcDELMAkGA1UEBhMCVVMxDDAKBgNVBAgMA2ZvbzEMMAoGA1UEBwwDZm9vMQww +CgYDVQQKDANmb28xDDAKBgNVBAsMA2ZvbzEVMBMGA1UEAwwMZXhwaXJlZC5tb2Nr +MRIwEAYJKoZIhvcNAQkBFgNmb28wIBcNMjAwMzA1MTEwNDIyWhgPOTk5OTEyMzEx +MTA0MjJaMHAxCzAJBgNVBAYTAlVTMQwwCgYDVQQIDANmb28xDDAKBgNVBAcMA2Zv +bzEMMAoGA1UECgwDZm9vMQwwCgYDVQQLDANmb28xFTATBgNVBAMMDGV4cGlyZWQu +bW9jazESMBAGCSqGSIb3DQEJARYDZm9vMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEA5Ws3yh++mg+fnvwp2UbW1jpO8MslpdudgPkaZOtdO40lg4teYTqA +rc770P+sfNha/0Gv1eel9LtdgsvyoHhABQfWoUYDUAU5ZbCalOHWOgZulZuw8Ff8 +zEiS+7ccBQs2ayGclIUVZo7PR3eURwmsCfd1CgJw60DQVegJhD/G4p0UhJRfArZ3 +OYvsYziE20QmoRNubntlR7gyzF30378pp8tP6661rFoZMuDx1ajvD+ExBCzUCiwV +5ly/21FdXaEOVqmjxkn6nYQX35SH7ypyVJCYLe7sSW+ExT5FrvSsVOU+0GzAO291 +b+xJQf82/wWbwXgcauzpZYidkhmJahlNKUSGvHQBIvzSbSe198PFWug99Evs/Ux6 +xacyPCxdZ90xYVqah1zoeCsJgi+xa+4zRSk+OreP+TOe6Ph78Ne9+rn/R6eTKzPY +JTNWgi6nt5os8rKijTMpTHx24BjekFOTtIfMC9geNGYWnamTfEplMq7M86d6kgiq +UOeLdXywVvoyMkbVg7oAMU2wbwycu3HTgP4P41esWjNp1Z36YUzRVJoavcZtEJtO +3ii1Elz/SFlqktLDwl27kh4GzvGEXwgFYjW+ucY4FrinTYmmZBCHQwuD3ZPPFVAu +n1XEdYJf/suIqInH7eN2x/0RTsJbVodtMmke+XO5DdynUWEuK+/bmjMCAwEAAaNT +MFEwHQYDVR0OBBYEFBEJJjHmDC+yd7Dem4SQg25HJ/mIMB8GA1UdIwQYMBaAFBEJ +JjHmDC+yd7Dem4SQg25HJ/mIMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggIBAGfXMDCJ3IpnLl3CF6zDkV8Jvv8MPx/61iQ5nNikuMEkP7smImfGx7bh +WJhPiOQPaWU67Cr29WQ0bt0lKneBAcfvqLQ2Ub6wV4QCldhc95MfOo1ReqqDQkA+ +mi9ILwlaDevYEpBlpqa6v3L/EtaDof/Q8dpGm0jX+8yKqAZShpRVWRZUZch9iJOF +XDS2Kee8wFP04jtt3isgXu6j+0JMytd834CoBHCGdjQsNFBnfTCKogg7iSweUoEa +jnfYfRzxAzFachM/kB3CSpDifKtQabagKNfkEw2gyJDdaj3q3LO7jFCwySu6K/7n +MeJSA6PJNSXouImUweNUjG+NJKFcXLHMW7s4lqbfnnaQVGq+n3nofhm11uasAOF1 +HdQRmyJQPKcZKTN19fz6ageJqJI6hCol2OENuTcmVH++KlJAK4R4z/S9/Er/9yOn +UIOvwuBbcfS9uxoRlUge8jXWCVTFzolq6DVGvCOwgLlNsQDT4oEsh7unAXE7E2w9 +blkFskCImPaUpKqXwdLXPtESyJK6ri5nC5qY72cE3MndQ/HsrdbpjyvliYBXxcAP +HxBgKj1HzUdME4OIHd7tIlndsBoaGnAwGdR018EkxaYZj4OhdbVSUwZop+NvtA8q +MutrU4WG95lqaWnhnt6UnL/rbf3zbxzP9xyJIp3NYW8p7juYUeik +-----END CERTIFICATE----- diff --git a/tls/tests/fips/ca.key b/tls/tests/fips/ca.key new file mode 100644 index 0000000000000..ab3351d26fb6a --- /dev/null +++ b/tls/tests/fips/ca.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/tls/tests/fips/docker-compose.yml b/tls/tests/fips/docker-compose.yml new file mode 100644 index 0000000000000..8315ee02e0868 --- /dev/null +++ b/tls/tests/fips/docker-compose.yml @@ -0,0 +1,28 @@ +services: + fips-server: + build: . + ports: + - "8443:443" + volumes: + - ./ca.crt:/etc/ssl/certs/server.crt + - ./ca.key:/etc/ssl/private/server.key + command: ["/usr/local/bin/start-server.sh", "ECDHE-RSA-AES128-SHA256"] + healthcheck: + test: ["CMD", "curl", "-f", "https://localhost:443"] + interval: 30s + timeout: 10s + retries: 3 + + non-fips-server: + build: . + ports: + - "9443:443" + volumes: + - ./ca.crt:/etc/ssl/certs/server.crt + - ./ca.key:/etc/ssl/private/server.key + command: ["/usr/local/bin/start-server.sh", "ECDHE-RSA-CHACHA20-POLY1305"] + healthcheck: + test: ["CMD", "curl", "-f", "https://localhost:443"] + interval: 30s + timeout: 10s + retries: 3 diff --git a/tls/tests/fips/start-server.sh b/tls/tests/fips/start-server.sh new file mode 100644 index 0000000000000..a6b40f97b5041 --- /dev/null +++ b/tls/tests/fips/start-server.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +if [ ! -f /etc/ssl/certs/server.crt ] || [ ! -f /etc/ssl/private/server.key ]; then + echo "Generating self-signed certificate..." + mkdir -p /etc/ssl/private + openssl req -x509 -newkey rsa:2048 -keyout /etc/ssl/private/server.key -out /etc/ssl/certs/server.crt -days 365 -nodes -subj "/CN=localhost" +fi + +CIPHER=$1 + +echo "Starting OpenSSL server on port 443 with cipher $CIPHER..." +openssl s_server \ + -accept 443 \ + -cert /etc/ssl/certs/server.crt \ + -key /etc/ssl/private/server.key \ + -cipher $CIPHER \ + -no_tls1_3 \ + -WWW diff --git a/tls/tests/test_e2e.py b/tls/tests/test_e2e.py index 1d4ff7b8e29fe..8e34480828826 100644 --- a/tls/tests/test_e2e.py +++ b/tls/tests/test_e2e.py @@ -13,7 +13,7 @@ @pytest.mark.e2e -def test_e2e(dd_agent_check, instance_e2e): +def test_e2e(dd_environment, dd_agent_check, instance_e2e): aggregator = dd_agent_check(instance_e2e) aggregator.assert_service_check(SERVICE_CHECK_CAN_CONNECT, status=TLSCheck.OK, count=1) diff --git a/tls/tests/test_fips.py b/tls/tests/test_fips.py new file mode 100644 index 0000000000000..75b2f84a9f724 --- /dev/null +++ b/tls/tests/test_fips.py @@ -0,0 +1,58 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) +from typing import Any # noqa: F401 + +import pytest + +from datadog_checks.tls import TLSCheck +from datadog_checks.tls.const import ( + SERVICE_CHECK_CAN_CONNECT, + SERVICE_CHECK_VALIDATION, +) + + +@pytest.mark.e2e +@pytest.mark.fips_off +def test_connection_before_fips(clean_fips_environment, dd_fips_environment, dd_agent_check, instance_e2e_fips): + """ + Connection to the FIPS server before enabling FIPS mode should succeed. + """ + aggregator = dd_agent_check(instance_e2e_fips) + aggregator.assert_service_check(SERVICE_CHECK_CAN_CONNECT, status=TLSCheck.OK, count=1) + aggregator.assert_service_check(SERVICE_CHECK_VALIDATION, status=TLSCheck.OK, count=1) + + +@pytest.mark.e2e +@pytest.mark.fips_off +def test_connection_before_non_fips(clean_fips_environment, dd_fips_environment, dd_agent_check, instance_e2e_non_fips): + """ + Connection to the non-FIPS server before enabling FIPS mode should succeed. + """ + aggregator = dd_agent_check(instance_e2e_non_fips) + aggregator.assert_service_check(SERVICE_CHECK_CAN_CONNECT, status=TLSCheck.OK, count=1) + aggregator.assert_service_check(SERVICE_CHECK_VALIDATION, status=TLSCheck.OK, count=1) + + +@pytest.mark.e2e +@pytest.mark.fips_on +def test_connection_after_fips(clean_fips_environment, dd_fips_environment, dd_agent_check, instance_e2e_fips): + """ + Connection to the FIPS server after enabling FIPS mode should succeed. + """ + aggregator = dd_agent_check(instance_e2e_fips) + aggregator.assert_service_check(SERVICE_CHECK_CAN_CONNECT, status=TLSCheck.OK, count=1) + aggregator.assert_service_check(SERVICE_CHECK_VALIDATION, status=TLSCheck.OK, count=1) + + +@pytest.mark.e2e +@pytest.mark.fips_on +def test_connection_after_non_fips(clean_fips_environment, dd_fips_environment, dd_agent_check, instance_e2e_non_fips): + """ + Connection to the non-FIPS server after enabling FIPS mode should fail. + """ + aggregator = dd_agent_check(instance_e2e_non_fips) + aggregator.assert_service_check( + SERVICE_CHECK_VALIDATION, + message="[SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] ssl/tls alert handshake failure (_ssl.c:1000)", + )