Skip to content

Commit

Permalink
Merge pull request road-core#59 from tisnik/tls-security-profile-in-c…
Browse files Browse the repository at this point in the history
…onfig

TLS security profile in configuration
  • Loading branch information
tisnik authored Oct 18, 2024
2 parents 78c8d79 + 089912b commit 29d2a72
Show file tree
Hide file tree
Showing 3 changed files with 297 additions and 0 deletions.
53 changes: 53 additions & 0 deletions ols/app/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
)

from ols import constants
from ols.utils import tls


def _is_valid_http_url(url: AnyHttpUrl) -> bool:
Expand Down Expand Up @@ -872,6 +873,51 @@ def check_storage_location_is_set_when_needed(self) -> Self:
return self


class TLSSecurityProfile(BaseModel):
"""TLS security profile structure."""

profile_type: Optional[str] = None
min_tls_version: Optional[str] = None
ciphers: Optional[list[str]] = None

def __init__(self, data: Optional[dict] = None) -> None:
"""Initialize configuration and perform basic validation."""
super().__init__()
if data is not None:
self.profile_type = data.get("type")
self.min_tls_version = data.get("minTLSVersion")
self.ciphers = data.get("ciphers")

def validate_yaml(self) -> None:
"""Validate structure content."""
# check the TLS profile type
if self.profile_type is not None:
try:
tls.TLSProfiles(self.profile_type)
except ValueError:
raise InvalidConfigurationError(
f"Invalid TLS profile type '{self.profile_type}'"
)
# check the TLS protocol version
if self.min_tls_version is not None:
try:
tls.TLSProtocolVersion(self.min_tls_version)
except ValueError:
raise InvalidConfigurationError(
f"Invalid minimal TLS version '{self.min_tls_version}'"
)
# check ciphers
if self.ciphers is not None:
# just perform the check for non-custom TLS profile type
if self.profile_type is not None and self.profile_type != "Custom":
supported_ciphers = tls.TLS_CIPHERS[tls.TLSProfiles(self.profile_type)]
for cipher in self.ciphers:
if cipher not in supported_ciphers:
raise InvalidConfigurationError(
f"Unsupported cipher '{cipher}' found in configuration"
)


class OLSConfig(BaseModel):
"""OLS configuration."""

Expand All @@ -889,6 +935,7 @@ class OLSConfig(BaseModel):
query_validation_method: Optional[str] = constants.QueryValidationMethod.DISABLED

user_data_collection: UserDataCollection = UserDataCollection()
tls_security_profile: Optional[TLSSecurityProfile] = None

extra_ca: list[FilePath] = []
certificate_directory: Optional[str] = None
Expand Down Expand Up @@ -932,6 +979,9 @@ def __init__(
self.certificate_directory = data.get(
"certificate_directory", constants.DEFAULT_CERTIFICATE_DIRECTORY
)
self.tls_security_profile = TLSSecurityProfile(
data.get("tlsSecurityProfile", None)
)

def __eq__(self, other: object) -> bool:
"""Compare two objects for equality."""
Expand All @@ -947,6 +997,7 @@ def __eq__(self, other: object) -> bool:
and self.tls_config == other.tls_config
and self.certificate_directory == other.certificate_directory
and self.system_prompt == other.system_prompt
and self.tls_security_profile == other.tls_security_profile
)
return False

Expand All @@ -961,6 +1012,8 @@ def validate_yaml(self, disable_tls: bool = False) -> None:
if self.query_filters is not None:
for query_filter in self.query_filters:
query_filter.validate_yaml()
if self.tls_security_profile is not None:
self.tls_security_profile.validate_yaml()

