diff --git a/.github/workflows/experimental.yml b/.github/workflows/experimental.yml new file mode 100644 index 0000000000000..8e3d099a23b8d --- /dev/null +++ b/.github/workflows/experimental.yml @@ -0,0 +1,201 @@ +name: Test FIPS experimental + +on: + workflow_dispatch: + inputs: + zip_url: + required: true + type: string + default: 'https://agent-ints-python-build-sandbox.s3.eu-north-1.amazonaws.com/python-windows-combined-v3.12.6-openssl-3.0.15-openssl-3.0.9-amd64.zip' + pull_request: + path: + - datadog_checks_base/datadog_checks/** + schedule: + - cron: '0 0,8,16 * * *' + +defaults: + run: + shell: bash + +jobs: + test: + strategy: + matrix: + include: + - platform: "Windows" + runner: "windows-2022" + zip_url: "https://agent-ints-python-build-sandbox.s3.eu-north-1.amazonaws.com/python-windows-combined-v3.12.6-openssl-3.0.15-openssl-3.0.9-amd64.zip" + - platform: "Linux" + runner: "ubuntu-22.04" + zip_url: "" + name: FIPS test on ${{ matrix.platform }} + runs-on: ${{ matrix.runner }} + + env: + FORCE_COLOR: "1" + DEBIAN_FRONTEND: "noninteractive" + OPENSSL_FIPS: "1" + PYTHON_VERSION: "3.12" + OPENSSL_VERSION: "3.0.15" + FIPS_MODULE_VERSION: "3.0.9" + + steps: + + - uses: actions/checkout@v4 + + - name: Install System Dependencies + if: runner.os == 'Linux' + run: | + sudo apt update + sudo apt install -y --no-install-recommends \ + wget \ + build-essential \ + gcc \ + make \ + perl \ + libc6-dev + + - name: Build FIPS Module + if: runner.os == 'Linux' + run: | + wget https://www.openssl.org/source/openssl-${{ env.FIPS_MODULE_VERSION }}.tar.gz \ + && tar -xvzf openssl-${{ env.FIPS_MODULE_VERSION }}.tar.gz \ + && cd openssl-${{ env.FIPS_MODULE_VERSION }} \ + && ./Configure enable-fips \ + && make \ + && sudo make install + + - name: Build OpenSSL + if: runner.os == 'Linux' + run: | + wget https://www.openssl.org/source/openssl-${{ env.OPENSSL_VERSION }}.tar.gz \ + && tar -xvzf openssl-${{ env.OPENSSL_VERSION }}.tar.gz \ + && cd openssl-${{ env.OPENSSL_VERSION }} \ + && ./Configure enable-fips \ + && make \ + && sudo make install + + - name: Build Python from Source with Custom OpenSSL + if: runner.os == 'Linux' + run: | + + # Install dependencies for building Python + sudo apt-get update && sudo apt-get install -y \ + build-essential \ + zlib1g-dev \ + libffi-dev \ + libssl-dev \ + libncurses5-dev \ + libsqlite3-dev \ + libreadline-dev \ + libbz2-dev \ + liblzma-dev \ + tk-dev \ + uuid-dev \ + libgdbm-dev \ + wget + + # Download and extract Python source + wget https://www.python.org/ftp/python/${{ env.PYTHON_VERSION }}/Python-${{ env.PYTHON_VERSION }}.tgz + tar -xvzf Python-${{ env.PYTHON_VERSION }}.tgz -C python_dir + cd python_dir + + # Configure and build Python with custom OpenSSL + ./configure --enable-optimizations --with-openssl=$(pwd)/../openssl-${{ env.OPENSSL_VERSION }} + make -j$(nproc) + sudo make altinstall + + - name: Download python-windows-combined + if: runner.os == 'Windows' + shell: powershell + run: | + Invoke-WebRequest -Uri ${{ inputs.zip_url || matrix.zip_url }} -OutFile 'python_combined.zip' + + - name: Unzip python_combined.zip + if: runner.os == 'Windows' + shell: powershell + run: | + Expand-Archive -Path python_combined.zip -DestinationPath .\python_dir + + - name: Run fipsintall.exe + if: runner.os == 'Windows' + working-directory: .\python_dir + shell: powershell + run: | + .\openssl.exe fipsinstall -module .\ossl-modules\fips.dll -out fipsmodule.cnf + + - name: Configure OpenSSL for FIPS + if: runner.os == 'Windows' + working-directory: .\python_dir + shell: powershell + run: | + # Create openssl.cnf to enable FIPS mode + $OpenSSLConf = @" + config_diagnostics = 1 + openssl_conf = openssl_init + + .include fipsmodule.cnf + + [openssl_init] + providers = provider_sect + alg_section = algorithm_sect + + [provider_sect] + fips = fips_sect + base = base_sect + + [base_sect] + activate = 1 + + [algorithm_sect] + default_properties = fips=yes + "@ + $OpenSSLConf | Set-Content -Path ".\openssl.cnf" + + - name: Verify OpenSSL + if: runner.os == 'Windows' + working-directory: .\python_dir + shell: powershell + run: | + .\openssl.exe version -a + .\openssl.exe list -providers + + - name: Verify OpenSSL with FIPS ENV vars + if: runner.os == 'Windows' + working-directory: .\python_dir + shell: powershell + run: | + $env:OPENSSL_MODULES = ".\ossl-modules" + $env:OPENSSL_CONF = ".\openssl.cnf" + .\openssl.exe list -providers + + - name: Add Python to PATH Windows + if: runner.os == 'Windows' + shell: powershell + run: | + Add-Content -Path $env:GITHUB_ENV -Value "PATH=$(pwd)\python_dir;$(pwd)\python_dir\Scripts;$env:PATH" + + - name: Add Python to PATH Linux + if: runner.os == 'Linux' + run: | + echo "PATH=$(pwd)/python_dir:$PATH" >> $GITHUB_ENV + + - name: Install pip and pytest + run: | + python -m ensurepip + + - name: Test + if: runner.os == 'Windows' + working-directory: .\python_dir + shell: powershell + run: | + $env:PATH_TO_OPENSSL_CONF = "$(pwd)\openssl.cnf" + $env:PATH_TO_OPENSSL_MODULES = "$(pwd)\ossl-modules" + $env:OPENSSL_CONF = "$(pwd)\openssl.cnf" + $env:OPENSSL_MODULES = "$(pwd)\ossl-modules" + .\openssl.exe list -providers + .\openssl.exe md5 + python -m pip install pytest + python -m pytest ..\datadog_checks_base -m fips_off + python -m pytest ..\datadog_checks_base -m fips_on + python -c "import ssl; ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT).set_ciphers('MD5')" 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 c87b2cdb70479..7c38d57c626b4 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/fips/test_fips.py b/datadog_checks_base/tests/fips/test_fips.py new file mode 100644 index 0000000000000..426e5e0861265 --- /dev/null +++ b/datadog_checks_base/tests/fips/test_fips.py @@ -0,0 +1,68 @@ +# (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 os +import pytest + +from datadog_checks.base.utils.fips import enable_fips + +PATH_TO_OPENSSL_CONF = os.getenv("PATH_TO_OPENSSL_CONF") +PATH_TO_OPENSSL_MODULES = os.getenv("PATH_TO_OPENSSL_MODULES") + + +@pytest.fixture(scope="function") +def clean_environment(monkeypatch): + monkeypatch.setenv("GOFIPS", "0") + monkeypatch.setenv("OPENSSL_CONF", "") + monkeypatch.setenv("OPENSSL_MODULES", "") + yield + + +@pytest.mark.fips_off +def test_ssl_md5_before_fips(clean_environment): + """ + MD5 cipher should be available through ssl before enabling FIPS mode. + """ + import ssl + + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.set_ciphers("MD5") + assert True + + +@pytest.mark.fips_off +def test_cryptography_md5_before_fips(clean_environment): + """ + MD5 cipher should be available through cryptography before enabling FIPS mode. + """ + from cryptography.hazmat.primitives import hashes + + assert hashes.Hash(hashes.MD5()) + + +@pytest.mark.fips_on +def test_ssl_md5_after_fips(clean_environment): + """ + MD5 cipher should not be available through ssl after enabling FIPS mode. + """ + import ssl + + enable_fips(path_to_openssl_conf=PATH_TO_OPENSSL_CONF, path_to_openssl_modules=PATH_TO_OPENSSL_MODULES) + with pytest.raises(ssl.SSLError, match='No cipher can be selected.'): + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.set_ciphers("MD5") + + +@pytest.mark.fips_on +def test_cryptography_md5_after_fips(clean_environment): + """ + MD5 cipher should not be available through cryptography after enabling FIPS mode. + """ + from cryptography.exceptions import InternalError + from cryptography.hazmat.primitives import hashes + + enable_fips(path_to_openssl_conf=PATH_TO_OPENSSL_CONF, path_to_openssl_modules=PATH_TO_OPENSSL_MODULES) + with pytest.raises(InternalError, match='Unknown OpenSSL error.'): + hashes.Hash(hashes.MD5()) 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)", + )