Skip to content

Commit f466bac

Browse files
Malewareadwk67razvan
committed
Fix/custom s3 certificates (#247)
# Description This PR is supposed to enable TLS for spark-k8s for S3 buckets. Fixes #194 TODO: add s3 certificate for history server access. Co-authored-by: Andrew Kenworthy <[email protected]> Co-authored-by: Razvan-Daniel Mihai <[email protected]>
1 parent 45e0cdc commit f466bac

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1186
-273
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file.
88

99
- Generate OLM bundle for Release 23.4.0 ([#238]).
1010
- Add support for Spark 3.4.0 ([#243]).
11+
- Add support for using custom certificates when accessing S3 with TLS ([#247]).
12+
- Use bitnami charts for testing S3 access with TLS ([#247]).
1113

1214
### Changed
1315

@@ -25,6 +27,7 @@ All notable changes to this project will be documented in this file.
2527
[#238]: https://github.com/stackabletech/spark-k8s-operator/pull/238
2628
[#241]: https://github.com/stackabletech/spark-k8s-operator/pull/241
2729
[#243]: https://github.com/stackabletech/spark-k8s-operator/pull/243
30+
[#247]: https://github.com/stackabletech/spark-k8s-operator/pull/247
2831

2932
## [23.4.0] - 2023-04-17
3033

docs/modules/spark-k8s/pages/usage-guide/s3.adoc

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
You can specify S3 connection details directly inside the `SparkApplication` specification or by referring to an external `S3Bucket` custom resource.
44

5+
== S3 access using credentials
6+
57
To specify S3 connection details directly as part of the `SparkApplication` resource you add an inline connection configuration as shown below.
68

79
[source,yaml]
@@ -17,7 +19,7 @@ s3connection: # <1>
1719
<1> Entry point for the S3 connection configuration.
1820
<2> Connection host.
1921
<3> Optional connection port.
20-
<4> Name of the `Secret` object expected to contain the following keys: `ACCESS_KEY_ID` and `SECRET_ACCESS_KEY`
22+
<4> Name of the `Secret` object expected to contain the following keys: `accessKey` and `secretKey`
2123

2224
It is also possible to configure the connection details as a separate Kubernetes resource and only refer to that object from the `SparkApplication` like this:
2325

@@ -46,3 +48,44 @@ spec:
4648
----
4749

4850
This has the advantage that one connection configuration can be shared across `SparkApplications` and reduces the cost of updating these details.
51+
52+
== S3 access with TLS
53+
54+
A custom certificate can also be used for S3 access. In the example below, a Secret containing a custom certificate is referenced, which will used a to create a custom truststore which is used by Spark for S3-bucket access:
55+
56+
[source,yaml]
57+
----
58+
---
59+
apiVersion: s3.stackable.tech/v1alpha1
60+
kind: S3Connection
61+
metadata:
62+
name: s3-connection-resource
63+
spec:
64+
host: test-minio
65+
port: 9000
66+
accessStyle: Path
67+
credentials:
68+
secretClass: minio-credentials-class # <1>
69+
tls:
70+
verification:
71+
server:
72+
caCert:
73+
secretClass: minio-tls-certificates # <2>
74+
----
75+
<1> Name of the `Secret` object expected to contain the following keys: `accessKey` and `secretKey` (as in the previous example).
76+
<2> Name of the `Secret` object containing the custom certificate. The certificate should comprise the 3 files named as shown below:
77+
78+
[source,yaml]
79+
----
80+
---
81+
apiVersion: v1
82+
kind: Secret
83+
metadata:
84+
name: minio-tls-certificates
85+
labels:
86+
secrets.stackable.tech/class: minio-tls-certificates
87+
data:
88+
ca.crt: ...
89+
tls.crt: ...
90+
tls.key: ...
91+
----

rust/crd/src/constants.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ pub const LOG4J2_CONFIG_FILE: &str = "log4j2.properties";
2828
pub const ACCESS_KEY_ID: &str = "accessKey";
2929
pub const SECRET_ACCESS_KEY: &str = "secretKey";
3030
pub const S3_SECRET_DIR_NAME: &str = "/stackable/secrets";
31+
pub const SYSTEM_TRUST_STORE: &str = "/etc/pki/java/cacerts";
32+
pub const STACKABLE_TRUST_STORE: &str = "/stackable/truststore";
33+
pub const STACKABLE_TRUST_STORE_NAME: &str = "stackable-truststore";
34+
pub const STACKABLE_TLS_STORE_PASSWORD: &str = "changeit";
35+
pub const SYSTEM_TRUST_STORE_PASSWORD: &str = "changeit";
36+
pub const STACKABLE_MOUNT_PATH_TLS: &str = "/stackable/mount_server_tls";
37+
pub const STACKABLE_MOUNT_NAME_TLS: &str = "servertls";
3138

3239
pub const MIN_MEMORY_OVERHEAD: u32 = 384;
3340
pub const JVM_OVERHEAD_FACTOR: f32 = 0.1;

rust/crd/src/lib.rs

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod affinity;
44
pub mod constants;
55
pub mod history;
66
pub mod s3logdir;
7+
pub mod tlscerts;
78

89
use std::{
910
cmp::max,
@@ -18,7 +19,7 @@ use s3logdir::S3LogDir;
1819
use serde::{Deserialize, Serialize};
1920
use snafu::{OptionExt, ResultExt, Snafu};
2021
use stackable_operator::{
21-
builder::VolumeBuilder,
22+
builder::{SecretOperatorVolumeSourceBuilder, VolumeBuilder},
2223
commons::{
2324
affinity::{StackableAffinity, StackableAffinityFragment},
2425
resources::{
@@ -332,6 +333,21 @@ impl SparkApplication {
332333
.build(),
333334
);
334335

336+
if let Some(cert_secrets) = tlscerts::tls_secret_names(s3conn, s3logdir) {
337+
result.push(
338+
VolumeBuilder::new(STACKABLE_TRUST_STORE_NAME)
339+
.with_empty_dir(None::<String>, Some(Quantity("5Mi".to_string())))
340+
.build(),
341+
);
342+
for cert_secret in cert_secrets {
343+
result.push(
344+
VolumeBuilder::new(cert_secret)
345+
.ephemeral(SecretOperatorVolumeSourceBuilder::new(cert_secret).build())
346+
.build(),
347+
);
348+
}
349+
}
350+
335351
result
336352
}
337353

@@ -427,6 +443,22 @@ impl SparkApplication {
427443
..VolumeMount::default()
428444
});
429445

446+
if let Some(cert_secrets) = tlscerts::tls_secret_names(s3conn, s3logdir) {
447+
mounts.push(VolumeMount {
448+
name: STACKABLE_TRUST_STORE_NAME.into(),
449+
mount_path: STACKABLE_TRUST_STORE.into(),
450+
..VolumeMount::default()
451+
});
452+
for cert_secret in cert_secrets {
453+
let secret_dir = format!("{STACKABLE_MOUNT_PATH_TLS}/{cert_secret}");
454+
mounts.push(VolumeMount {
455+
name: cert_secret.to_string(),
456+
mount_path: secret_dir,
457+
..VolumeMount::default()
458+
});
459+
}
460+
}
461+
430462
mounts
431463
}
432464

@@ -508,6 +540,12 @@ impl SparkApplication {
508540
}
509541
}
510542

543+
// s3 with TLS
544+
if tlscerts::tls_secret_names(s3conn, s3_log_dir).is_some() {
545+
submit_cmd.push(format!("--conf spark.driver.extraJavaOptions=\"-Djavax.net.ssl.trustStore={STACKABLE_TRUST_STORE}/truststore.p12 -Djavax.net.ssl.trustStorePassword={STACKABLE_TLS_STORE_PASSWORD} -Djavax.net.ssl.trustStoreType=pkcs12 -Djavax.net.debug=ssl,handshake\""));
546+
submit_cmd.push(format!("--conf spark.executor.extraJavaOptions=\"-Djavax.net.ssl.trustStore={STACKABLE_TRUST_STORE}/truststore.p12 -Djavax.net.ssl.trustStorePassword={STACKABLE_TLS_STORE_PASSWORD} -Djavax.net.ssl.trustStoreType=pkcs12 -Djavax.net.debug=ssl,handshake\""));
547+
}
548+
511549
// repositories and packages arguments
512550
if let Some(deps) = self.spec.deps.clone() {
513551
submit_cmd.extend(
@@ -675,7 +713,11 @@ impl SparkApplication {
675713
Ok(format!("{}m", original_memory - deduction))
676714
}
677715

678-
pub fn env(&self) -> Vec<EnvVar> {
716+
pub fn env(
717+
&self,
718+
s3conn: &Option<S3ConnectionSpec>,
719+
s3logdir: &Option<S3LogDir>,
720+
) -> Vec<EnvVar> {
679721
let tmp = self.spec.env.as_ref();
680722
let mut e: Vec<EnvVar> = tmp.iter().flat_map(|e| e.iter()).cloned().collect();
681723
if self.requirements().is_some() {
@@ -687,6 +729,25 @@ impl SparkApplication {
687729
value_from: None,
688730
});
689731
}
732+
if tlscerts::tls_secret_names(s3conn, s3logdir).is_some() {
733+
e.push(EnvVar {
734+
name: "STACKABLE_TLS_STORE_PASSWORD".to_string(),
735+
value: Some(STACKABLE_TLS_STORE_PASSWORD.to_string()),
736+
value_from: None,
737+
});
738+
}
739+
if let Some(s3logdir) = s3logdir {
740+
if tlscerts::tls_secret_name(&s3logdir.bucket.connection).is_some() {
741+
e.push(EnvVar {
742+
name: "SPARK_DAEMON_JAVA_OPTS".to_string(),
743+
value: Some(format!(
744+
"-Djavax.net.ssl.trustStore={STACKABLE_TRUST_STORE}/truststore.p12 -Djavax.net.ssl.trustStorePassword={STACKABLE_TLS_STORE_PASSWORD} -Djavax.net.ssl.trustStoreType=pkcs12"
745+
)),
746+
value_from: None,
747+
});
748+
}
749+
}
750+
690751
e
691752
}
692753

@@ -811,6 +872,7 @@ pub enum SparkContainer {
811872
Requirements,
812873
Spark,
813874
Vector,
875+
Tls,
814876
}
815877

816878
#[derive(Clone, Debug, Default, Fragment, JsonSchema, PartialEq)]

rust/crd/src/s3logdir.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ use crate::{
44
LogFileDirectorySpec::{self, S3},
55
S3LogFileDirectorySpec,
66
},
7+
tlscerts,
78
};
89
use stackable_operator::{
10+
builder::{SecretOperatorVolumeSourceBuilder, VolumeBuilder},
911
commons::{
1012
s3::{InlinedS3BucketSpec, S3AccessStyle},
1113
secret_class::SecretClassVolume,
@@ -79,9 +81,7 @@ impl S3LogDir {
7981
TlsVerification::Server(server_verification) => {
8082
match &server_verification.ca_cert {
8183
CaCert::WebPki {} => {}
82-
CaCert::SecretClass(_) => {
83-
return S3TlsCaVerificationNotSupportedSnafu.fail()
84-
}
84+
CaCert::SecretClass(_) => {}
8585
}
8686
}
8787
}
@@ -120,6 +120,7 @@ impl S3LogDir {
120120
);
121121
}
122122
}
123+
123124
result
124125
}
125126

@@ -177,6 +178,35 @@ impl S3LogDir {
177178
)
178179
}
179180

181+
pub fn volumes(&self) -> Vec<Volume> {
182+
let mut volumes: Vec<Volume> = self.credentials_volume().into_iter().collect();
183+
184+
if let Some(secret_name) = tlscerts::tls_secret_name(&self.bucket.connection) {
185+
volumes.push(
186+
VolumeBuilder::new(secret_name)
187+
.ephemeral(SecretOperatorVolumeSourceBuilder::new(secret_name).build())
188+
.build(),
189+
);
190+
}
191+
volumes
192+
}
193+
194+
pub fn volume_mounts(&self) -> Vec<VolumeMount> {
195+
let mut volume_mounts: Vec<VolumeMount> =
196+
self.credentials_volume_mount().into_iter().collect();
197+
198+
if let Some(secret_name) = tlscerts::tls_secret_name(&self.bucket.connection) {
199+
let secret_dir = format!("{STACKABLE_MOUNT_PATH_TLS}/{secret_name}");
200+
201+
volume_mounts.push(VolumeMount {
202+
name: secret_name.to_string(),
203+
mount_path: secret_dir,
204+
..VolumeMount::default()
205+
});
206+
}
207+
volume_mounts
208+
}
209+
180210
pub fn credentials_volume(&self) -> Option<Volume> {
181211
self.credentials()
182212
.map(|credentials| credentials.to_volume(credentials.secret_class.as_ref()))

rust/crd/src/tlscerts.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use stackable_operator::commons::{
2+
s3::S3ConnectionSpec,
3+
tls::{CaCert, TlsVerification},
4+
};
5+
6+
use crate::{
7+
constants::{
8+
STACKABLE_MOUNT_PATH_TLS, STACKABLE_TLS_STORE_PASSWORD, STACKABLE_TRUST_STORE,
9+
SYSTEM_TRUST_STORE, SYSTEM_TRUST_STORE_PASSWORD,
10+
},
11+
s3logdir::S3LogDir,
12+
};
13+
14+
pub fn tls_secret_name(s3conn: &Option<S3ConnectionSpec>) -> Option<&str> {
15+
if let Some(conn) = s3conn.as_ref() {
16+
if let Some(tls) = &conn.tls {
17+
if let TlsVerification::Server(verification) = &tls.verification {
18+
if let CaCert::SecretClass(secret_name) = &verification.ca_cert {
19+
return Some(secret_name);
20+
}
21+
}
22+
}
23+
}
24+
None
25+
}
26+
27+
pub fn tls_secret_names<'a>(
28+
s3conn: &'a Option<S3ConnectionSpec>,
29+
s3logdir: &'a Option<S3LogDir>,
30+
) -> Option<Vec<&'a str>> {
31+
let mut names = Vec::new();
32+
33+
if let Some(secret_name) = tls_secret_name(s3conn) {
34+
names.push(secret_name);
35+
}
36+
37+
if let Some(logdir) = s3logdir {
38+
if let Some(secret_name) = tls_secret_name(&logdir.bucket.connection) {
39+
names.push(secret_name);
40+
}
41+
}
42+
if names.is_empty() {
43+
None
44+
} else {
45+
Some(names)
46+
}
47+
}
48+
49+
pub fn create_key_and_trust_store() -> Vec<String> {
50+
vec![
51+
format!("keytool -importkeystore -srckeystore {SYSTEM_TRUST_STORE} -srcstoretype jks -srcstorepass {SYSTEM_TRUST_STORE_PASSWORD} -destkeystore {STACKABLE_TRUST_STORE}/truststore.p12 -deststoretype pkcs12 -deststorepass {STACKABLE_TLS_STORE_PASSWORD} -noprompt"),
52+
]
53+
}
54+
55+
pub fn add_cert_to_stackable_truststore(secret_name: &str) -> Vec<String> {
56+
vec![
57+
format!("echo [{STACKABLE_MOUNT_PATH_TLS}/{secret_name}/ca.crt] Adding cert..."),
58+
format!("keytool -importcert -file {STACKABLE_MOUNT_PATH_TLS}/{secret_name}/ca.crt -alias stackable-{secret_name} -keystore {STACKABLE_TRUST_STORE}/truststore.p12 -storepass {STACKABLE_TLS_STORE_PASSWORD} -noprompt"),
59+
format!("echo [{STACKABLE_MOUNT_PATH_TLS}/{secret_name}/ca.crt] Checking for cert..."),
60+
format!("keytool -list -keystore {STACKABLE_TRUST_STORE}/truststore.p12 -storepass {STACKABLE_TLS_STORE_PASSWORD} -noprompt | grep stackable"),
61+
]
62+
}

0 commit comments

Comments
 (0)