valid_query_validation_methods = list(constants.QueryValidationMethod)
if self.query_validation_method not in valid_query_validation_methods:
Expand Down
90 changes: 90 additions & 0 deletions ols/utils/tls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""TLS-related data structures and constants."""

# For further information please look at TLS security profiles source:
# wokeignore:rule=master
# https://github.com/openshift/api/blob/master/config/v1/types_tlssecurityprofile.go

from enum import StrEnum


class TLSProfiles(StrEnum):
"""TLS profile names."""

OLD_TYPE = "OldType"
INTERMEDIATE_TYPE = "IntermediateType"
MODERN_TYPE = "ModernType"
CUSTOM_TYPE = "Custom"


class TLSProtocolVersion(StrEnum):
"""TLS protocol versions."""

# version 1.0 of the TLS security protocol.
VERSION_TLS_10 = "VersionTLS10"
# version 1.1 of the TLS security protocol.
VERSION_TLS_11 = "VersionTLS11"
# version 1.2 of the TLS security protocol.
VERSION_TLS_12 = "VersionTLS12"
# version 1.3 of the TLS security protocol.
VERSION_TLS_13 = "VersionTLS13"


# Minimal TLS versions required for each TLS profile
MIN_TLS_VERSIONS = {
TLSProfiles.OLD_TYPE: TLSProtocolVersion.VERSION_TLS_10,
TLSProfiles.INTERMEDIATE_TYPE: TLSProtocolVersion.VERSION_TLS_12,
TLSProfiles.MODERN_TYPE: TLSProtocolVersion.VERSION_TLS_13,
}

# TLS ciphers defined for each TLS profile
TLS_CIPHERS = {
TLSProfiles.OLD_TYPE: [
"TLS_AES_128_GCM_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_CHACHA20_POLY1305_SHA256",
"ECDHE-ECDSA-AES128-GCM-SHA256",
"ECDHE-RSA-AES128-GCM-SHA256",
"ECDHE-ECDSA-AES256-GCM-SHA384",
"ECDHE-RSA-AES256-GCM-SHA384",
"ECDHE-ECDSA-CHACHA20-POLY1305",
"ECDHE-RSA-CHACHA20-POLY1305",
"DHE-RSA-AES128-GCM-SHA256",
"DHE-RSA-AES256-GCM-SHA384",
"DHE-RSA-CHACHA20-POLY1305",
"ECDHE-ECDSA-AES128-SHA256",
"ECDHE-RSA-AES128-SHA256",
"ECDHE-ECDSA-AES128-SHA",
"ECDHE-RSA-AES128-SHA",
"ECDHE-ECDSA-AES256-SHA384",
"ECDHE-RSA-AES256-SHA384",
"ECDHE-ECDSA-AES256-SHA",
"ECDHE-RSA-AES256-SHA",
"DHE-RSA-AES128-SHA256",
"DHE-RSA-AES256-SHA256",
"AES128-GCM-SHA256",
"AES256-GCM-SHA384",
"AES128-SHA256",
"AES256-SHA256",
"AES128-SHA",
"AES256-SHA",
"DES-CBC3-SHA",
],
TLSProfiles.INTERMEDIATE_TYPE: [
"TLS_AES_128_GCM_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_CHACHA20_POLY1305_SHA256",
"ECDHE-ECDSA-AES128-GCM-SHA256",
"ECDHE-RSA-AES128-GCM-SHA256",
"ECDHE-ECDSA-AES256-GCM-SHA384",
"ECDHE-RSA-AES256-GCM-SHA384",
"ECDHE-ECDSA-CHACHA20-POLY1305",
"ECDHE-RSA-CHACHA20-POLY1305",
"DHE-RSA-AES128-GCM-SHA256",
"DHE-RSA-AES256-GCM-SHA384",
],
TLSProfiles.MODERN_TYPE: [
"TLS_AES_128_GCM_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_CHACHA20_POLY1305_SHA256",
],
}
154 changes: 154 additions & 0 deletions tests/unit/app/models/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pytest
from pydantic import ValidationError

import ols.utils.tls as tls
from ols import constants
from ols.app.models.config import (
AuthenticationConfig,
Expand All @@ -24,6 +25,7 @@
RedisConfig,
ReferenceContent,
TLSConfig,
TLSSecurityProfile,
UserDataCollection,
UserDataCollectorConfig,
)
Expand Down Expand Up @@ -1405,6 +1407,158 @@ def test_invalid_values():
LoggingConfig(uvicorn_log_level="foo")


def test_tls_security_profile_default_values():
"""Test the TLSSecurityProfile model."""
tls_security_profile = TLSSecurityProfile()
assert tls_security_profile.profile_type is None
assert tls_security_profile.min_tls_version is None
assert tls_security_profile.ciphers is None


def test_tls_security_profile_correct_values():
"""Test the TLSSecurityProfile model."""
tls_security_profile = TLSSecurityProfile(
{
"type": "Custom",
"minTLSVersion": "VersionTLS13",
"ciphers": [
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
],
}
)
assert tls_security_profile.profile_type == "Custom"
assert tls_security_profile.min_tls_version == "VersionTLS13"
assert "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" in tls_security_profile.ciphers
assert "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" in tls_security_profile.ciphers


tls_types = (
tls.TLSProfiles.OLD_TYPE,
tls.TLSProfiles.INTERMEDIATE_TYPE,
tls.TLSProfiles.MODERN_TYPE,
tls.TLSProfiles.CUSTOM_TYPE,
)


tls_versions = (
tls.TLSProtocolVersion.VERSION_TLS_10,
tls.TLSProtocolVersion.VERSION_TLS_11,
tls.TLSProtocolVersion.VERSION_TLS_12,
tls.TLSProtocolVersion.VERSION_TLS_13,
)


@pytest.mark.parametrize("tls_type", tls_types)
@pytest.mark.parametrize("min_tls_version", tls_versions)
def test_tls_security_profile_validate_yaml(tls_type, min_tls_version):
"""Test the TLSSecurityProfile model validation."""
tls_security_profile = TLSSecurityProfile()
tls_security_profile.validate_yaml()

tls_security_profile = TLSSecurityProfile(
{
"type": tls_type,
"minTLSVersion": min_tls_version,
"ciphers": [],
}
)
tls_security_profile.validate_yaml()


def test_tls_security_profile_validate_invalid_yaml_type():
"""Test the TLSSecurityProfile model validation."""
tls_security_profile = TLSSecurityProfile(
{
"type": "Foo",
"minTLSVersion": "VersionTLS13",
"ciphers": [
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
],
}
)
with pytest.raises(
InvalidConfigurationError,
match="Invalid TLS profile type 'Foo'",
):
tls_security_profile.validate_yaml()


def test_tls_security_profile_validate_invalid_yaml_min_tls_version():
"""Test the TLSSecurityProfile model validation."""
tls_security_profile = TLSSecurityProfile(
{
"type": "Custom",
"minTLSVersion": "foo",
"ciphers": [
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
],
}
)
with pytest.raises(
InvalidConfigurationError,
match="Invalid minimal TLS version 'foo'",
):
tls_security_profile.validate_yaml()


tls_types_without_custom = (
tls.TLSProfiles.OLD_TYPE,
tls.TLSProfiles.INTERMEDIATE_TYPE,
tls.TLSProfiles.MODERN_TYPE,
)


@pytest.mark.parametrize("tls_type", tls_types_without_custom)
def test_tls_security_profile_validate_invalid_yaml_ciphers(tls_type):
"""Test the TLSSecurityProfile model validation."""
tls_security_profile = TLSSecurityProfile(
{
"type": tls_type,
"minTLSVersion": "VersionTLS13",
"ciphers": [
"foo",
"bar",
],
}
)
with pytest.raises(
InvalidConfigurationError,
match="Unsupported cipher 'foo' found in configuration",
):
tls_security_profile.validate_yaml()


def test_tls_security_profile_equality():
"""Test equality or inequality of two security profiles."""
profile1 = TLSSecurityProfile(
{
"type": "Custom",
"minTLSVersion": "VersionTLS13",
"ciphers": [],
}
)
profile2 = TLSSecurityProfile(
{
"type": "Custom",
"minTLSVersion": "VersionTLS13",
"ciphers": [],
}
)
assert profile1 == profile2

profile3 = TLSSecurityProfile(
{
"type": "Custom",
"minTLSVersion": "VersionTLS12",
"ciphers": [],
}
)
assert profile1 != profile3


def test_tls_config_default_values():
"""Test the TLSConfig model."""
tls_config = TLSConfig()
Expand Down

0 comments on commit 29d2a72

Please sign in to comment.