Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.
- Helm: Allow Pod `priorityClassName` to be configured ([#762]).
- Add support for OPA `1.8.0` ([#765]).
- Add `prometheus.io/path|port|scheme` annotations to metrics service ([#767]).
- Add support for TLS ([#774])

### Changed

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

## [25.7.0] - 2025-07-23

Expand Down
14 changes: 14 additions & 0 deletions deploy/helm/opa-operator/crds/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ spec:
clusterConfig:
default:
listenerClass: cluster-internal
tls: null
userInfo: null
description: Global OPA cluster configuration that applies to all roles and role groups.
properties:
Expand All @@ -49,6 +50,19 @@ spec:
- external-unstable
- external-stable
type: string
tls:
description: |-
TLS encryption settings for the OPA server.
When configured, OPA will use HTTPS (port 8443) instead of HTTP (port 8081).
Clients must connect using HTTPS and trust the certificates provided by the configured SecretClass.
nullable: true
properties:
serverSecretClass:
description: Name of the SecretClass which will provide TLS certificates for the OPA server.
type: string
required:
- serverSecretClass
type: object
userInfo:
description: |-
Configures how to fetch additional metadata about users (such as group memberships)
Expand Down
71 changes: 71 additions & 0 deletions docs/modules/opa/pages/usage-guide/tls.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
= Enabling TLS Encryption
:description: Learn how to enable TLS encryption for your OPA cluster to secure client connections.

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.

== Overview

TLS encryption in OPA is disabled by default. To enable it, you need to:

1. Create a `SecretClass` that provides TLS certificates
2. Reference the `SecretClass` in your `OpaCluster` custom resource

The operator integrates with the xref:secret-operator:index.adoc[Secret Operator] to automatically provision and mount TLS certificates into the OPA pods.

== Configuration

=== Creating a SecretClass

First, create a `SecretClass` that will provide TLS certificates. Here's an example using xref:secret-operator:secretclass.adoc#backend-autotls[autoTls]:

[source,yaml]
----
apiVersion: secrets.stackable.tech/v1alpha1
kind: SecretClass
metadata:
name: opa-tls
spec:
backend:
autoTls:
ca:
autoGenerate: true
secret:
name: opa-tls-ca
namespace: default
----

This SecretClass uses the autoTls backend, which automatically generates a Certificate Authority (CA) and signs certificates for your OPA cluster.

Similarly, you can also use xref:secret-operator:secretclass.adoc#backend[other backends] supported by Secret Operator.

=== Enabling TLS in OpaCluster

Once you have a SecretClass, enable TLS in your OpaCluster by setting the `.spec.clusterConfig.tls.serverSecretClass` field:

[source,yaml]
----
kind: OpaCluster
name: opa-with-tls
spec:
clusterConfig:
tls:
serverSecretClass: opa-tls # <1>
----
<1> Reference the SecretClass created above

== Discovery ConfigMap

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:

[source,yaml]
----
apiVersion: v1
kind: ConfigMap
metadata:
name: opa-with-tls
data:
OPA: "https://opa-with-tls.default.svc.cluster.local:8443/"
OPA_SECRET_CLASS: "opa-tls"
----

Applications can use this ConfigMap to discover and connect to the OPA cluster automatically.
1 change: 1 addition & 0 deletions docs/modules/opa/partials/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
** xref:opa:usage-guide/monitoring.adoc[]
** xref:opa:usage-guide/OpenTelemetry.adoc[]
** xref:opa:usage-guide/configuration-environment-overrides.adoc[]
** xref:opa:usage-guide/tls.adoc[]
** xref:opa:usage-guide/operations/index.adoc[]
*** xref:opa:usage-guide/operations/cluster-operations.adoc[]
// *** xref:hdfs:usage-guide/operations/pod-placement.adoc[] Missing
Expand Down
86 changes: 73 additions & 13 deletions rust/operator-binary/src/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use stackable_operator::{
container::{ContainerBuilder, FieldPathEnvVar},
resources::ResourceRequirementsBuilder,
security::PodSecurityContextBuilder,
volume::VolumeBuilder,
volume::{SecretOperatorVolumeSourceBuilder, VolumeBuilder},
},
},
cluster_resources::{ClusterResourceApplyStrategy, ClusterResources},
Expand Down Expand Up @@ -104,6 +104,8 @@ const USER_INFO_FETCHER_CREDENTIALS_VOLUME_NAME: &str = "credentials";
const USER_INFO_FETCHER_CREDENTIALS_DIR: &str = "/stackable/credentials";
const USER_INFO_FETCHER_KERBEROS_VOLUME_NAME: &str = "kerberos";
const USER_INFO_FETCHER_KERBEROS_DIR: &str = "/stackable/kerberos";
const TLS_VOLUME_NAME: &str = "tls";
const TLS_STORE_DIR: &str = "/stackable/tls";

const DOCKER_IMAGE_BASE_NAME: &str = "opa";

Expand Down Expand Up @@ -329,6 +331,11 @@ pub enum Error {

#[snafu(display("failed to build service"))]
BuildService { source: service::Error },

#[snafu(display("failed to build TLS volume"))]
TlsVolumeBuild {
source: builder::pod::volume::SecretOperatorVolumeSourceBuilderError,
},
}
type Result<T, E = Error> = std::result::Result<T, E>;

Expand Down Expand Up @@ -835,29 +842,50 @@ fn build_server_rolegroup_daemonset(
.args(vec![build_opa_start_command(
merged_config,
&opa_container_name,
opa.spec.cluster_config.tls_enabled(),
)])
.add_env_vars(env)
.add_env_var(
"CONTAINERDEBUG_LOG_DIRECTORY",
format!("{STACKABLE_LOG_DIR}/containerdebug"),
)
.add_container_port(APP_PORT_NAME, APP_PORT.into())
// If we also add a container port "metrics" pointing to the same port number, we get a
//
// .spec.template.spec.containers[name="opa"].ports: duplicate entries for key [containerPort=8081,protocol="TCP"]
//
// So we don't do that
);

// Add appropriate container port based on TLS configuration
// If we also add a container port "metrics" pointing to the same port number, we get a
//
// .spec.template.spec.containers[name="opa"].ports: duplicate entries for key [containerPort=8081,protocol="TCP"]
//
// So we don't do that
if opa.spec.cluster_config.tls_enabled() {
cb_opa.add_container_port(service::APP_TLS_PORT_NAME, service::APP_TLS_PORT.into());
cb_opa
.add_volume_mount(TLS_VOLUME_NAME, TLS_STORE_DIR)
.context(AddVolumeMountSnafu)?;
} else {
cb_opa.add_container_port(APP_PORT_NAME, APP_PORT.into());
}

cb_opa
.add_volume_mount(CONFIG_VOLUME_NAME, CONFIG_DIR)
.context(AddVolumeMountSnafu)?
.add_volume_mount(LOG_VOLUME_NAME, STACKABLE_LOG_DIR)
.context(AddVolumeMountSnafu)?
.resources(merged_config.resources.to_owned().into())
.resources(merged_config.resources.to_owned().into());

let (probe_port_name, probe_scheme) = if opa.spec.cluster_config.tls_enabled() {
(service::APP_TLS_PORT_NAME, Some("HTTPS".to_string()))
} else {
(APP_PORT_NAME, Some("HTTP".to_string()))
};

cb_opa
.readiness_probe(Probe {
initial_delay_seconds: Some(5),
period_seconds: Some(10),
failure_threshold: Some(5),
http_get: Some(HTTPGetAction {
port: IntOrString::String(APP_PORT_NAME.to_string()),
port: IntOrString::String(probe_port_name.to_string()),
scheme: probe_scheme.clone(),
..HTTPGetAction::default()
}),
..Probe::default()
Expand All @@ -866,7 +894,8 @@ fn build_server_rolegroup_daemonset(
initial_delay_seconds: Some(30),
period_seconds: Some(10),
http_get: Some(HTTPGetAction {
port: IntOrString::String(APP_PORT_NAME.to_string()),
port: IntOrString::String(probe_port_name.to_string()),
scheme: probe_scheme,
..HTTPGetAction::default()
}),
..Probe::default()
Expand Down Expand Up @@ -918,6 +947,22 @@ fn build_server_rolegroup_daemonset(
.service_account_name(service_account.name_any())
.security_context(PodSecurityContextBuilder::new().fs_group(1000).build());

if let Some(tls) = &opa.spec.cluster_config.tls {
pb.add_volume(
VolumeBuilder::new(TLS_VOLUME_NAME)
.ephemeral(
SecretOperatorVolumeSourceBuilder::new(&tls.server_secret_class)
.with_service_scope(opa.server_role_service_name())
.with_service_scope(rolegroup_ref.rolegroup_headless_service_name())
.with_service_scope(rolegroup_ref.rolegroup_metrics_service_name())
.build()
.context(TlsVolumeBuildSnafu)?,
)
.build(),
)
.context(AddVolumeSnafu)?;
}

if let Some(user_info) = &opa.spec.cluster_config.user_info {
let mut cb_user_info_fetcher =
ContainerBuilder::new("user-info-fetcher").context(IllegalContainerNameSnafu)?;
Expand Down Expand Up @@ -1146,7 +1191,11 @@ fn build_config_file(merged_config: &v1alpha1::OpaConfig) -> String {
serde_json::to_string_pretty(&json!(config)).unwrap()
}

fn build_opa_start_command(merged_config: &v1alpha1::OpaConfig, container_name: &str) -> String {
fn build_opa_start_command(
merged_config: &v1alpha1::OpaConfig,
container_name: &str,
tls_enabled: bool,
) -> String {
let mut file_log_level = DEFAULT_FILE_LOG_LEVEL;
let mut console_log_level = DEFAULT_CONSOLE_LOG_LEVEL;
let mut server_log_level = DEFAULT_SERVER_LOG_LEVEL;
Expand Down Expand Up @@ -1187,6 +1236,17 @@ fn build_opa_start_command(merged_config: &v1alpha1::OpaConfig, container_name:
}
}

let (bind_port, tls_flags) = if tls_enabled {
(
service::APP_TLS_PORT,
format!(
"--tls-cert-file {TLS_STORE_DIR}/tls.crt --tls-private-key-file {TLS_STORE_DIR}/tls.key"
),
)
} else {
(APP_PORT, String::new())
};

// Redirects matter!
// We need to watch out, that the following "$!" call returns the PID of the main (opa-bundle-builder) process,
// and not some utility (e.g. multilog or tee) process.
Expand All @@ -1202,7 +1262,7 @@ fn build_opa_start_command(merged_config: &v1alpha1::OpaConfig, container_name:
{remove_vector_shutdown_file_command}
prepare_signal_handlers
containerdebug --output={STACKABLE_LOG_DIR}/containerdebug-state.json --loop &
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} &
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} &
wait_for_termination $!
{create_vector_shutdown_file_command}
",
Expand Down
19 changes: 19 additions & 0 deletions rust/operator-binary/src/crd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ pub mod versioned {
/// from an external directory service.
#[serde(default)]
pub user_info: Option<user_info_fetcher::v1alpha1::Config>,
/// TLS encryption settings for the OPA server.
/// When configured, OPA will use HTTPS (port 8443) instead of HTTP (port 8081).
/// Clients must connect using HTTPS and trust the certificates provided by the configured SecretClass.
#[serde(default)]
pub tls: Option<v1alpha1::OpaTls>,
}

#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpaTls {
/// Name of the SecretClass which will provide TLS certificates for the OPA server.
pub server_secret_class: String,
}

// TODO: Temporary solution until listener-operator is finished
Expand Down Expand Up @@ -239,6 +251,13 @@ impl v1alpha1::CurrentlySupportedListenerClasses {
}
}

impl v1alpha1::OpaClusterConfig {
/// Returns whether TLS encryption is enabled for the OPA server.
pub fn tls_enabled(&self) -> bool {
self.tls.is_some()
}
}

impl v1alpha1::OpaConfig {
fn default_config() -> v1alpha1::OpaConfigFragment {
v1alpha1::OpaConfigFragment {
Expand Down
28 changes: 20 additions & 8 deletions rust/operator-binary/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ use stackable_operator::{
utils::cluster_info::KubernetesClusterInfo,
};

use crate::{controller::build_recommended_labels, service::APP_PORT};
use crate::{
controller::build_recommended_labels,
service::{APP_PORT, APP_TLS_PORT},
};

#[derive(Snafu, Debug)]
pub enum Error {
Expand Down Expand Up @@ -63,16 +66,21 @@ fn build_discovery_configmap(
svc: &Service,
cluster_info: &KubernetesClusterInfo,
) -> Result<ConfigMap, Error> {
let (scheme, port) = if opa.spec.cluster_config.tls_enabled() {
("https", APP_TLS_PORT)
} else {
("http", APP_PORT)
};

let url = format!(
"http://{name}.{namespace}.svc.{cluster_domain}:{port}/",
"{scheme}://{name}.{namespace}.svc.{cluster_domain}:{port}/",
name = svc.metadata.name.as_deref().context(NoNameSnafu)?,
namespace = svc
.metadata
.namespace
.as_deref()
.context(NoNamespaceSnafu)?,
cluster_domain = cluster_info.cluster_domain,
port = APP_PORT
);

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

ConfigMapBuilder::new()
.metadata(metadata)
.add_data("OPA", url)
.build()
.context(BuildConfigMapSnafu)
let mut cm_builder = ConfigMapBuilder::new();

cm_builder.metadata(metadata).add_data("OPA", url);

if let Some(tls) = opa.spec.cluster_config.tls.as_ref() {
cm_builder.add_data("OPA_SECRET_CLASS", &tls.server_secret_class);
}

cm_builder.build().context(BuildConfigMapSnafu)
}
Loading