Skip to content

Commit dc5cd75

Browse files
committed
feat: tls support
1 parent 86e38df commit dc5cd75

File tree

6 files changed

+210
-23
lines changed

6 files changed

+210
-23
lines changed

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

Lines changed: 11 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:
@@ -47,6 +48,16 @@ spec:
4748
- external-unstable
4849
- external-stable
4950
type: string
51+
tls:
52+
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.
53+
nullable: true
54+
properties:
55+
serverSecretClass:
56+
description: Name of the SecretClass which will provide TLS certificates for the OPA server.
57+
type: string
58+
required:
59+
- serverSecretClass
60+
type: object
5061
userInfo:
5162
description: Configures how to fetch additional metadata about users (such as group memberships) from an external directory service.
5263
nullable: true
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 connections between clients and the OPA server can be configured in the `OpaCluster` resource. When TLS is enabled, OPA will serve 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` specification
12+
13+
The operator integrates with the xref:secret-operator:index.adoc[Secret Operator] to automatically provision and mount TLS certificates to 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
@@ -9,6 +9,7 @@
99
** xref:opa:usage-guide/logging.adoc[]
1010
** xref:opa:usage-guide/monitoring.adoc[]
1111
** xref:opa:usage-guide/configuration-environment-overrides.adoc[]
12+
** xref:opa:usage-guide/tls.adoc[]
1213
** xref:opa:usage-guide/operations/index.adoc[]
1314
*** xref:opa:usage-guide/operations/cluster-operations.adoc[]
1415
// *** xref:hdfs:usage-guide/operations/pod-placement.adoc[] Missing

rust/operator-binary/src/controller.rs

Lines changed: 97 additions & 15 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},
@@ -84,7 +84,9 @@ pub const OPA_FULL_CONTROLLER_NAME: &str = concatcp!(OPA_CONTROLLER_NAME, '.', O
8484

8585
pub const CONFIG_FILE: &str = "config.json";
8686
pub const APP_PORT: u16 = 8081;
87+
pub const APP_TLS_PORT: u16 = 8443;
8788
pub const APP_PORT_NAME: &str = "http";
89+
pub const APP_TLS_PORT_NAME: &str = "https";
8890
pub const METRICS_PORT_NAME: &str = "metrics";
8991
pub const BUNDLES_ACTIVE_DIR: &str = "/bundles/active";
9092
pub const BUNDLES_INCOMING_DIR: &str = "/bundles/incoming";
@@ -101,6 +103,8 @@ const USER_INFO_FETCHER_CREDENTIALS_VOLUME_NAME: &str = "credentials";
101103
const USER_INFO_FETCHER_CREDENTIALS_DIR: &str = "/stackable/credentials";
102104
const USER_INFO_FETCHER_KERBEROS_VOLUME_NAME: &str = "kerberos";
103105
const USER_INFO_FETCHER_KERBEROS_DIR: &str = "/stackable/kerberos";
106+
const TLS_VOLUME_NAME: &str = "tls";
107+
const TLS_STORE_DIR: &str = "/stackable/tls";
104108

105109
const DOCKER_IMAGE_BASE_NAME: &str = "opa";
106110

@@ -314,6 +318,11 @@ pub enum Error {
314318
AddVolumeMount {
315319
source: builder::pod::container::Error,
316320
},
321+
322+
#[snafu(display("failed to build TLS volume"))]
323+
TlsVolumeBuild {
324+
source: builder::pod::volume::SecretOperatorVolumeSourceBuilderError,
325+
},
317326
}
318327
type Result<T, E = Error> = std::result::Result<T, E>;
319328

@@ -600,11 +609,17 @@ pub fn build_server_role_service(
600609
let service_selector_labels =
601610
Labels::role_selector(opa, APP_NAME, &role_name).context(BuildLabelSnafu)?;
602611

612+
let (port_name, port) = if opa.spec.cluster_config.tls.is_some() {
613+
(APP_TLS_PORT_NAME, APP_TLS_PORT)
614+
} else {
615+
(APP_PORT_NAME, APP_PORT)
616+
};
617+
603618
let service_spec = ServiceSpec {
604619
type_: Some(opa.spec.cluster_config.listener_class.k8s_service_type()),
605620
ports: Some(vec![ServicePort {
606-
name: Some(APP_PORT_NAME.to_string()),
607-
port: APP_PORT.into(),
621+
name: Some(port_name.to_string()),
622+
port: port.into(),
608623
protocol: Some("TCP".to_string()),
609624
..ServicePort::default()
610625
}]),
@@ -654,7 +669,7 @@ fn build_rolegroup_service(
654669
// Internal communication does not need to be exposed
655670
type_: Some("ClusterIP".to_string()),
656671
cluster_ip: Some("None".to_string()),
657-
ports: Some(service_ports()),
672+
ports: Some(service_ports(opa.spec.cluster_config.tls.is_some())),
658673
selector: Some(service_selector_labels.into()),
659674
publish_not_ready_addresses: Some(true),
660675
..ServiceSpec::default()
@@ -839,6 +854,8 @@ fn build_server_rolegroup_daemonset(
839854
..Probe::default()
840855
});
841856

857+
let opa_tls_config = opa.spec.cluster_config.tls.as_ref();
858+
842859
cb_opa
843860
.image_from_product_image(resolved_product_image)
844861
.command(vec![
@@ -851,24 +868,45 @@ fn build_server_rolegroup_daemonset(
851868
.args(vec![build_opa_start_command(
852869
merged_config,
853870
&opa_container_name,
871+
opa_tls_config,
854872
)])
855873
.add_env_vars(env)
856874
.add_env_var(
857875
"CONTAINERDEBUG_LOG_DIRECTORY",
858876
format!("{STACKABLE_LOG_DIR}/containerdebug"),
859-
)
860-
.add_container_port(APP_PORT_NAME, APP_PORT.into())
877+
);
878+
879+
// Add appropriate container port based on TLS configuration
880+
if opa_tls_config.is_some() {
881+
cb_opa.add_container_port(APP_TLS_PORT_NAME, APP_TLS_PORT.into());
882+
cb_opa
883+
.add_volume_mount(TLS_VOLUME_NAME, TLS_STORE_DIR)
884+
.context(AddVolumeMountSnafu)?;
885+
} else {
886+
cb_opa.add_container_port(APP_PORT_NAME, APP_PORT.into());
887+
}
888+
889+
cb_opa
861890
.add_volume_mount(CONFIG_VOLUME_NAME, CONFIG_DIR)
862891
.context(AddVolumeMountSnafu)?
863892
.add_volume_mount(LOG_VOLUME_NAME, STACKABLE_LOG_DIR)
864893
.context(AddVolumeMountSnafu)?
865-
.resources(merged_config.resources.to_owned().into())
894+
.resources(merged_config.resources.to_owned().into());
895+
896+
let (probe_port_name, probe_scheme) = if opa_tls_config.is_some() {
897+
(APP_TLS_PORT_NAME, Some("HTTPS".to_string()))
898+
} else {
899+
(APP_PORT_NAME, Some("HTTP".to_string()))
900+
};
901+
902+
cb_opa
866903
.readiness_probe(Probe {
867904
initial_delay_seconds: Some(5),
868905
period_seconds: Some(10),
869906
failure_threshold: Some(5),
870907
http_get: Some(HTTPGetAction {
871-
port: IntOrString::String(APP_PORT_NAME.to_string()),
908+
port: IntOrString::String(probe_port_name.to_string()),
909+
scheme: probe_scheme.clone(),
872910
..HTTPGetAction::default()
873911
}),
874912
..Probe::default()
@@ -877,7 +915,8 @@ fn build_server_rolegroup_daemonset(
877915
initial_delay_seconds: Some(30),
878916
period_seconds: Some(10),
879917
http_get: Some(HTTPGetAction {
880-
port: IntOrString::String(APP_PORT_NAME.to_string()),
918+
port: IntOrString::String(probe_port_name.to_string()),
919+
scheme: probe_scheme,
881920
..HTTPGetAction::default()
882921
}),
883922
..Probe::default()
@@ -929,6 +968,20 @@ fn build_server_rolegroup_daemonset(
929968
.service_account_name(service_account.name_any())
930969
.security_context(PodSecurityContextBuilder::new().fs_group(1000).build());
931970

971+
if let Some(tls) = opa_tls_config {
972+
pb.add_volume(
973+
VolumeBuilder::new(TLS_VOLUME_NAME)
974+
.ephemeral(
975+
SecretOperatorVolumeSourceBuilder::new(&tls.server_secret_class)
976+
.with_service_scope(opa_name)
977+
.build()
978+
.context(TlsVolumeBuildSnafu)?,
979+
)
980+
.build(),
981+
)
982+
.context(AddVolumeSnafu)?;
983+
}
984+
932985
if let Some(user_info) = &opa.spec.cluster_config.user_info {
933986
let mut cb_user_info_fetcher =
934987
ContainerBuilder::new("user-info-fetcher").context(IllegalContainerNameSnafu)?;
@@ -1150,7 +1203,11 @@ fn build_config_file(merged_config: &v1alpha1::OpaConfig) -> String {
11501203
serde_json::to_string_pretty(&json!(config)).unwrap()
11511204
}
11521205

1153-
fn build_opa_start_command(merged_config: &v1alpha1::OpaConfig, container_name: &str) -> String {
1206+
fn build_opa_start_command(
1207+
merged_config: &v1alpha1::OpaConfig,
1208+
container_name: &str,
1209+
tls_config: Option<&v1alpha1::OpaTls>,
1210+
) -> String {
11541211
let mut file_log_level = DEFAULT_FILE_LOG_LEVEL;
11551212
let mut console_log_level = DEFAULT_CONSOLE_LOG_LEVEL;
11561213
let mut server_log_level = DEFAULT_SERVER_LOG_LEVEL;
@@ -1191,6 +1248,17 @@ fn build_opa_start_command(merged_config: &v1alpha1::OpaConfig, container_name:
11911248
}
11921249
}
11931250

1251+
let (bind_port, tls_flags) = if tls_config.is_some() {
1252+
(
1253+
APP_TLS_PORT,
1254+
format!(
1255+
"--tls-cert-file {TLS_STORE_DIR}/tls.crt --tls-private-key-file {TLS_STORE_DIR}/tls.key"
1256+
),
1257+
)
1258+
} else {
1259+
(APP_PORT, String::new())
1260+
};
1261+
11941262
// Redirects matter!
11951263
// We need to watch out, that the following "$!" call returns the PID of the main (opa-bundle-builder) process,
11961264
// and not some utility (e.g. multilog or tee) process.
@@ -1206,7 +1274,7 @@ fn build_opa_start_command(merged_config: &v1alpha1::OpaConfig, container_name:
12061274
{remove_vector_shutdown_file_command}
12071275
prepare_signal_handlers
12081276
containerdebug --output={STACKABLE_LOG_DIR}/containerdebug-state.json --loop &
1209-
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} &
1277+
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} &
12101278
wait_for_termination $!
12111279
{create_vector_shutdown_file_command}
12121280
",
@@ -1305,19 +1373,33 @@ fn build_prepare_start_command(
13051373
prepare_container_args
13061374
}
13071375

1308-
fn service_ports() -> Vec<ServicePort> {
1376+
fn service_ports(tls_enabled: bool) -> Vec<ServicePort> {
1377+
let (port_name, port, target_port) = if tls_enabled {
1378+
(
1379+
APP_TLS_PORT_NAME,
1380+
APP_TLS_PORT,
1381+
IntOrString::String(APP_TLS_PORT_NAME.to_string()),
1382+
)
1383+
} else {
1384+
(
1385+
APP_PORT_NAME,
1386+
APP_PORT,
1387+
IntOrString::String(APP_PORT_NAME.to_string()),
1388+
)
1389+
};
1390+
13091391
vec![
13101392
ServicePort {
1311-
name: Some(APP_PORT_NAME.to_string()),
1312-
port: APP_PORT.into(),
1393+
name: Some(port_name.to_string()),
1394+
port: port.into(),
13131395
protocol: Some("TCP".to_string()),
13141396
..ServicePort::default()
13151397
},
13161398
ServicePort {
13171399
name: Some(METRICS_PORT_NAME.to_string()),
13181400
port: 9504, // Arbitrary port number, this is never actually used anywhere
13191401
protocol: Some("TCP".to_string()),
1320-
target_port: Some(IntOrString::String(APP_PORT_NAME.to_string())),
1402+
target_port: Some(target_port),
13211403
..ServicePort::default()
13221404
},
13231405
]

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

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

116128
// TODO: Temporary solution until listener-operator is finished

0 commit comments

Comments
 (0)