From 87bcb060a53e7f8c2e988e43251a7705dcd21626 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Wed, 10 Sep 2025 14:32:03 +0200 Subject: [PATCH 1/7] chore: Use internal secret for JWT token --- Cargo.lock | 54 +++++++ Cargo.nix | 144 ++++++++++++++++++ Cargo.toml | 1 + rust/operator-binary/Cargo.toml | 3 +- .../operator-binary/src/airflow_controller.rs | 94 +++++++++++- rust/operator-binary/src/crd/mod.rs | 12 ++ rust/operator-binary/src/env_vars.rs | 31 ++-- 7 files changed, 316 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df41415b..c1cf7089 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -773,6 +773,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1728,12 +1743,50 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "opentelemetry" version = "0.30.0" @@ -2612,6 +2665,7 @@ dependencies = [ "fnv", "futures 0.3.31", "indoc", + "openssl", "product-config", "rstest", "serde", diff --git a/Cargo.nix b/Cargo.nix index becc710f..5016f730 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -2339,6 +2339,34 @@ rec { "default" = [ "std" ]; }; }; + "foreign-types" = rec { + crateName = "foreign-types"; + version = "0.3.2"; + edition = "2015"; + sha256 = "1cgk0vyd7r45cj769jym4a6s7vwshvd0z4bqrb92q1fwibmkkwzn"; + libName = "foreign_types"; + authors = [ + "Steven Fackler " + ]; + dependencies = [ + { + name = "foreign-types-shared"; + packageId = "foreign-types-shared"; + } + ]; + + }; + "foreign-types-shared" = rec { + crateName = "foreign-types-shared"; + version = "0.1.1"; + edition = "2015"; + sha256 = "0jxgzd04ra4imjv8jgkmdq59kj8fsz6w4zxsbmlai34h26225c00"; + libName = "foreign_types_shared"; + authors = [ + "Steven Fackler " + ]; + + }; "form_urlencoded" = rec { crateName = "form_urlencoded"; version = "1.2.2"; @@ -5614,6 +5642,77 @@ rec { }; resolvedDefaultFeatures = [ "default" ]; }; + "openssl" = rec { + crateName = "openssl"; + version = "0.10.73"; + edition = "2021"; + sha256 = "1y7b3kanpgb92wiqhwbyqfsdr1xdjzssxqywl4cixay88r6p61c5"; + authors = [ + "Steven Fackler " + ]; + dependencies = [ + { + name = "bitflags"; + packageId = "bitflags"; + } + { + name = "cfg-if"; + packageId = "cfg-if"; + } + { + name = "foreign-types"; + packageId = "foreign-types"; + } + { + name = "libc"; + packageId = "libc"; + } + { + name = "once_cell"; + packageId = "once_cell"; + } + { + name = "openssl-macros"; + packageId = "openssl-macros"; + } + { + name = "openssl-sys"; + packageId = "openssl-sys"; + rename = "ffi"; + } + ]; + features = { + "aws-lc" = [ "ffi/aws-lc" ]; + "bindgen" = [ "ffi/bindgen" ]; + "unstable_boringssl" = [ "ffi/unstable_boringssl" ]; + "vendored" = [ "ffi/vendored" ]; + }; + resolvedDefaultFeatures = [ "default" ]; + }; + "openssl-macros" = rec { + crateName = "openssl-macros"; + version = "0.1.1"; + edition = "2018"; + sha256 = "173xxvfc63rr5ybwqwylsir0vq6xsj4kxiv4hmg4c3vscdmncj59"; + procMacro = true; + libName = "openssl_macros"; + dependencies = [ + { + name = "proc-macro2"; + packageId = "proc-macro2"; + } + { + name = "quote"; + packageId = "quote"; + } + { + name = "syn"; + packageId = "syn 2.0.106"; + features = [ "full" ]; + } + ]; + + }; "openssl-probe" = rec { crateName = "openssl-probe"; version = "0.1.6"; @@ -5625,6 +5724,47 @@ rec { ]; }; + "openssl-sys" = rec { + crateName = "openssl-sys"; + version = "0.9.109"; + edition = "2021"; + links = "openssl"; + sha256 = "0wc54dshsac1xicq6b5wz01p358zcbf542f2s6vph3b38wp6w2ch"; + build = "build/main.rs"; + libName = "openssl_sys"; + authors = [ + "Alex Crichton " + "Steven Fackler " + ]; + dependencies = [ + { + name = "libc"; + packageId = "libc"; + } + ]; + buildDependencies = [ + { + name = "cc"; + packageId = "cc"; + } + { + name = "pkg-config"; + packageId = "pkg-config"; + } + { + name = "vcpkg"; + packageId = "vcpkg"; + } + ]; + features = { + "aws-lc" = [ "dep:aws-lc-sys" ]; + "bindgen" = [ "dep:bindgen" ]; + "bssl-sys" = [ "dep:bssl-sys" ]; + "openssl-src" = [ "dep:openssl-src" ]; + "unstable_boringssl" = [ "bssl-sys" ]; + "vendored" = [ "openssl-src" ]; + }; + }; "opentelemetry" = rec { crateName = "opentelemetry"; version = "0.30.0"; @@ -8472,6 +8612,10 @@ rec { name = "indoc"; packageId = "indoc"; } + { + name = "openssl"; + packageId = "openssl"; + } { name = "product-config"; packageId = "product-config"; diff --git a/Cargo.toml b/Cargo.toml index bbe8df44..2368d4ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ const_format = "0.2" fnv = "1.0" futures = { version = "0.3", features = ["compat"] } indoc = "2.0" +openssl = "0.10" rstest = "0.26" semver = "1.0" serde = { version = "1.0", features = ["derive"] } diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index 3edfae3e..27dff66e 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -17,6 +17,8 @@ clap.workspace = true const_format.workspace = true fnv.workspace = true futures.workspace = true +indoc.workspace = true +openssl.workspace = true serde.workspace = true serde_json.workspace = true serde_yaml.workspace = true @@ -24,7 +26,6 @@ snafu.workspace = true strum.workspace = true tokio.workspace = true tracing.workspace = true -indoc.workspace = true [build-dependencies] built.workspace = true diff --git a/rust/operator-binary/src/airflow_controller.rs b/rust/operator-binary/src/airflow_controller.rs index 13419ff1..b71d6bfd 100644 --- a/rust/operator-binary/src/airflow_controller.rs +++ b/rust/operator-binary/src/airflow_controller.rs @@ -29,6 +29,7 @@ use stackable_operator::{ }, }, }, + client::Client, cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, commons::{ product_image_selection::{self, ResolvedProductImage}, @@ -43,7 +44,7 @@ use stackable_operator::{ api::{ apps::v1::{StatefulSet, StatefulSetSpec}, core::v1::{ - ConfigMap, PersistentVolumeClaim, PodTemplateSpec, Probe, ServiceAccount, + ConfigMap, PersistentVolumeClaim, PodTemplateSpec, Probe, Secret, ServiceAccount, TCPSocketAction, }, }, @@ -84,10 +85,11 @@ use crate::{ controller_commons::{self, CONFIG_VOLUME_NAME, LOG_CONFIG_VOLUME_NAME, LOG_VOLUME_NAME}, crd::{ self, AIRFLOW_CONFIG_FILENAME, APP_NAME, AirflowClusterStatus, AirflowConfig, - AirflowConfigOptions, AirflowExecutor, AirflowRole, CONFIG_PATH, Container, ExecutorConfig, - ExecutorConfigFragment, HTTP_PORT, HTTP_PORT_NAME, LISTENER_VOLUME_DIR, - LISTENER_VOLUME_NAME, LOG_CONFIG_DIR, METRICS_PORT, METRICS_PORT_NAME, OPERATOR_NAME, - STACKABLE_LOG_DIR, TEMPLATE_LOCATION, TEMPLATE_NAME, TEMPLATE_VOLUME_NAME, + AirflowConfigOptions, AirflowExecutor, AirflowRole, CONFIG_PATH, Container, + ENV_INTERNAL_SECRET, ENV_JWT_SECRET, ExecutorConfig, ExecutorConfigFragment, HTTP_PORT, + HTTP_PORT_NAME, LISTENER_VOLUME_DIR, LISTENER_VOLUME_NAME, LOG_CONFIG_DIR, METRICS_PORT, + METRICS_PORT_NAME, OPERATOR_NAME, STACKABLE_LOG_DIR, TEMPLATE_LOCATION, TEMPLATE_NAME, + TEMPLATE_VOLUME_NAME, authentication::{ AirflowAuthenticationClassResolved, AirflowClientAuthenticationDetailsResolved, }, @@ -346,6 +348,19 @@ pub enum Error { ResolveProductImage { source: product_image_selection::Error, }, + + #[snafu(display("object defines no namespace"))] + ObjectHasNoNamespace, + + #[snafu(display("failed to retrieve secret for internal communications"))] + FailedToRetrieveInternalSecret { + source: stackable_operator::client::Error, + }, + + #[snafu(display("failed to apply internal secret"))] + ApplyInternalSecret { + source: stackable_operator::client::Error, + }, } type Result = std::result::Result; @@ -470,6 +485,26 @@ pub async fn reconcile_airflow( .await?; } + create_random_secret( + airflow.shared_internal_secret_name().as_ref(), + ENV_INTERNAL_SECRET, + 512, + airflow, + client, + ) + .await?; + + // This secret is created even if spooling is not configured. + // Trino currently requires the secret to be exactly 256 bits long. + create_random_secret( + airflow.shared_jwt_secret_name().as_ref(), + ENV_JWT_SECRET, + 512, + airflow, + client, + ) + .await?; + for (role_name, role_config) in validated_role_config.iter() { let airflow_role = AirflowRole::from_str(role_name).context(UnidentifiedAirflowRoleSnafu { @@ -1415,3 +1450,52 @@ fn add_git_sync_resources( Ok(()) } + +async fn create_random_secret( + secret_name: &str, + secret_key: &str, + secret_byte_size: usize, + airflow: &v1alpha1::AirflowCluster, + client: &Client, +) -> Result<()> { + let mut internal_secret = BTreeMap::new(); + internal_secret.insert(secret_key.to_string(), get_random_base64(secret_byte_size)); + + let secret = Secret { + immutable: Some(true), + metadata: ObjectMetaBuilder::new() + .name(secret_name) + .namespace_opt(airflow.namespace()) + .ownerreference_from_resource(airflow, None, Some(true)) + .context(ObjectMissingMetadataForOwnerRefSnafu)? + .build(), + string_data: Some(internal_secret), + ..Secret::default() + }; + + if client + .get_opt::( + &secret.name_any(), + secret + .namespace() + .as_deref() + .context(ObjectHasNoNamespaceSnafu)?, + ) + .await + .context(FailedToRetrieveInternalSecretSnafu)? + .is_none() + { + client + .apply_patch(AIRFLOW_CONTROLLER_NAME, &secret, &secret) + .await + .context(ApplyInternalSecretSnafu)?; + } + + Ok(()) +} + +fn get_random_base64(byte_size: usize) -> String { + let mut buf: Vec = vec![0; byte_size]; + openssl::rand::rand_bytes(&mut buf).unwrap(); + openssl::base64::encode_block(&buf) +} diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 772fd43c..b36ce5e6 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -89,6 +89,10 @@ pub const MAX_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity { unit: BinaryMultiple::Mebi, }; +// secret vars +pub const ENV_INTERNAL_SECRET: &str = "INTERNAL_SECRET"; +pub const ENV_JWT_SECRET: &str = "JWT_SECRET"; + #[derive(Snafu, Debug)] pub enum Error { #[snafu(display("Unknown Airflow role found {role}. Should be one of {roles:?}"))] @@ -452,6 +456,14 @@ impl v1alpha1::AirflowCluster { tracing::debug!("Merged executor config: {:?}", conf_executor); fragment::validate(conf_executor).context(FragmentValidationFailureSnafu) } + + pub fn shared_internal_secret_name(&self) -> String { + format!("{}-internal-secret", &self.name_any()) + } + + pub fn shared_jwt_secret_name(&self) -> String { + format!("{}-jwt-secret", &self.name_any()) + } } fn extract_role_from_webserver_config( diff --git a/rust/operator-binary/src/env_vars.rs b/rust/operator-binary/src/env_vars.rs index 610d9afe..a7d45458 100644 --- a/rust/operator-binary/src/env_vars.rs +++ b/rust/operator-binary/src/env_vars.rs @@ -15,8 +15,8 @@ use stackable_operator::{ use crate::{ crd::{ - AirflowExecutor, AirflowRole, ExecutorConfig, LOG_CONFIG_DIR, STACKABLE_LOG_DIR, - TEMPLATE_LOCATION, TEMPLATE_NAME, + AirflowExecutor, AirflowRole, ENV_INTERNAL_SECRET, ENV_JWT_SECRET, ExecutorConfig, + LOG_CONFIG_DIR, STACKABLE_LOG_DIR, TEMPLATE_LOCATION, TEMPLATE_NAME, authentication::{ AirflowAuthenticationClassResolved, AirflowClientAuthenticationDetailsResolved, }, @@ -58,11 +58,6 @@ const ADMIN_EMAIL: &str = "ADMIN_EMAIL"; const PYTHONPATH: &str = "PYTHONPATH"; -/// This key is only intended for use during experimental support and will -/// be replaced with a secret at a later stage. See the issue covering -/// this at . -const JWT_KEY: &str = "ThisKeyIsNotIntendedForProduction!"; - #[derive(Snafu, Debug)] pub enum Error { #[snafu(display( @@ -87,6 +82,7 @@ pub fn build_airflow_statefulset_envs( ) -> Result, Error> { let mut env: BTreeMap = BTreeMap::new(); let secret = airflow.spec.cluster_config.credentials_secret.as_str(); + let internal_secret_name = airflow.shared_internal_secret_name(); env.extend(static_envs(git_sync_resources)); @@ -97,12 +93,12 @@ pub fn build_airflow_statefulset_envs( env.insert( AIRFLOW_WEBSERVER_SECRET_KEY.into(), - // The secret key is used to run the webserver flask app and also used to authorize - // requests to Celery workers when logs are retrieved. + // The secret key is used to run the webserver flask app and also + // used to authorize requests to Celery workers when logs are retrieved. env_var_from_secret( AIRFLOW_WEBSERVER_SECRET_KEY, - secret, - "connections.secretKey", + &internal_secret_name, + ENV_INTERNAL_SECRET, ), ); env.insert( @@ -477,15 +473,16 @@ fn add_version_specific_env_vars( // api-services and replicas/roles for a given // cluster, but should also be cluster-specific. // See issue : - // later it will be accessed from a secret to avoid cluster restarts + // it is accessed from a secret to avoid cluster restarts // being triggered by an operator restart. + let jwt_secret_name = airflow.shared_jwt_secret_name(); env.insert( "AIRFLOW__API_AUTH__JWT_SECRET".into(), - EnvVar { - name: "AIRFLOW__API_AUTH__JWT_SECRET".into(), - value: Some(JWT_KEY.into()), - ..Default::default() - }, + env_var_from_secret( + "AIRFLOW__API_AUTH__JWT_SECRET", + &jwt_secret_name, + ENV_JWT_SECRET, + ), ); if airflow_role == &AirflowRole::Webserver { // Sometimes a race condition can arise when both scheduler and From a595224da9e67db3fbddce55ed919ceeee569c6a Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Wed, 10 Sep 2025 15:27:18 +0200 Subject: [PATCH 2/7] remove connections.secretKey references, changelog, env-var comments --- CHANGELOG.md | 5 +++++ .../airflow/examples/example-airflow-secret.yaml | 1 - .../getting_started/code/airflow-credentials.yaml | 1 - .../airflow/pages/getting_started/first_steps.adoc | 3 --- examples/simple-airflow-cluster-dags-cmap.yaml | 1 - .../simple-airflow-cluster-ldap-insecure-tls.yaml | 1 - examples/simple-airflow-cluster-ldap.yaml | 1 - examples/simple-airflow-cluster.yaml | 1 - rust/operator-binary/src/crd/mod.rs | 10 +++++++++- rust/operator-binary/src/env_vars.rs | 11 +++++++++++ .../cluster-operation/08-install-airflow.yaml.j2 | 1 - .../external-access/install-airflow-cluster.yaml.j2 | 1 - .../kuttl/ldap/60-install-airflow-cluster.yaml.j2 | 1 - .../kuttl/logging/41-install-airflow-cluster.yaml.j2 | 1 - .../30-install-airflow-cluster.yaml.j2 | 1 - .../30-install-airflow-cluster.yaml.j2 | 1 - tests/templates/kuttl/oidc/install-airflow.yaml.j2 | 1 - tests/templates/kuttl/opa/30-install-airflow.yaml.j2 | 1 - .../30-install-airflow-cluster.yaml.j2 | 1 - .../kuttl/overrides/10-install-airflow.yaml.j2 | 2 -- .../remote-logging/40-install-airflow-cluster.yaml.j2 | 1 - .../resources/30-install-airflow-cluster.yaml.j2 | 1 - .../kuttl/smoke/40-install-airflow-cluster.yaml.j2 | 1 - .../triggerer/30-install-airflow-cluster.yaml.j2 | 1 - 24 files changed, 25 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a7e8e05..8dcdf673 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ - Add new roles for dag-processor and triggerer processes ([#679]). - Added a note on webserver workers to the trouble-shooting section ([#685]). +### Changed + +- Use internal secrets for secret- and jwt-keys ([#686]). + ### Fixed - Don't panic on invalid authorization config. Previously, a missing OPA ConfigMap would crash the operator ([#667]). @@ -24,6 +28,7 @@ [#679]: https://github.com/stackabletech/airflow-operator/pull/679 [#683]: https://github.com/stackabletech/airflow-operator/pull/683 [#685]: https://github.com/stackabletech/airflow-operator/pull/685 +[#686]: https://github.com/stackabletech/airflow-operator/pull/686 ## [25.7.0] - 2025-07-23 diff --git a/docs/modules/airflow/examples/example-airflow-secret.yaml b/docs/modules/airflow/examples/example-airflow-secret.yaml index 4df23926..5e112e91 100644 --- a/docs/modules/airflow/examples/example-airflow-secret.yaml +++ b/docs/modules/airflow/examples/example-airflow-secret.yaml @@ -10,7 +10,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql.default.svc.cluster.local/airflow # Only needed when using celery workers (instead of Kubernetes executors) connections.celeryResultBackend: db+postgresql://airflow:airflow@airflow-postgresql.default.svc.cluster.local/airflow diff --git a/docs/modules/airflow/examples/getting_started/code/airflow-credentials.yaml b/docs/modules/airflow/examples/getting_started/code/airflow-credentials.yaml index 4df23926..5e112e91 100644 --- a/docs/modules/airflow/examples/getting_started/code/airflow-credentials.yaml +++ b/docs/modules/airflow/examples/getting_started/code/airflow-credentials.yaml @@ -10,7 +10,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql.default.svc.cluster.local/airflow # Only needed when using celery workers (instead of Kubernetes executors) connections.celeryResultBackend: db+postgresql://airflow:airflow@airflow-postgresql.default.svc.cluster.local/airflow diff --git a/docs/modules/airflow/pages/getting_started/first_steps.adoc b/docs/modules/airflow/pages/getting_started/first_steps.adoc index 3ee569ff..f25fcd2c 100644 --- a/docs/modules/airflow/pages/getting_started/first_steps.adoc +++ b/docs/modules/airflow/pages/getting_started/first_steps.adoc @@ -23,9 +23,6 @@ And apply it: [source,bash] include::example$getting_started/code/getting_started.sh[tag=apply-airflow-credentials] -The `connections.secretKey` is used for securely signing the session cookies and can be used for any other security related needs by extensions. -It should be a long random string of bytes. - `connections.sqlalchemyDatabaseUri` must contain the connection string to the SQL database storing the Airflow metadata. `connections.celeryResultBackend` must contain the connection string to the SQL database storing the job metadata (the example above uses the same PostgreSQL database for both). diff --git a/examples/simple-airflow-cluster-dags-cmap.yaml b/examples/simple-airflow-cluster-dags-cmap.yaml index ae8145ca..8899293a 100644 --- a/examples/simple-airflow-cluster-dags-cmap.yaml +++ b/examples/simple-airflow-cluster-dags-cmap.yaml @@ -10,7 +10,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql.default.svc.cluster.local/airflow # Only needed when using celery workers (instead of Kubernetes executors) connections.celeryResultBackend: db+postgresql://airflow:airflow@airflow-postgresql.default.svc.cluster.local/airflow diff --git a/examples/simple-airflow-cluster-ldap-insecure-tls.yaml b/examples/simple-airflow-cluster-ldap-insecure-tls.yaml index 80168a86..334c4c70 100644 --- a/examples/simple-airflow-cluster-ldap-insecure-tls.yaml +++ b/examples/simple-airflow-cluster-ldap-insecure-tls.yaml @@ -138,7 +138,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql.default.svc.cluster.local/airflow # Only needed when using celery workers (instead of Kubernetes executors) connections.celeryResultBackend: db+postgresql://airflow:airflow@airflow-postgresql.default.svc.cluster.local/airflow diff --git a/examples/simple-airflow-cluster-ldap.yaml b/examples/simple-airflow-cluster-ldap.yaml index a22fda12..a0e17a4e 100644 --- a/examples/simple-airflow-cluster-ldap.yaml +++ b/examples/simple-airflow-cluster-ldap.yaml @@ -136,7 +136,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql.default.svc.cluster.local/airflow # Only needed when using celery workers (instead of Kubernetes executors) connections.celeryResultBackend: db+postgresql://airflow:airflow@airflow-postgresql.default.svc.cluster.local/airflow diff --git a/examples/simple-airflow-cluster.yaml b/examples/simple-airflow-cluster.yaml index 39aebe6c..54b66d4c 100644 --- a/examples/simple-airflow-cluster.yaml +++ b/examples/simple-airflow-cluster.yaml @@ -10,7 +10,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql.default.svc.cluster.local/airflow # Only needed when using celery workers (instead of Kubernetes executors) connections.celeryResultBackend: db+postgresql://airflow:airflow@airflow-postgresql.default.svc.cluster.local/airflow diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index b36ce5e6..50a36cad 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -89,8 +89,16 @@ pub const MAX_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity { unit: BinaryMultiple::Mebi, }; -// secret vars +// Used for env-vars: AIRFLOW__WEBSERVER__SECRET_KEY, AIRFLOW__API__SECRET_KEY +// N.B. AIRFLOW__WEBSERVER__SECRET_KEY is deprecated as of 3.0.2. +// Secret key used to run the api server. It should be as random as possible. +// It should be consistent across instances of the webserver. The webserver key +// is also used to authorize requests to Celery workers when logs are retrieved. pub const ENV_INTERNAL_SECRET: &str = "INTERNAL_SECRET"; +// Used for env-var: AIRFLOW__API_AUTH__JWT_SECRET +// Secret key used to encode and decode JWTs to authenticate to public and +// private APIs. It should be as random as possible, but consistent across +// instances of API services. pub const ENV_JWT_SECRET: &str = "JWT_SECRET"; #[derive(Snafu, Debug)] diff --git a/rust/operator-binary/src/env_vars.rs b/rust/operator-binary/src/env_vars.rs index a7d45458..6e13b0d4 100644 --- a/rust/operator-binary/src/env_vars.rs +++ b/rust/operator-binary/src/env_vars.rs @@ -91,6 +91,7 @@ pub fn build_airflow_statefulset_envs( add_version_specific_env_vars(airflow, airflow_role, resolved_product_image, &mut env); + // N.B. this has been deprecated and replaced with AIRFLOW__API__SECRET_KEY since 3.0.2. Can be removed when 3.0.1 is no longer supported. env.insert( AIRFLOW_WEBSERVER_SECRET_KEY.into(), // The secret key is used to run the webserver flask app and also @@ -101,6 +102,16 @@ pub fn build_airflow_statefulset_envs( ENV_INTERNAL_SECRET, ), ); + env.insert( + "AIRFLOW__API__SECRET_KEY".into(), + // The secret key is used to run the webserver flask app and also + // used to authorize requests to Celery workers when logs are retrieved. + env_var_from_secret( + "AIRFLOW__API__SECRET_KEY", + &internal_secret_name, + ENV_INTERNAL_SECRET, + ), + ); env.insert( AIRFLOW_DATABASE_SQL_ALCHEMY_CONN.into(), env_var_from_secret( diff --git a/tests/templates/kuttl/cluster-operation/08-install-airflow.yaml.j2 b/tests/templates/kuttl/cluster-operation/08-install-airflow.yaml.j2 index 8410596e..3aa50e6e 100644 --- a/tests/templates/kuttl/cluster-operation/08-install-airflow.yaml.j2 +++ b/tests/templates/kuttl/cluster-operation/08-install-airflow.yaml.j2 @@ -15,7 +15,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql/airflow connections.celeryResultBackend: db+postgresql://airflow:airflow@airflow-postgresql/airflow connections.celeryBrokerUrl: redis://:redis@airflow-redis-master:6379/0 diff --git a/tests/templates/kuttl/external-access/install-airflow-cluster.yaml.j2 b/tests/templates/kuttl/external-access/install-airflow-cluster.yaml.j2 index 0ed7716b..84f6547a 100644 --- a/tests/templates/kuttl/external-access/install-airflow-cluster.yaml.j2 +++ b/tests/templates/kuttl/external-access/install-airflow-cluster.yaml.j2 @@ -10,7 +10,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql/airflow connections.celeryResultBackend: db+postgresql://airflow:airflow@airflow-postgresql/airflow connections.celeryBrokerUrl: redis://:redis@airflow-redis-master:6379/0 diff --git a/tests/templates/kuttl/ldap/60-install-airflow-cluster.yaml.j2 b/tests/templates/kuttl/ldap/60-install-airflow-cluster.yaml.j2 index fb8ced2c..cbba7152 100644 --- a/tests/templates/kuttl/ldap/60-install-airflow-cluster.yaml.j2 +++ b/tests/templates/kuttl/ldap/60-install-airflow-cluster.yaml.j2 @@ -18,7 +18,6 @@ commands: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql/airflow {% if test_scenario['values']['executor'] == 'celery' %} connections.celeryResultBackend: db+postgresql://airflow:airflow@airflow-postgresql/airflow diff --git a/tests/templates/kuttl/logging/41-install-airflow-cluster.yaml.j2 b/tests/templates/kuttl/logging/41-install-airflow-cluster.yaml.j2 index 7ebb7d78..498f6db0 100644 --- a/tests/templates/kuttl/logging/41-install-airflow-cluster.yaml.j2 +++ b/tests/templates/kuttl/logging/41-install-airflow-cluster.yaml.j2 @@ -15,7 +15,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql/airflow {% if test_scenario['values']['executor'] == 'celery' %} connections.celeryResultBackend: db+postgresql://airflow:airflow@airflow-postgresql/airflow diff --git a/tests/templates/kuttl/mount-dags-configmap/30-install-airflow-cluster.yaml.j2 b/tests/templates/kuttl/mount-dags-configmap/30-install-airflow-cluster.yaml.j2 index e11f6bc0..b3bf36c8 100644 --- a/tests/templates/kuttl/mount-dags-configmap/30-install-airflow-cluster.yaml.j2 +++ b/tests/templates/kuttl/mount-dags-configmap/30-install-airflow-cluster.yaml.j2 @@ -15,7 +15,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql/airflow {% if test_scenario['values']['executor'] == 'celery' %} connections.celeryResultBackend: db+postgresql://airflow:airflow@airflow-postgresql/airflow diff --git a/tests/templates/kuttl/mount-dags-gitsync/30-install-airflow-cluster.yaml.j2 b/tests/templates/kuttl/mount-dags-gitsync/30-install-airflow-cluster.yaml.j2 index 5b0d5ca4..7d5d098f 100644 --- a/tests/templates/kuttl/mount-dags-gitsync/30-install-airflow-cluster.yaml.j2 +++ b/tests/templates/kuttl/mount-dags-gitsync/30-install-airflow-cluster.yaml.j2 @@ -15,7 +15,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql/airflow {% if test_scenario['values']['executor'] == 'celery' %} connections.celeryResultBackend: db+postgresql://airflow:airflow@airflow-postgresql/airflow diff --git a/tests/templates/kuttl/oidc/install-airflow.yaml.j2 b/tests/templates/kuttl/oidc/install-airflow.yaml.j2 index 9fd843b0..48826e4b 100644 --- a/tests/templates/kuttl/oidc/install-airflow.yaml.j2 +++ b/tests/templates/kuttl/oidc/install-airflow.yaml.j2 @@ -11,7 +11,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql/airflow --- apiVersion: v1 diff --git a/tests/templates/kuttl/opa/30-install-airflow.yaml.j2 b/tests/templates/kuttl/opa/30-install-airflow.yaml.j2 index 631602ac..9fe3daa0 100644 --- a/tests/templates/kuttl/opa/30-install-airflow.yaml.j2 +++ b/tests/templates/kuttl/opa/30-install-airflow.yaml.j2 @@ -15,7 +15,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql/airflow --- apiVersion: airflow.stackable.tech/v1alpha1 diff --git a/tests/templates/kuttl/orphaned-resources/30-install-airflow-cluster.yaml.j2 b/tests/templates/kuttl/orphaned-resources/30-install-airflow-cluster.yaml.j2 index 8410596e..3aa50e6e 100644 --- a/tests/templates/kuttl/orphaned-resources/30-install-airflow-cluster.yaml.j2 +++ b/tests/templates/kuttl/orphaned-resources/30-install-airflow-cluster.yaml.j2 @@ -15,7 +15,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql/airflow connections.celeryResultBackend: db+postgresql://airflow:airflow@airflow-postgresql/airflow connections.celeryBrokerUrl: redis://:redis@airflow-redis-master:6379/0 diff --git a/tests/templates/kuttl/overrides/10-install-airflow.yaml.j2 b/tests/templates/kuttl/overrides/10-install-airflow.yaml.j2 index 7c5324e6..efc2c61a 100644 --- a/tests/templates/kuttl/overrides/10-install-airflow.yaml.j2 +++ b/tests/templates/kuttl/overrides/10-install-airflow.yaml.j2 @@ -10,7 +10,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql/airflow connections.celeryResultBackend: db+postgresql://airflow:airflow@airflow-postgresql/airflow connections.celeryBrokerUrl: redis://:redis@airflow-redis-master:6379/0 @@ -26,7 +25,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql/airflow connections.celeryResultBackend: db+postgresql://airflow:airflow@airflow-postgresql/airflow connections.celeryBrokerUrl: redis://:redis@airflow-redis-master:6379/0 diff --git a/tests/templates/kuttl/remote-logging/40-install-airflow-cluster.yaml.j2 b/tests/templates/kuttl/remote-logging/40-install-airflow-cluster.yaml.j2 index 694322e8..c0719d48 100644 --- a/tests/templates/kuttl/remote-logging/40-install-airflow-cluster.yaml.j2 +++ b/tests/templates/kuttl/remote-logging/40-install-airflow-cluster.yaml.j2 @@ -15,7 +15,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql/airflow {% if test_scenario['values']['executor'] == 'celery' %} connections.celeryResultBackend: db+postgresql://airflow:airflow@airflow-postgresql/airflow diff --git a/tests/templates/kuttl/resources/30-install-airflow-cluster.yaml.j2 b/tests/templates/kuttl/resources/30-install-airflow-cluster.yaml.j2 index cda76f90..5af22cb8 100644 --- a/tests/templates/kuttl/resources/30-install-airflow-cluster.yaml.j2 +++ b/tests/templates/kuttl/resources/30-install-airflow-cluster.yaml.j2 @@ -15,7 +15,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql/airflow connections.celeryResultBackend: db+postgresql://airflow:airflow@airflow-postgresql/airflow connections.celeryBrokerUrl: redis://:redis@airflow-redis-master:6379/0 diff --git a/tests/templates/kuttl/smoke/40-install-airflow-cluster.yaml.j2 b/tests/templates/kuttl/smoke/40-install-airflow-cluster.yaml.j2 index 61276545..5227979c 100644 --- a/tests/templates/kuttl/smoke/40-install-airflow-cluster.yaml.j2 +++ b/tests/templates/kuttl/smoke/40-install-airflow-cluster.yaml.j2 @@ -15,7 +15,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql/airflow {% if test_scenario['values']['executor'] == 'celery' %} connections.celeryResultBackend: db+postgresql://airflow:airflow@airflow-postgresql/airflow diff --git a/tests/templates/kuttl/triggerer/30-install-airflow-cluster.yaml.j2 b/tests/templates/kuttl/triggerer/30-install-airflow-cluster.yaml.j2 index 9ddad88b..2f728798 100644 --- a/tests/templates/kuttl/triggerer/30-install-airflow-cluster.yaml.j2 +++ b/tests/templates/kuttl/triggerer/30-install-airflow-cluster.yaml.j2 @@ -15,7 +15,6 @@ stringData: adminUser.lastname: Admin adminUser.email: airflow@airflow.com adminUser.password: airflow - connections.secretKey: thisISaSECRET_1234 connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:airflow@airflow-postgresql/airflow {% if test_scenario['values']['executor'] == 'celery' %} connections.celeryResultBackend: db+postgresql://airflow:airflow@airflow-postgresql/airflow From d06543c3a12a1bff652dd675f0d2879e2b0b1b5b Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Wed, 10 Sep 2025 15:32:30 +0200 Subject: [PATCH 3/7] cleaned up comments --- rust/operator-binary/src/airflow_controller.rs | 2 -- rust/operator-binary/src/env_vars.rs | 6 ++---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/rust/operator-binary/src/airflow_controller.rs b/rust/operator-binary/src/airflow_controller.rs index b71d6bfd..fd797129 100644 --- a/rust/operator-binary/src/airflow_controller.rs +++ b/rust/operator-binary/src/airflow_controller.rs @@ -494,8 +494,6 @@ pub async fn reconcile_airflow( ) .await?; - // This secret is created even if spooling is not configured. - // Trino currently requires the secret to be exactly 256 bits long. create_random_secret( airflow.shared_jwt_secret_name().as_ref(), ENV_JWT_SECRET, diff --git a/rust/operator-binary/src/env_vars.rs b/rust/operator-binary/src/env_vars.rs index 6e13b0d4..7a3ec0a1 100644 --- a/rust/operator-binary/src/env_vars.rs +++ b/rust/operator-binary/src/env_vars.rs @@ -102,10 +102,9 @@ pub fn build_airflow_statefulset_envs( ENV_INTERNAL_SECRET, ), ); + // Replaces AIRFLOW__WEBSERVER__SECRET_KEY >= 3.0.2. env.insert( "AIRFLOW__API__SECRET_KEY".into(), - // The secret key is used to run the webserver flask app and also - // used to authorize requests to Celery workers when logs are retrieved. env_var_from_secret( "AIRFLOW__API__SECRET_KEY", &internal_secret_name, @@ -483,8 +482,7 @@ fn add_version_specific_env_vars( // This should be random, but must also be consistent across // api-services and replicas/roles for a given // cluster, but should also be cluster-specific. - // See issue : - // it is accessed from a secret to avoid cluster restarts + // It is accessed from a secret to avoid cluster restarts // being triggered by an operator restart. let jwt_secret_name = airflow.shared_jwt_secret_name(); env.insert( From 8fe1fd4b50a56482639237959052c8254235e82a Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 15 Sep 2025 12:14:52 +0200 Subject: [PATCH 4/7] review feedback: refactoring --- .../operator-binary/src/airflow_controller.rs | 85 +++------------ .../src/crd/internal_secret.rs | 101 ++++++++++++++++++ rust/operator-binary/src/crd/mod.rs | 13 +-- rust/operator-binary/src/env_vars.rs | 5 +- 4 files changed, 119 insertions(+), 85 deletions(-) create mode 100644 rust/operator-binary/src/crd/internal_secret.rs diff --git a/rust/operator-binary/src/airflow_controller.rs b/rust/operator-binary/src/airflow_controller.rs index fd797129..55fbc4ff 100644 --- a/rust/operator-binary/src/airflow_controller.rs +++ b/rust/operator-binary/src/airflow_controller.rs @@ -29,7 +29,6 @@ use stackable_operator::{ }, }, }, - client::Client, cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, commons::{ product_image_selection::{self, ResolvedProductImage}, @@ -44,7 +43,7 @@ use stackable_operator::{ api::{ apps::v1::{StatefulSet, StatefulSetSpec}, core::v1::{ - ConfigMap, PersistentVolumeClaim, PodTemplateSpec, Probe, Secret, ServiceAccount, + ConfigMap, PersistentVolumeClaim, PodTemplateSpec, Probe, ServiceAccount, TCPSocketAction, }, }, @@ -85,16 +84,17 @@ use crate::{ controller_commons::{self, CONFIG_VOLUME_NAME, LOG_CONFIG_VOLUME_NAME, LOG_VOLUME_NAME}, crd::{ self, AIRFLOW_CONFIG_FILENAME, APP_NAME, AirflowClusterStatus, AirflowConfig, - AirflowConfigOptions, AirflowExecutor, AirflowRole, CONFIG_PATH, Container, - ENV_INTERNAL_SECRET, ENV_JWT_SECRET, ExecutorConfig, ExecutorConfigFragment, HTTP_PORT, - HTTP_PORT_NAME, LISTENER_VOLUME_DIR, LISTENER_VOLUME_NAME, LOG_CONFIG_DIR, METRICS_PORT, - METRICS_PORT_NAME, OPERATOR_NAME, STACKABLE_LOG_DIR, TEMPLATE_LOCATION, TEMPLATE_NAME, - TEMPLATE_VOLUME_NAME, + AirflowConfigOptions, AirflowExecutor, AirflowRole, CONFIG_PATH, Container, ExecutorConfig, + ExecutorConfigFragment, HTTP_PORT, HTTP_PORT_NAME, LISTENER_VOLUME_DIR, + LISTENER_VOLUME_NAME, LOG_CONFIG_DIR, METRICS_PORT, METRICS_PORT_NAME, OPERATOR_NAME, + STACKABLE_LOG_DIR, TEMPLATE_LOCATION, TEMPLATE_NAME, TEMPLATE_VOLUME_NAME, authentication::{ AirflowAuthenticationClassResolved, AirflowClientAuthenticationDetailsResolved, }, authorization::AirflowAuthorizationResolved, - build_recommended_labels, v1alpha1, + build_recommended_labels, + internal_secret::{ENV_INTERNAL_SECRET, ENV_JWT_SECRET, create_random_secret}, + v1alpha1, }, env_vars::{self, build_airflow_template_envs}, operations::{ @@ -349,18 +349,8 @@ pub enum Error { source: product_image_selection::Error, }, - #[snafu(display("object defines no namespace"))] - ObjectHasNoNamespace, - - #[snafu(display("failed to retrieve secret for internal communications"))] - FailedToRetrieveInternalSecret { - source: stackable_operator::client::Error, - }, - - #[snafu(display("failed to apply internal secret"))] - ApplyInternalSecret { - source: stackable_operator::client::Error, - }, + #[snafu(display("failed to create internal secret"))] + InvalidInternalSecret { source: crd::internal_secret::Error }, } type Result = std::result::Result; @@ -492,7 +482,8 @@ pub async fn reconcile_airflow( airflow, client, ) - .await?; + .await + .context(InvalidInternalSecretSnafu)?; create_random_secret( airflow.shared_jwt_secret_name().as_ref(), @@ -501,7 +492,8 @@ pub async fn reconcile_airflow( airflow, client, ) - .await?; + .await + .context(InvalidInternalSecretSnafu)?; for (role_name, role_config) in validated_role_config.iter() { let airflow_role = @@ -1448,52 +1440,3 @@ fn add_git_sync_resources( Ok(()) } - -async fn create_random_secret( - secret_name: &str, - secret_key: &str, - secret_byte_size: usize, - airflow: &v1alpha1::AirflowCluster, - client: &Client, -) -> Result<()> { - let mut internal_secret = BTreeMap::new(); - internal_secret.insert(secret_key.to_string(), get_random_base64(secret_byte_size)); - - let secret = Secret { - immutable: Some(true), - metadata: ObjectMetaBuilder::new() - .name(secret_name) - .namespace_opt(airflow.namespace()) - .ownerreference_from_resource(airflow, None, Some(true)) - .context(ObjectMissingMetadataForOwnerRefSnafu)? - .build(), - string_data: Some(internal_secret), - ..Secret::default() - }; - - if client - .get_opt::( - &secret.name_any(), - secret - .namespace() - .as_deref() - .context(ObjectHasNoNamespaceSnafu)?, - ) - .await - .context(FailedToRetrieveInternalSecretSnafu)? - .is_none() - { - client - .apply_patch(AIRFLOW_CONTROLLER_NAME, &secret, &secret) - .await - .context(ApplyInternalSecretSnafu)?; - } - - Ok(()) -} - -fn get_random_base64(byte_size: usize) -> String { - let mut buf: Vec = vec![0; byte_size]; - openssl::rand::rand_bytes(&mut buf).unwrap(); - openssl::base64::encode_block(&buf) -} diff --git a/rust/operator-binary/src/crd/internal_secret.rs b/rust/operator-binary/src/crd/internal_secret.rs new file mode 100644 index 00000000..af1e25e0 --- /dev/null +++ b/rust/operator-binary/src/crd/internal_secret.rs @@ -0,0 +1,101 @@ +use std::collections::BTreeMap; + +use snafu::{OptionExt, ResultExt, Snafu}; +use stackable_operator::{ + builder::meta::ObjectMetaBuilder, client::Client, k8s_openapi::api::core::v1::Secret, + kube::ResourceExt, logging::controller::ReconcilerError, +}; +use strum::{EnumDiscriminants, IntoStaticStr}; + +use crate::{airflow_controller::AIRFLOW_CONTROLLER_NAME, crd::v1alpha1}; + +// Used for env-vars: AIRFLOW__WEBSERVER__SECRET_KEY, AIRFLOW__API__SECRET_KEY +// N.B. AIRFLOW__WEBSERVER__SECRET_KEY is deprecated as of 3.0.2. +// Secret key used to run the api server. It should be as random as possible. +// It should be consistent across instances of the webserver. The webserver key +// is also used to authorize requests to Celery workers when logs are retrieved. +pub const ENV_INTERNAL_SECRET: &str = "INTERNAL_SECRET"; +// Used for env-var: AIRFLOW__API_AUTH__JWT_SECRET +// Secret key used to encode and decode JWTs to authenticate to public and +// private APIs. It should be as random as possible, but consistent across +// instances of API services. +pub const ENV_JWT_SECRET: &str = "JWT_SECRET"; + +type Result = std::result::Result; + +impl ReconcilerError for Error { + fn category(&self) -> &'static str { + ErrorDiscriminants::from(self).into() + } +} + +#[derive(Snafu, Debug, EnumDiscriminants)] +#[strum_discriminants(derive(IntoStaticStr))] +pub enum Error { + #[snafu(display("object defines no namespace"))] + ObjectHasNoNamespace, + + #[snafu(display("object is missing metadata to build owner reference"))] + ObjectMissingMetadataForOwnerRef { + source: stackable_operator::builder::meta::Error, + }, + + #[snafu(display("failed to retrieve secret for internal communications"))] + FailedToRetrieveInternalSecret { + source: stackable_operator::client::Error, + }, + + #[snafu(display("failed to apply internal secret"))] + ApplyInternalSecret { + source: stackable_operator::client::Error, + }, +} + +pub async fn create_random_secret( + secret_name: &str, + secret_key: &str, + secret_byte_size: usize, + airflow: &v1alpha1::AirflowCluster, + client: &Client, +) -> Result<()> { + let mut internal_secret = BTreeMap::new(); + internal_secret.insert(secret_key.to_string(), get_random_base64(secret_byte_size)); + + let secret = Secret { + immutable: Some(true), + metadata: ObjectMetaBuilder::new() + .name(secret_name) + .namespace_opt(airflow.namespace()) + .ownerreference_from_resource(airflow, None, Some(true)) + .context(ObjectMissingMetadataForOwnerRefSnafu)? + .build(), + string_data: Some(internal_secret), + ..Secret::default() + }; + + if client + .get_opt::( + &secret.name_any(), + secret + .namespace() + .as_deref() + .context(ObjectHasNoNamespaceSnafu)?, + ) + .await + .context(FailedToRetrieveInternalSecretSnafu)? + .is_none() + { + client + .apply_patch(AIRFLOW_CONTROLLER_NAME, &secret, &secret) + .await + .context(ApplyInternalSecretSnafu)?; + } + + Ok(()) +} + +fn get_random_base64(byte_size: usize) -> String { + let mut buf: Vec = vec![0; byte_size]; + openssl::rand::rand_bytes(&mut buf).unwrap(); + openssl::base64::encode_block(&buf) +} diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 50a36cad..8311ad22 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -60,6 +60,7 @@ use crate::{ pub mod affinity; pub mod authentication; pub mod authorization; +pub mod internal_secret; pub const APP_NAME: &str = "airflow"; pub const OPERATOR_NAME: &str = "airflow.stackable.tech"; @@ -89,18 +90,6 @@ pub const MAX_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity { unit: BinaryMultiple::Mebi, }; -// Used for env-vars: AIRFLOW__WEBSERVER__SECRET_KEY, AIRFLOW__API__SECRET_KEY -// N.B. AIRFLOW__WEBSERVER__SECRET_KEY is deprecated as of 3.0.2. -// Secret key used to run the api server. It should be as random as possible. -// It should be consistent across instances of the webserver. The webserver key -// is also used to authorize requests to Celery workers when logs are retrieved. -pub const ENV_INTERNAL_SECRET: &str = "INTERNAL_SECRET"; -// Used for env-var: AIRFLOW__API_AUTH__JWT_SECRET -// Secret key used to encode and decode JWTs to authenticate to public and -// private APIs. It should be as random as possible, but consistent across -// instances of API services. -pub const ENV_JWT_SECRET: &str = "JWT_SECRET"; - #[derive(Snafu, Debug)] pub enum Error { #[snafu(display("Unknown Airflow role found {role}. Should be one of {roles:?}"))] diff --git a/rust/operator-binary/src/env_vars.rs b/rust/operator-binary/src/env_vars.rs index 7a3ec0a1..9c60f4fb 100644 --- a/rust/operator-binary/src/env_vars.rs +++ b/rust/operator-binary/src/env_vars.rs @@ -15,12 +15,13 @@ use stackable_operator::{ use crate::{ crd::{ - AirflowExecutor, AirflowRole, ENV_INTERNAL_SECRET, ENV_JWT_SECRET, ExecutorConfig, - LOG_CONFIG_DIR, STACKABLE_LOG_DIR, TEMPLATE_LOCATION, TEMPLATE_NAME, + AirflowExecutor, AirflowRole, ExecutorConfig, LOG_CONFIG_DIR, STACKABLE_LOG_DIR, + TEMPLATE_LOCATION, TEMPLATE_NAME, authentication::{ AirflowAuthenticationClassResolved, AirflowClientAuthenticationDetailsResolved, }, authorization::AirflowAuthorizationResolved, + internal_secret::{ENV_INTERNAL_SECRET, ENV_JWT_SECRET}, v1alpha1, }, util::{env_var_from_secret, role_service_name}, From c00aa2f73c613946def30f128ead797ebc1b6782 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 15 Sep 2025 20:16:58 +0200 Subject: [PATCH 5/7] replace openssl with rand crate --- Cargo.lock | 56 +------ Cargo.nix | 154 +----------------- Cargo.toml | 3 +- rust/operator-binary/Cargo.toml | 3 +- .../operator-binary/src/airflow_controller.rs | 2 - .../src/crd/internal_secret.rs | 12 +- 6 files changed, 21 insertions(+), 209 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c1cf7089..0765299e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -773,21 +773,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1743,50 +1728,12 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" -[[package]] -name = "openssl" -version = "0.10.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" -[[package]] -name = "openssl-sys" -version = "0.9.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "opentelemetry" version = "0.30.0" @@ -2659,14 +2606,15 @@ name = "stackable-airflow-operator" version = "0.0.0-dev" dependencies = [ "anyhow", + "base64", "built", "clap", "const_format", "fnv", "futures 0.3.31", "indoc", - "openssl", "product-config", + "rand", "rstest", "serde", "serde_json", diff --git a/Cargo.nix b/Cargo.nix index 5016f730..ee4e7f97 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -2339,34 +2339,6 @@ rec { "default" = [ "std" ]; }; }; - "foreign-types" = rec { - crateName = "foreign-types"; - version = "0.3.2"; - edition = "2015"; - sha256 = "1cgk0vyd7r45cj769jym4a6s7vwshvd0z4bqrb92q1fwibmkkwzn"; - libName = "foreign_types"; - authors = [ - "Steven Fackler " - ]; - dependencies = [ - { - name = "foreign-types-shared"; - packageId = "foreign-types-shared"; - } - ]; - - }; - "foreign-types-shared" = rec { - crateName = "foreign-types-shared"; - version = "0.1.1"; - edition = "2015"; - sha256 = "0jxgzd04ra4imjv8jgkmdq59kj8fsz6w4zxsbmlai34h26225c00"; - libName = "foreign_types_shared"; - authors = [ - "Steven Fackler " - ]; - - }; "form_urlencoded" = rec { crateName = "form_urlencoded"; version = "1.2.2"; @@ -5642,77 +5614,6 @@ rec { }; resolvedDefaultFeatures = [ "default" ]; }; - "openssl" = rec { - crateName = "openssl"; - version = "0.10.73"; - edition = "2021"; - sha256 = "1y7b3kanpgb92wiqhwbyqfsdr1xdjzssxqywl4cixay88r6p61c5"; - authors = [ - "Steven Fackler " - ]; - dependencies = [ - { - name = "bitflags"; - packageId = "bitflags"; - } - { - name = "cfg-if"; - packageId = "cfg-if"; - } - { - name = "foreign-types"; - packageId = "foreign-types"; - } - { - name = "libc"; - packageId = "libc"; - } - { - name = "once_cell"; - packageId = "once_cell"; - } - { - name = "openssl-macros"; - packageId = "openssl-macros"; - } - { - name = "openssl-sys"; - packageId = "openssl-sys"; - rename = "ffi"; - } - ]; - features = { - "aws-lc" = [ "ffi/aws-lc" ]; - "bindgen" = [ "ffi/bindgen" ]; - "unstable_boringssl" = [ "ffi/unstable_boringssl" ]; - "vendored" = [ "ffi/vendored" ]; - }; - resolvedDefaultFeatures = [ "default" ]; - }; - "openssl-macros" = rec { - crateName = "openssl-macros"; - version = "0.1.1"; - edition = "2018"; - sha256 = "173xxvfc63rr5ybwqwylsir0vq6xsj4kxiv4hmg4c3vscdmncj59"; - procMacro = true; - libName = "openssl_macros"; - dependencies = [ - { - name = "proc-macro2"; - packageId = "proc-macro2"; - } - { - name = "quote"; - packageId = "quote"; - } - { - name = "syn"; - packageId = "syn 2.0.106"; - features = [ "full" ]; - } - ]; - - }; "openssl-probe" = rec { crateName = "openssl-probe"; version = "0.1.6"; @@ -5724,47 +5625,6 @@ rec { ]; }; - "openssl-sys" = rec { - crateName = "openssl-sys"; - version = "0.9.109"; - edition = "2021"; - links = "openssl"; - sha256 = "0wc54dshsac1xicq6b5wz01p358zcbf542f2s6vph3b38wp6w2ch"; - build = "build/main.rs"; - libName = "openssl_sys"; - authors = [ - "Alex Crichton " - "Steven Fackler " - ]; - dependencies = [ - { - name = "libc"; - packageId = "libc"; - } - ]; - buildDependencies = [ - { - name = "cc"; - packageId = "cc"; - } - { - name = "pkg-config"; - packageId = "pkg-config"; - } - { - name = "vcpkg"; - packageId = "vcpkg"; - } - ]; - features = { - "aws-lc" = [ "dep:aws-lc-sys" ]; - "bindgen" = [ "dep:bindgen" ]; - "bssl-sys" = [ "dep:bssl-sys" ]; - "openssl-src" = [ "dep:openssl-src" ]; - "unstable_boringssl" = [ "bssl-sys" ]; - "vendored" = [ "openssl-src" ]; - }; - }; "opentelemetry" = rec { crateName = "opentelemetry"; version = "0.30.0"; @@ -6822,7 +6682,7 @@ rec { "std_rng" = [ "dep:rand_chacha" ]; "thread_rng" = [ "std" "std_rng" "os_rng" ]; }; - resolvedDefaultFeatures = [ "alloc" "os_rng" "small_rng" "std" "std_rng" "thread_rng" ]; + resolvedDefaultFeatures = [ "alloc" "default" "os_rng" "small_rng" "std" "std_rng" "thread_rng" ]; }; "rand_chacha" = rec { crateName = "rand_chacha"; @@ -8591,6 +8451,10 @@ rec { name = "anyhow"; packageId = "anyhow"; } + { + name = "base64"; + packageId = "base64"; + } { name = "clap"; packageId = "clap"; @@ -8612,14 +8476,14 @@ rec { name = "indoc"; packageId = "indoc"; } - { - name = "openssl"; - packageId = "openssl"; - } { name = "product-config"; packageId = "product-config"; } + { + name = "rand"; + packageId = "rand"; + } { name = "serde"; packageId = "serde"; diff --git a/Cargo.toml b/Cargo.toml index 2368d4ac..2ae95e0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,13 +14,14 @@ product-config = { git = "https://github.com/stackabletech/product-config.git", stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", features = ["telemetry", "versioned"], tag = "stackable-operator-0.95.0" } anyhow = "1.0" +base64 = "0.22" built = { version = "0.8", features = ["chrono", "git2"] } clap = "4.5" const_format = "0.2" fnv = "1.0" futures = { version = "0.3", features = ["compat"] } indoc = "2.0" -openssl = "0.10" +rand = "0.9.0" rstest = "0.26" semver = "1.0" serde = { version = "1.0", features = ["derive"] } diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index 27dff66e..8abd5e6f 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -13,12 +13,13 @@ product-config.workspace = true stackable-operator.workspace = true anyhow.workspace = true +base64.workspace = true clap.workspace = true const_format.workspace = true fnv.workspace = true futures.workspace = true indoc.workspace = true -openssl.workspace = true +rand.workspace = true serde.workspace = true serde_json.workspace = true serde_yaml.workspace = true diff --git a/rust/operator-binary/src/airflow_controller.rs b/rust/operator-binary/src/airflow_controller.rs index 55fbc4ff..28cb3c8f 100644 --- a/rust/operator-binary/src/airflow_controller.rs +++ b/rust/operator-binary/src/airflow_controller.rs @@ -478,7 +478,6 @@ pub async fn reconcile_airflow( create_random_secret( airflow.shared_internal_secret_name().as_ref(), ENV_INTERNAL_SECRET, - 512, airflow, client, ) @@ -488,7 +487,6 @@ pub async fn reconcile_airflow( create_random_secret( airflow.shared_jwt_secret_name().as_ref(), ENV_JWT_SECRET, - 512, airflow, client, ) diff --git a/rust/operator-binary/src/crd/internal_secret.rs b/rust/operator-binary/src/crd/internal_secret.rs index af1e25e0..4267be20 100644 --- a/rust/operator-binary/src/crd/internal_secret.rs +++ b/rust/operator-binary/src/crd/internal_secret.rs @@ -1,5 +1,6 @@ use std::collections::BTreeMap; +use base64::{Engine as _, engine::general_purpose}; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ builder::meta::ObjectMetaBuilder, client::Client, k8s_openapi::api::core::v1::Secret, @@ -54,12 +55,11 @@ pub enum Error { pub async fn create_random_secret( secret_name: &str, secret_key: &str, - secret_byte_size: usize, airflow: &v1alpha1::AirflowCluster, client: &Client, ) -> Result<()> { let mut internal_secret = BTreeMap::new(); - internal_secret.insert(secret_key.to_string(), get_random_base64(secret_byte_size)); + internal_secret.insert(secret_key.to_string(), get_random_base64()); let secret = Secret { immutable: Some(true), @@ -94,8 +94,8 @@ pub async fn create_random_secret( Ok(()) } -fn get_random_base64(byte_size: usize) -> String { - let mut buf: Vec = vec![0; byte_size]; - openssl::rand::rand_bytes(&mut buf).unwrap(); - openssl::base64::encode_block(&buf) +fn get_random_base64() -> String { + let serial_number = rand::random::(); + let bytes = serial_number.to_le_bytes(); + general_purpose::STANDARD.encode(bytes) } From 7d06d461d652dd1537257eebbc4645ec82a45879 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 22 Sep 2025 11:52:06 +0200 Subject: [PATCH 6/7] use rand crate for generating keys --- .../operator-binary/src/airflow_controller.rs | 2 ++ .../src/crd/internal_secret.rs | 25 +++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/rust/operator-binary/src/airflow_controller.rs b/rust/operator-binary/src/airflow_controller.rs index 28cb3c8f..219d3d97 100644 --- a/rust/operator-binary/src/airflow_controller.rs +++ b/rust/operator-binary/src/airflow_controller.rs @@ -478,6 +478,7 @@ pub async fn reconcile_airflow( create_random_secret( airflow.shared_internal_secret_name().as_ref(), ENV_INTERNAL_SECRET, + 256, airflow, client, ) @@ -487,6 +488,7 @@ pub async fn reconcile_airflow( create_random_secret( airflow.shared_jwt_secret_name().as_ref(), ENV_JWT_SECRET, + 256, airflow, client, ) diff --git a/rust/operator-binary/src/crd/internal_secret.rs b/rust/operator-binary/src/crd/internal_secret.rs index 4267be20..2347bea7 100644 --- a/rust/operator-binary/src/crd/internal_secret.rs +++ b/rust/operator-binary/src/crd/internal_secret.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; use base64::{Engine as _, engine::general_purpose}; +use rand::{TryRngCore, rand_core::OsError, rngs::OsRng}; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ builder::meta::ObjectMetaBuilder, client::Client, k8s_openapi::api::core::v1::Secret, @@ -50,16 +51,20 @@ pub enum Error { ApplyInternalSecret { source: stackable_operator::client::Error, }, + + #[snafu(display("object defines no namespace"))] + SeedRandomGenerator { source: OsError }, } pub async fn create_random_secret( secret_name: &str, secret_key: &str, + secret_byte_size: usize, airflow: &v1alpha1::AirflowCluster, client: &Client, ) -> Result<()> { let mut internal_secret = BTreeMap::new(); - internal_secret.insert(secret_key.to_string(), get_random_base64()); + internal_secret.insert(secret_key.to_string(), get_random_base64(secret_byte_size)?); let secret = Secret { immutable: Some(true), @@ -94,8 +99,18 @@ pub async fn create_random_secret( Ok(()) } -fn get_random_base64() -> String { - let serial_number = rand::random::(); - let bytes = serial_number.to_le_bytes(); - general_purpose::STANDARD.encode(bytes) +fn get_random_base64(byte_size: usize) -> Result { + let mut buf = vec![0u8; byte_size]; + // OsRng is a cryptographically secure pseudo-random number generator + // (CSPRNG) and also has no possible state to leak and cannot be + // improperly seeded. See: https://rust-random.github.io/book/guide-gen.html#cryptographically-secure-pseudo-random-number-generator + // and https://github.com/rust-random/rand/blob/master/SECURITY.md#specific-generators + // This call explicity returns a Result. An alternative would be to + // use let mut rng = StdRng::from_os_rng() and then use fill_bytes + // but this may *still* panic if the underlying (OS) mechanism fails + // for some reason, so keep the potential panic transparent. + OsRng + .try_fill_bytes(&mut buf) + .context(SeedRandomGeneratorSnafu)?; + Ok(general_purpose::STANDARD.encode(buf)) } From 923cbb2759541fb58601641282d0b530c1dbb1f0 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 22 Sep 2025 12:43:03 +0200 Subject: [PATCH 7/7] corrected error text --- rust/operator-binary/src/crd/internal_secret.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/operator-binary/src/crd/internal_secret.rs b/rust/operator-binary/src/crd/internal_secret.rs index 2347bea7..f0b7d4d6 100644 --- a/rust/operator-binary/src/crd/internal_secret.rs +++ b/rust/operator-binary/src/crd/internal_secret.rs @@ -52,7 +52,7 @@ pub enum Error { source: stackable_operator::client::Error, }, - #[snafu(display("object defines no namespace"))] + #[snafu(display("failed to generate random bytes"))] SeedRandomGenerator { source: OsError }, }