Skip to content

Commit c0fdf99

Browse files
feat: TLS support (#774)
* feat: tls support * fix: python formatting * fix: pre-commit lint fix * Update docs/modules/opa/pages/usage-guide/tls.adoc Co-authored-by: Malte Sander <[email protected]> * Update docs/modules/opa/pages/usage-guide/tls.adoc Co-authored-by: Malte Sander <[email protected]> * Update docs/modules/opa/pages/usage-guide/tls.adoc Co-authored-by: Malte Sander <[email protected]> * Update tests/templates/kuttl/smoke/09-install-secretclass.yaml.j2 Co-authored-by: Malte Sander <[email protected]> * Update tests/templates/kuttl/smoke/10-install-opa.yaml.j2 Co-authored-by: Malte Sander <[email protected]> * refactor: streamline TLS configuration checks and add tls_enabled method * feat: test OPA using HTTP in smoke test as well * chore: newline at end of file * feat: make smoke test use tls dimension * chore: newline at end of file --------- Co-authored-by: Malte Sander <[email protected]>
1 parent 3231239 commit c0fdf99

File tree

21 files changed

+436
-142
lines changed

21 files changed

+436
-142
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.
1515
- Helm: Allow Pod `priorityClassName` to be configured ([#762]).
1616
- Add support for OPA `1.8.0` ([#765]).
1717
- Add `prometheus.io/path|port|scheme` annotations to metrics service ([#767]).
18+
- Add support for TLS ([#774])
1819

1920
### Changed
2021

@@ -41,6 +42,7 @@ All notable changes to this project will be documented in this file.
4142
[#767]: https://github.com/stackabletech/opa-operator/pull/767
4243
[#771]: https://github.com/stackabletech/opa-operator/pull/771
4344
[#772]: https://github.com/stackabletech/opa-operator/pull/772
45+
[#774]: https://github.com/stackabletech/opa-operator/pull/774
4446

4547
## [25.7.0] - 2025-07-23
4648

deploy/helm/opa-operator/crds/crds.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ spec:
2727
clusterConfig:
2828
default:
2929
listenerClass: cluster-internal
30+
tls: null
3031
userInfo: null
3132
description: Global OPA cluster configuration that applies to all roles and role groups.
3233
properties:
@@ -49,6 +50,19 @@ spec:
4950
- external-unstable
5051
- external-stable
5152
type: string
53+
tls:
54+
description: |-
55+
TLS encryption settings for the OPA server.
56+
When configured, OPA will use HTTPS (port 8443) instead of HTTP (port 8081).
57+
Clients must connect using HTTPS and trust the certificates provided by the configured SecretClass.
58+
nullable: true
59+
properties:
60+
serverSecretClass:
61+
description: Name of the SecretClass which will provide TLS certificates for the OPA server.
62+
type: string
63+
required:
64+
- serverSecretClass
65+
type: object
5266
userInfo:
5367
description: |-
5468
Configures how to fetch additional metadata about users (such as group memberships)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
= Enabling TLS Encryption
2+
:description: Learn how to enable TLS encryption for your OPA cluster to secure client connections.
3+
4+
TLS encryption for securing client connections to the OPA server can be configured in the `OpaCluster` resource. When enabled, OPA serves requests over HTTPS instead of HTTP.
5+
6+
== Overview
7+
8+
TLS encryption in OPA is disabled by default. To enable it, you need to:
9+
10+
1. Create a `SecretClass` that provides TLS certificates
11+
2. Reference the `SecretClass` in your `OpaCluster` custom resource
12+
13+
The operator integrates with the xref:secret-operator:index.adoc[Secret Operator] to automatically provision and mount TLS certificates into the OPA pods.
14+
15+
== Configuration
16+
17+
=== Creating a SecretClass
18+
19+
First, create a `SecretClass` that will provide TLS certificates. Here's an example using xref:secret-operator:secretclass.adoc#backend-autotls[autoTls]:
20+
21+
[source,yaml]
22+
----
23+
apiVersion: secrets.stackable.tech/v1alpha1
24+
kind: SecretClass
25+
metadata:
26+
name: opa-tls
27+
spec:
28+
backend:
29+
autoTls:
30+
ca:
31+
autoGenerate: true
32+
secret:
33+
name: opa-tls-ca
34+
namespace: default
35+
----
36+
37+
This SecretClass uses the autoTls backend, which automatically generates a Certificate Authority (CA) and signs certificates for your OPA cluster.
38+
39+
Similarly, you can also use xref:secret-operator:secretclass.adoc#backend[other backends] supported by Secret Operator.
40+
41+
=== Enabling TLS in OpaCluster
42+
43+
Once you have a SecretClass, enable TLS in your OpaCluster by setting the `.spec.clusterConfig.tls.serverSecretClass` field:
44+
45+
[source,yaml]
46+
----
47+
kind: OpaCluster
48+
name: opa-with-tls
49+
spec:
50+
clusterConfig:
51+
tls:
52+
serverSecretClass: opa-tls # <1>
53+
----
54+
<1> Reference the SecretClass created above
55+
56+
== Discovery ConfigMap
57+
58+
The operator automatically creates a discovery ConfigMap, with the same name as the OPA cluster, that contains the connection URL for your cluster. When TLS is enabled, this ConfigMap will contain an HTTPS URL and the SecretClass name:
59+
60+
[source,yaml]
61+
----
62+
apiVersion: v1
63+
kind: ConfigMap
64+
metadata:
65+
name: opa-with-tls
66+
data:
67+
OPA: "https://opa-with-tls.default.svc.cluster.local:8443/"
68+
OPA_SECRET_CLASS: "opa-tls"
69+
----
70+
71+
Applications can use this ConfigMap to discover and connect to the OPA cluster automatically.

docs/modules/opa/partials/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
** xref:opa:usage-guide/monitoring.adoc[]
1111
** xref:opa:usage-guide/OpenTelemetry.adoc[]
1212
** xref:opa:usage-guide/configuration-environment-overrides.adoc[]
13+
** xref:opa:usage-guide/tls.adoc[]
1314
** xref:opa:usage-guide/operations/index.adoc[]
1415
*** xref:opa:usage-guide/operations/cluster-operations.adoc[]
1516
// *** xref:hdfs:usage-guide/operations/pod-placement.adoc[] Missing

rust/operator-binary/src/controller.rs

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use stackable_operator::{
2323
container::{ContainerBuilder, FieldPathEnvVar},
2424
resources::ResourceRequirementsBuilder,
2525
security::PodSecurityContextBuilder,
26-
volume::VolumeBuilder,
26+
volume::{SecretOperatorVolumeSourceBuilder, VolumeBuilder},
2727
},
2828
},
2929
cluster_resources::{ClusterResourceApplyStrategy, ClusterResources},
@@ -104,6 +104,8 @@ const USER_INFO_FETCHER_CREDENTIALS_VOLUME_NAME: &str = "credentials";
104104
const USER_INFO_FETCHER_CREDENTIALS_DIR: &str = "/stackable/credentials";
105105
const USER_INFO_FETCHER_KERBEROS_VOLUME_NAME: &str = "kerberos";
106106
const USER_INFO_FETCHER_KERBEROS_DIR: &str = "/stackable/kerberos";
107+
const TLS_VOLUME_NAME: &str = "tls";
108+
const TLS_STORE_DIR: &str = "/stackable/tls";
107109

108110
const DOCKER_IMAGE_BASE_NAME: &str = "opa";
109111

@@ -329,6 +331,11 @@ pub enum Error {
329331

330332
#[snafu(display("failed to build service"))]
331333
BuildService { source: service::Error },
334+
335+
#[snafu(display("failed to build TLS volume"))]
336+
TlsVolumeBuild {
337+
source: builder::pod::volume::SecretOperatorVolumeSourceBuilderError,
338+
},
332339
}
333340
type Result<T, E = Error> = std::result::Result<T, E>;
334341

@@ -835,29 +842,50 @@ fn build_server_rolegroup_daemonset(
835842
.args(vec![build_opa_start_command(
836843
merged_config,
837844
&opa_container_name,
845+
opa.spec.cluster_config.tls_enabled(),
838846
)])
839847
.add_env_vars(env)
840848
.add_env_var(
841849
"CONTAINERDEBUG_LOG_DIRECTORY",
842850
format!("{STACKABLE_LOG_DIR}/containerdebug"),
843-
)
844-
.add_container_port(APP_PORT_NAME, APP_PORT.into())
845-
// If we also add a container port "metrics" pointing to the same port number, we get a
846-
//
847-
// .spec.template.spec.containers[name="opa"].ports: duplicate entries for key [containerPort=8081,protocol="TCP"]
848-
//
849-
// So we don't do that
851+
);
852+
853+
// Add appropriate container port based on TLS configuration
854+
// If we also add a container port "metrics" pointing to the same port number, we get a
855+
//
856+
// .spec.template.spec.containers[name="opa"].ports: duplicate entries for key [containerPort=8081,protocol="TCP"]
857+
//
858+
// So we don't do that
859+
if opa.spec.cluster_config.tls_enabled() {
860+
cb_opa.add_container_port(service::APP_TLS_PORT_NAME, service::APP_TLS_PORT.into());
861+
cb_opa
862+
.add_volume_mount(TLS_VOLUME_NAME, TLS_STORE_DIR)
863+
.context(AddVolumeMountSnafu)?;
864+
} else {
865+
cb_opa.add_container_port(APP_PORT_NAME, APP_PORT.into());
866+
}
867+
868+
cb_opa
850869
.add_volume_mount(CONFIG_VOLUME_NAME, CONFIG_DIR)
851870
.context(AddVolumeMountSnafu)?
852871
.add_volume_mount(LOG_VOLUME_NAME, STACKABLE_LOG_DIR)
853872
.context(AddVolumeMountSnafu)?
854-
.resources(merged_config.resources.to_owned().into())
873+
.resources(merged_config.resources.to_owned().into());
874+
875+
let (probe_port_name, probe_scheme) = if opa.spec.cluster_config.tls_enabled() {
876+
(service::APP_TLS_PORT_NAME, Some("HTTPS".to_string()))
877+
} else {
878+
(APP_PORT_NAME, Some("HTTP".to_string()))
879+
};
880+
881+
cb_opa
855882
.readiness_probe(Probe {
856883
initial_delay_seconds: Some(5),
857884
period_seconds: Some(10),
858885
failure_threshold: Some(5),
859886
http_get: Some(HTTPGetAction {
860-
port: IntOrString::String(APP_PORT_NAME.to_string()),
887+
port: IntOrString::String(probe_port_name.to_string()),
888+
scheme: probe_scheme.clone(),
861889
..HTTPGetAction::default()
862890
}),
863891
..Probe::default()
@@ -866,7 +894,8 @@ fn build_server_rolegroup_daemonset(
866894
initial_delay_seconds: Some(30),
867895
period_seconds: Some(10),
868896
http_get: Some(HTTPGetAction {
869-
port: IntOrString::String(APP_PORT_NAME.to_string()),
897+
port: IntOrString::String(probe_port_name.to_string()),
898+
scheme: probe_scheme,
870899
..HTTPGetAction::default()
871900
}),
872901
..Probe::default()
@@ -918,6 +947,22 @@ fn build_server_rolegroup_daemonset(
918947
.service_account_name(service_account.name_any())
919948
.security_context(PodSecurityContextBuilder::new().fs_group(1000).build());
920949

950+
if let Some(tls) = &opa.spec.cluster_config.tls {
951+
pb.add_volume(
952+
VolumeBuilder::new(TLS_VOLUME_NAME)
953+
.ephemeral(
954+
SecretOperatorVolumeSourceBuilder::new(&tls.server_secret_class)
955+
.with_service_scope(opa.server_role_service_name())
956+
.with_service_scope(rolegroup_ref.rolegroup_headless_service_name())
957+
.with_service_scope(rolegroup_ref.rolegroup_metrics_service_name())
958+
.build()
959+
.context(TlsVolumeBuildSnafu)?,
960+
)
961+
.build(),
962+
)
963+
.context(AddVolumeSnafu)?;
964+
}
965+
921966
if let Some(user_info) = &opa.spec.cluster_config.user_info {
922967
let mut cb_user_info_fetcher =
923968
ContainerBuilder::new("user-info-fetcher").context(IllegalContainerNameSnafu)?;
@@ -1146,7 +1191,11 @@ fn build_config_file(merged_config: &v1alpha1::OpaConfig) -> String {
11461191
serde_json::to_string_pretty(&json!(config)).unwrap()
11471192
}
11481193

1149-
fn build_opa_start_command(merged_config: &v1alpha1::OpaConfig, container_name: &str) -> String {
1194+
fn build_opa_start_command(
1195+
merged_config: &v1alpha1::OpaConfig,
1196+
container_name: &str,
1197+
tls_enabled: bool,
1198+
) -> String {
11501199
let mut file_log_level = DEFAULT_FILE_LOG_LEVEL;
11511200
let mut console_log_level = DEFAULT_CONSOLE_LOG_LEVEL;
11521201
let mut server_log_level = DEFAULT_SERVER_LOG_LEVEL;
@@ -1187,6 +1236,17 @@ fn build_opa_start_command(merged_config: &v1alpha1::OpaConfig, container_name:
11871236
}
11881237
}
11891238

1239+
let (bind_port, tls_flags) = if tls_enabled {
1240+
(
1241+
service::APP_TLS_PORT,
1242+
format!(
1243+
"--tls-cert-file {TLS_STORE_DIR}/tls.crt --tls-private-key-file {TLS_STORE_DIR}/tls.key"
1244+
),
1245+
)
1246+
} else {
1247+
(APP_PORT, String::new())
1248+
};
1249+
11901250
// Redirects matter!
11911251
// We need to watch out, that the following "$!" call returns the PID of the main (opa-bundle-builder) process,
11921252
// and not some utility (e.g. multilog or tee) process.
@@ -1202,7 +1262,7 @@ fn build_opa_start_command(merged_config: &v1alpha1::OpaConfig, container_name:
12021262
{remove_vector_shutdown_file_command}
12031263
prepare_signal_handlers
12041264
containerdebug --output={STACKABLE_LOG_DIR}/containerdebug-state.json --loop &
1205-
opa run -s -a 0.0.0.0:{APP_PORT} -c {CONFIG_DIR}/{CONFIG_FILE} -l {opa_log_level} --shutdown-grace-period {shutdown_grace_period_s} --disable-telemetry {logging_redirects} &
1265+
opa run -s -a 0.0.0.0:{bind_port} -c {CONFIG_DIR}/{CONFIG_FILE} -l {opa_log_level} --shutdown-grace-period {shutdown_grace_period_s} --disable-telemetry {tls_flags} {logging_redirects} &
12061266
wait_for_termination $!
12071267
{create_vector_shutdown_file_command}
12081268
",

rust/operator-binary/src/crd/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,18 @@ pub mod versioned {
115115
/// from an external directory service.
116116
#[serde(default)]
117117
pub user_info: Option<user_info_fetcher::v1alpha1::Config>,
118+
/// TLS encryption settings for the OPA server.
119+
/// When configured, OPA will use HTTPS (port 8443) instead of HTTP (port 8081).
120+
/// Clients must connect using HTTPS and trust the certificates provided by the configured SecretClass.
121+
#[serde(default)]
122+
pub tls: Option<v1alpha1::OpaTls>,
123+
}
124+
125+
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
126+
#[serde(rename_all = "camelCase")]
127+
pub struct OpaTls {
128+
/// Name of the SecretClass which will provide TLS certificates for the OPA server.
129+
pub server_secret_class: String,
118130
}
119131

120132
// TODO: Temporary solution until listener-operator is finished
@@ -239,6 +251,13 @@ impl v1alpha1::CurrentlySupportedListenerClasses {
239251
}
240252
}
241253

254+
impl v1alpha1::OpaClusterConfig {
255+
/// Returns whether TLS encryption is enabled for the OPA server.
256+
pub fn tls_enabled(&self) -> bool {
257+
self.tls.is_some()
258+
}
259+
}
260+
242261
impl v1alpha1::OpaConfig {
243262
fn default_config() -> v1alpha1::OpaConfigFragment {
244263
v1alpha1::OpaConfigFragment {

rust/operator-binary/src/discovery.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ use stackable_operator::{
88
utils::cluster_info::KubernetesClusterInfo,
99
};
1010

11-
use crate::{controller::build_recommended_labels, service::APP_PORT};
11+
use crate::{
12+
controller::build_recommended_labels,
13+
service::{APP_PORT, APP_TLS_PORT},
14+
};
1215

1316
#[derive(Snafu, Debug)]
1417
pub enum Error {
@@ -63,16 +66,21 @@ fn build_discovery_configmap(
6366
svc: &Service,
6467
cluster_info: &KubernetesClusterInfo,
6568
) -> Result<ConfigMap, Error> {
69+
let (scheme, port) = if opa.spec.cluster_config.tls_enabled() {
70+
("https", APP_TLS_PORT)
71+
} else {
72+
("http", APP_PORT)
73+
};
74+
6675
let url = format!(
67-
"http://{name}.{namespace}.svc.{cluster_domain}:{port}/",
76+
"{scheme}://{name}.{namespace}.svc.{cluster_domain}:{port}/",
6877
name = svc.metadata.name.as_deref().context(NoNameSnafu)?,
6978
namespace = svc
7079
.metadata
7180
.namespace
7281
.as_deref()
7382
.context(NoNamespaceSnafu)?,
7483
cluster_domain = cluster_info.cluster_domain,
75-
port = APP_PORT
7684
);
7785

7886
let metadata = ObjectMetaBuilder::new()
@@ -91,9 +99,13 @@ fn build_discovery_configmap(
9199
.context(ObjectMetaSnafu)?
92100
.build();
93101

94-
ConfigMapBuilder::new()
95-
.metadata(metadata)
96-
.add_data("OPA", url)
97-
.build()
98-
.context(BuildConfigMapSnafu)
102+
let mut cm_builder = ConfigMapBuilder::new();
103+
104+
cm_builder.metadata(metadata).add_data("OPA", url);
105+
106+
if let Some(tls) = opa.spec.cluster_config.tls.as_ref() {
107+
cm_builder.add_data("OPA_SECRET_CLASS", &tls.server_secret_class);
108+
}
109+
110+
cm_builder.build().context(BuildConfigMapSnafu)
99111
}

0 commit comments

Comments
 (0)