diff --git a/operator/crd/crd.yaml b/operator/crd/crd.yaml index e44d3b0..3283ccb 100644 --- a/operator/crd/crd.yaml +++ b/operator/crd/crd.yaml @@ -110,7 +110,7 @@ spec: - key - type keystore: - description: "Configure server TLS connections using a JKS keystore. The keystore password should be stored in a secret and referenced in the route Custom Resource as a secretSource. The format of the secret should be `server.ssl.key-store-password=password`." + description: "Configure HTTP server TLS connections using a JKS or PKCS12 keystore. The keystore password should be stored in a secret and referenced in the route Custom Resource. The format of the secret should be `password=`." type: object properties: secretName: @@ -122,10 +122,13 @@ spec: enum: - jks - pkcs12 + passwordSecretRef: + type: string required: - secretName - key - type + - passwordSecretRef required: - routeConfigMap required: diff --git a/operator/example/testroute.yaml b/operator/example/testroute.yaml index c906053..276bebb 100644 --- a/operator/example/testroute.yaml +++ b/operator/example/testroute.yaml @@ -11,3 +11,9 @@ spec: # group: abc secretSources: - testroute-secret + tls: + keystore: + secretName: test-keystore + type: jks + key: keystore.jks + passwordSecretRef: jks-password diff --git a/operator/webhook/introute/sync.py b/operator/webhook/introute/sync.py index b2c06f2..e9ce196 100644 --- a/operator/webhook/introute/sync.py +++ b/operator/webhook/introute/sync.py @@ -174,21 +174,36 @@ def _spring_cloud_k8s_config(parent) -> Optional[Mapping]: } -def _spring_app_config_env_var(parent) -> Mapping[str, str]: +def _get_server_ssl_config(parent) -> Optional[Mapping]: + tls_config = parent["spec"].get("tls") + if not tls_config: + return None + + keystore = tls_config.get("keystore") + + if not keystore: + return None + + return { + "ssl": { + "key-alias": "certificate", + "key-store": str(Path(KEYSTORE_PATH, keystore["key"])), + "key-store-type": keystore["type"].upper() + }, + "port": 8443 + } + + +def _spring_app_config_env_var(parent) -> Optional[Mapping]: metadata = parent["metadata"] app_config = { "spring": { "application": {"name": metadata["name"]}, } } - app_config["management"] = { - "server": { - "port": 80, - "ssl": { - "enabled": False - } - } - } + + if tls_config := _get_server_ssl_config(parent): + app_config["server"] = tls_config if cloud_config := _spring_cloud_k8s_config(parent): app_config["spring"]["config.import"] = "kubernetes:" @@ -200,60 +215,39 @@ def _spring_app_config_env_var(parent) -> Mapping[str, str]: } -def _get_tls_options(parent) -> Optional[List[Mapping[str, str]]]: - tls_config = parent["spec"].get("tls") +def _get_keystore_password_env(tls) -> Mapping[str, str]: - if not tls_config: + keystore = tls.get("keystore") + + if not keystore: return None - tls_env_vars = [] - if 'truststore' in tls_config: - tls_type = tls_config['truststore']['type'] - _validate_tls_type(tls_type) - truststore_password = "changeit" if tls_type == "jks" else "" - truststore_vars = [ - { - "name": "SERVER_SSL_TRUSTSTORE", - "value": str(Path(TRUSTSTORE_PATH, tls_config['truststore']['key'])) - }, - { - "name": "SERVER_SSL_TRUSTSTOREPASSWORD", - "value": truststore_password - }, - { - "name": "SERVER_SSL_TRUSTSTORETYPE", - "value": tls_type.upper() + return { + "name": "SERVER_SSL_KEYSTOREPASSWORD", + "valueFrom": { + "secretKeyRef": { + "name": keystore["passwordSecretRef"], + "key": "password" } - ] - tls_env_vars.extend(truststore_vars) + } + } - if 'keystore' in tls_config: - tls_type = tls_config['keystore']['type'] - _validate_tls_type(tls_type) - keystore_vars = [ - { - "name": "SERVER_PORT", - "value": 443 - }, - { - "name": "SERVER_SSL_KEYSTORE", - "value": str(Path(KEYSTORE_PATH, tls_config['keystore']['key'])) - }, - { - "name": "SERVER_SSL_KEYSTORETYPE", - "value": tls_type.upper() - }, - { - "name": "SERVER_SSL_KEYALIAS", - "value": "certificate" - } - ] - tls_env_vars.extend(keystore_vars) - return tls_env_vars -def _validate_tls_type(tls_type): - assert tls_type in ["jks", "pkcs12"], \ - f"({tls_type}) is not a supported TLS type. Supported types: ('jks', 'pkcs12')" +def _get_java_jdk_options(tls) -> Optional[Mapping[str, str]]: + + truststore = tls.get("truststore") + + if not truststore: + return None + + tls_type = truststore["type"] + truststore_password = "changeit" if tls_type == "jks" else "" + + return { + "name": "JDK_JAVA_OPTIONS", + "value": f"-Djavax.net.ssl.trustStore={str(Path(TRUSTSTORE_PATH, truststore['key']))} -Djavax.net.ssl.trustStorePassword={truststore_password} -Djavax.net.ssl.trustStoreType={tls_type.upper()}" + } + def _generate_container_env_vars(parent) -> List[Mapping[str, str]]: env_vars = [] @@ -261,8 +255,13 @@ def _generate_container_env_vars(parent) -> List[Mapping[str, str]]: if spring_app_config := _spring_app_config_env_var(parent): env_vars.append(spring_app_config) - if tls_options := _get_tls_options(parent): - env_vars.extend(tls_options) + if tls := parent["spec"].get("tls"): + if jdk_options := _get_java_jdk_options(tls): + env_vars.append(jdk_options) + + if keystore_password_env := _get_keystore_password_env(tls): + env_vars.append(keystore_password_env) + return env_vars @@ -284,14 +283,16 @@ def _create_pod_template(parent, labels, integration_image): "livenessProbe": { "httpGet": { "path": "/actuator/health/liveness", - "port": 80, + "port": 8443, + "scheme": "HTTPS" }, "initialDelaySeconds": 10, }, "readinessProbe": { "httpGet": { "path": "/actuator/health/readiness", - "port": 80, + "port": 8443, + "scheme": "HTTPS" }, "initialDelaySeconds": 10, }, diff --git a/operator/webhook/test/json/full-iroute-request.json b/operator/webhook/test/json/full-iroute-request.json index 44f7550..700d97e 100644 --- a/operator/webhook/test/json/full-iroute-request.json +++ b/operator/webhook/test/json/full-iroute-request.json @@ -44,7 +44,8 @@ "keystore": { "secretName": "test-tls-secret", "key": "test-keystore.jks", - "type": "jks" + "type": "jks", + "passwordSecretRef": "keystore-password-ref" }, "truststore": { "configMapName": "test-tls-cm", diff --git a/operator/webhook/test/json/full-response.json b/operator/webhook/test/json/full-response.json index 0401043..e1aacce 100644 --- a/operator/webhook/test/json/full-response.json +++ b/operator/webhook/test/json/full-response.json @@ -79,49 +79,36 @@ "livenessProbe": { "httpGet": { "path": "/actuator/health/liveness", - "port": 80 + "port": 8443, + "scheme": "HTTPS" }, "initialDelaySeconds": 10 }, "readinessProbe": { "httpGet": { "path": "/actuator/health/readiness", - "port": 80 + "port": 8443, + "scheme": "HTTPS" }, "initialDelaySeconds": 10 }, "env": [ { "name": "SPRING_APPLICATION_JSON", - "value": "{\"spring\": {\"application\": {\"name\": \"testroute\"}, \"config.import\": \"kubernetes:\", \"cloud\": {\"kubernetes\": {\"config\": {\"fail-fast\": true, \"namespace\": \"testspace\", \"sources\": [{\"name\": \"testroute-props\"}, {\"labels\": {\"group\": \"ir-common\"}}]}, \"secrets\": {\"paths\": \"/etc/secrets\"}}}}, \"management\": {\"server\": {\"port\": 80, \"ssl\": {\"enabled\": false}}}}" + "value": "{\"spring\": {\"application\": {\"name\": \"testroute\"}, \"config.import\": \"kubernetes:\", \"cloud\": {\"kubernetes\": {\"config\": {\"fail-fast\": true, \"namespace\": \"testspace\", \"sources\": [{\"name\": \"testroute-props\"}, {\"labels\": {\"group\": \"ir-common\"}}]}, \"secrets\": {\"paths\": \"/etc/secrets\"}}}}, \"server\": {\"ssl\": {\"key-alias\": \"certificate\", \"key-store\": \"/etc/keystore/test-keystore.jks\", \"key-store-type\": \"JKS\"}, \"port\": 8443}}" }, { - "name": "SERVER_SSL_TRUSTSTORE", - "value": "/etc/cabundle/test-truststore.p12" + "name": "JDK_JAVA_OPTIONS", + "value": "-Djavax.net.ssl.trustStore=/etc/cabundle/test-truststore.p12 -Djavax.net.ssl.trustStorePassword= -Djavax.net.ssl.trustStoreType=PKCS12" }, { - "name": "SERVER_SSL_TRUSTSTOREPASSWORD", - "value": "" - }, - { - "name": "SERVER_SSL_TRUSTSTORETYPE", - "value": "PKCS12" - }, - { - "name": "SERVER_PORT", - "value": 443 - }, - { - "name": "SERVER_SSL_KEYSTORE", - "value": "/etc/keystore/test-keystore.jks" - }, - { - "name": "SERVER_SSL_KEYSTORETYPE", - "value": "JKS" - }, - { - "name": "SERVER_SSL_KEYALIAS", - "value": "certificate" + "name": "SERVER_SSL_KEYSTOREPASSWORD", + "valueFrom": { + "secretKeyRef": { + "name": "keystore-password-ref", + "key": "password" + } + } } ] } @@ -187,4 +174,4 @@ } } ] -} \ No newline at end of file +} diff --git a/operator/webhook/test/test_sync.py b/operator/webhook/test/test_sync.py index 3c25089..bf247b4 100644 --- a/operator/webhook/test/test_sync.py +++ b/operator/webhook/test/test_sync.py @@ -11,9 +11,12 @@ SECRETS_ROOT, _new_deployment, _spring_app_config_env_var, - _get_tls_options, + _get_java_jdk_options, + _generate_container_env_vars + ) +JDK_OPTIONS_ENV_NAME = "JDK_JAVA_OPTIONS" def test_empty_parent_raises_exception(full_route): with pytest.raises(KeyError): @@ -42,28 +45,6 @@ def test_vol_config_missing_optional_vols_no_fail(full_route): assert len(vol_conf.get_volumes()) > 0 assert len(vol_conf.get_mounts()) > 0 -def test_tls_config_missing(full_route): - del full_route["spec"]["tls"] - vol_conf = VolumeConfig(full_route["spec"]) - - assert len(vol_conf.get_volumes()) > 0 - assert len(vol_conf.get_mounts()) > 0 - - print(vol_conf) - -def test_tls_config_missing_client_config(full_route): - del full_route["spec"]["tls"]["truststore"] - vol_conf = VolumeConfig(full_route["spec"]) - - assert len(vol_conf.get_volumes()) > 0 - assert len(vol_conf.get_mounts()) > 0 - -def test_tls_config_missing_server_config(full_route): - del full_route["spec"]["tls"]["keystore"] - vol_conf = VolumeConfig(full_route["spec"]) - - assert len(vol_conf.get_volumes()) > 0 - assert len(vol_conf.get_mounts()) > 0 def test_spring_app_config_json_missing_props_sources(full_route): del full_route["spec"]["propSources"] @@ -90,93 +71,86 @@ def test_spring_app_config_json_missing_props_and_secret_sources(full_route): assert spring_conf["name"] == "SPRING_APPLICATION_JSON" - expected_json = {"spring": {"application": {"name": "testroute"}}, "management": {"server": {"port": 80, "ssl": {"enabled": False}}}} + expected_json = {"spring": {"application": {"name": "testroute"}}, "server": {"ssl": {"key-alias": "certificate", "key-store": "/etc/keystore/test-keystore.jks", "key-store-type": "JKS"}, "port": 8443}} + assert json_props == expected_json -def test_tls_options_pkcs12_type(full_route): - options = _get_tls_options(full_route) +def test_pod_template_no_annotations(full_route): + del full_route["spec"]["annotations"] - assert len(options) > 0 + deployment = _new_deployment(full_route) - truststore_key = next((x for x in options if x.get('name') == 'SERVER_SSL_TRUSTSTORE'), None) - truststore_type = next((x for x in options if x.get('name') == 'SERVER_SSL_TRUSTSTORETYPE'), None) - assert truststore_key.get('value') == "/etc/cabundle/test-truststore.p12" - assert truststore_type.get('value') == "PKCS12" + pod_template = deployment["spec"]["template"] + assert pod_template["metadata"].get("annotations") is None - keystore_key = next((x for x in options if x.get('name') == 'SERVER_SSL_KEYSTORE'), None) - keystore_type = next((x for x in options if x.get('name') == 'SERVER_SSL_KEYSTORETYPE'), None) - assert keystore_key.get('value') == "/etc/keystore/test-keystore.jks" - assert keystore_type.get('value') == "JKS" +def test_pod_template_empty_annotations(full_route): + full_route["spec"]["annotations"] = {} -def test_tls_options_no_truststore(full_route): - tls_config = full_route["spec"]["tls"] + deployment = _new_deployment(full_route) - del full_route["spec"]["tls"]["keystore"] - options = _get_tls_options(full_route) + pod_template = deployment["spec"]["template"] + assert pod_template["metadata"].get("annotations") is None - assert not any(x for x in options if x.get('name') == 'SERVER_PORT') - assert not any(x for x in options if x.get('name') == 'SERVER_SSL_KEYSTORE') - assert not any(x for x in options if x.get('name') == 'SERVER_SSL_KEYSTORETYPE') - assert not any(x for x in options if x.get('name') == 'SERVER_SSL_KEYALIAS') +def test_pod_template_no_tls(full_route): + del full_route["spec"]["tls"] -def test_tls_options_no_keystore(full_route): - tls_config = full_route["spec"]["tls"] + deployment = _new_deployment(full_route) - del full_route["spec"]["tls"]["keystore"] - options = _get_tls_options(full_route) + check_volume_absent(deployment, "truststore") + check_volume_mounts_absent(deployment, "truststore") + check_volume_absent(deployment, "keystore") + check_volume_mounts_absent(deployment, "keystore") - assert not any(x for x in options if x.get('name') == 'SERVER_PORT') - assert not any(x for x in options if x.get('name') == 'SERVER_SSL_KEYSTORE') - assert not any(x for x in options if x.get('name') == 'SERVER_SSL_KEYSTORETYPE') - assert not any(x for x in options if x.get('name') == 'SERVER_SSL_KEYALIAS') +def test_pod_template_no_truststore(full_route): + del full_route["spec"]["tls"]["truststore"] -def test_jdk_options_truststore_unknown_type(full_route): - tls_config = full_route["spec"]["tls"]["truststore"] - tls_config["type"] = "pem" + deployment = _new_deployment(full_route) - with pytest.raises(AssertionError): - _get_tls_options(full_route) + check_volume_absent(deployment, "truststore") + check_volume_mounts_absent(deployment, "truststore") -def test_jdk_options_keystore_unknown_type(full_route): - tls_config = full_route["spec"]["tls"]["keystore"] - tls_config["type"] = "pem" +def test_pod_template_no_keystore(full_route): + del full_route["spec"]["tls"]["keystore"] - with pytest.raises(AssertionError): - _get_tls_options(full_route) + deployment = _new_deployment(full_route) + check_volume_absent(deployment, "keystore") + check_volume_mounts_absent(deployment, "keystore") -def test_pod_template_no_annotations(full_route): - del full_route["spec"]["annotations"] - deployment = _new_deployment(full_route) +def test_jdk_options_pkcs12_type(full_route): + tls_config = full_route["spec"]["tls"] + options = _get_java_jdk_options(tls_config) - pod_template = deployment["spec"]["template"] - assert pod_template["metadata"].get("annotations") is None + assert options["name"] == JDK_OPTIONS_ENV_NAME + expected_options = "-Djavax.net.ssl.trustStore=/etc/cabundle/test-truststore.p12 -Djavax.net.ssl.trustStorePassword= -Djavax.net.ssl.trustStoreType=PKCS12" + assert options["value"] == expected_options -def test_pod_template_empty_annotations(full_route): - full_route["spec"]["annotations"] = {} - deployment = _new_deployment(full_route) +def test_jdk_options_jks_type(full_route): + tls_config = full_route["spec"]["tls"] + tls_config["truststore"]["type"] = "jks" + tls_config["truststore"]["key"] = "test-truststore.jks" - pod_template = deployment["spec"]["template"] - assert pod_template["metadata"].get("annotations") is None + options = _get_java_jdk_options(tls_config) + assert options["name"] == JDK_OPTIONS_ENV_NAME + expected_options = "-Djavax.net.ssl.trustStore=/etc/cabundle/test-truststore.jks -Djavax.net.ssl.trustStorePassword=changeit -Djavax.net.ssl.trustStoreType=JKS" + assert options["value"] == expected_options -def test_pod_template_no_tls(full_route): - del full_route["spec"]["tls"] - deployment = _new_deployment(full_route) +def test_env_vars_no_keystore(full_route): + del full_route["spec"]["tls"]["keystore"] - check_volume_absent(deployment, "truststore") - check_volume_mounts_absent(deployment, "truststore") - check_volume_absent(deployment, "keystore") - check_volume_mounts_absent(deployment, "keystore") + options = _generate_container_env_vars(full_route) + + assert not any(x for x in options if x.get('name') == 'SERVER_SSL_KEYSTORE_PASSWORD') def test_deployment_missing_labels(full_route):