From de5ad594c0a8ac1ded42bba6bca138c0a8b84a6b Mon Sep 17 00:00:00 2001 From: Lars Francke Date: Thu, 22 Oct 2026 18:30:25 +0200 Subject: [PATCH 1/2] fix: Keep the Airflow admin password out of container logs --- .../src/controller/build/mod.rs | 44 +++++++++++++++++++ rust/operator-binary/src/crd/mod.rs | 24 ++++------ 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/rust/operator-binary/src/controller/build/mod.rs b/rust/operator-binary/src/controller/build/mod.rs index 614ffbaa..c5496183 100644 --- a/rust/operator-binary/src/controller/build/mod.rs +++ b/rust/operator-binary/src/controller/build/mod.rs @@ -416,6 +416,7 @@ pub(crate) mod test_support { mod tests { use std::collections::BTreeMap; + use rstest::rstest; use stackable_operator::kube::Resource; use super::{ @@ -503,6 +504,49 @@ mod tests { .collect() } + /// Asserts that `script` runs `set +x`, without a later `set -x`, before the first line + /// containing `needle`. + /// + /// The containers run under `bash -x`, which prints every command with its variables + /// expanded. + /// Anything that reads a secret must have xtrace set to off. + /// + /// Panics if no line contains `needle`. + fn assert_not_traced(script: &str, needle: &str) { + let lines: Vec<&str> = script.lines().map(str::trim).collect(); + let position = lines + .iter() + .position(|line| line.contains(needle)) + .unwrap_or_else(|| panic!("no line contains {needle:?} in:\n{script}")); + let last_toggle = lines[..position] + .iter() + .rev() + .find(|line| matches!(**line, "set +x" | "set -x")); + + assert_eq!( + last_toggle, + Some(&"set +x"), + "{needle:?} is traced in:\n{script}" + ); + } + + #[rstest] + #[case::airflow_3x("3.3.2")] + #[case::airflow_2x("2.9.3")] + fn admin_password_is_not_traced(#[case] product_version: &str) { + let cluster = validated_cluster_with( + "celeryExecutors", + "{config: {}, roleGroups: {}}", + |cluster| { + cluster["spec"]["image"]["productVersion"] = + yaml_serde::Value::from(product_version); + }, + ); + let args = airflow_container_args(&cluster, "my-airflow-scheduler-default"); + + assert_not_traced(&args, "$ADMIN_PASSWORD"); + } + #[test] fn webserver_start_command_passes_proxy_headers_when_proxies_are_trusted() { let cluster = cluster_with_trusted_proxies(&["10.244.0.0/16"]); diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index be2f71c7..ce380e30 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -541,6 +541,12 @@ pub struct AirflowOpaConfig { pub cache: UserInformationCache, } +// xtrace (x option) would print the expanded admin password to the container log. +const CREATE_ADMIN_USER_COMMAND: &str = "set +x +echo 'Running \"airflow users create [...]\", which is not shown as it leaks the Airflow admin credentials' +airflow users create --username \"$ADMIN_USERNAME\" --firstname \"$ADMIN_FIRSTNAME\" --lastname \"$ADMIN_LASTNAME\" --email \"$ADMIN_EMAIL\" --password \"$ADMIN_PASSWORD\" --role \"Admin\" +set -x"; + #[derive(Clone, Debug, EnumIter, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum AirflowRole { Webserver, @@ -597,14 +603,7 @@ impl AirflowRole { tracing::info!("Database initialization has been enabled."); command.extend(vec![ "airflow db migrate".to_string(), - "airflow users create \ - --username \"$ADMIN_USERNAME\" \ - --firstname \"$ADMIN_FIRSTNAME\" \ - --lastname \"$ADMIN_LASTNAME\" \ - --email \"$ADMIN_EMAIL\" \ - --password \"$ADMIN_PASSWORD\" \ - --role \"Admin\"" - .to_string(), + CREATE_ADMIN_USER_COMMAND.to_string(), ]); } else { tracing::info!("Database initialization routines have been skipped!") @@ -661,14 +660,7 @@ impl AirflowRole { // Database initialization is limited to the scheduler, see https://github.com/stackabletech/airflow-operator/issues/259 "airflow db init".to_string(), "airflow db upgrade".to_string(), - "airflow users create \ - --username \"$ADMIN_USERNAME\" \ - --firstname \"$ADMIN_FIRSTNAME\" \ - --lastname \"$ADMIN_LASTNAME\" \ - --email \"$ADMIN_EMAIL\" \ - --password \"$ADMIN_PASSWORD\" \ - --role \"Admin\"" - .to_string(), + CREATE_ADMIN_USER_COMMAND.to_string(), ]); } else { tracing::info!("Database initialization routines have been skipped!") From feda3ff0e7df258512a86c2dade6276dcde590a4 Mon Sep 17 00:00:00 2001 From: Lars Francke Date: Thu, 22 Oct 2026 21:48:26 +0200 Subject: [PATCH 2/2] chore: Add changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 892ccda2..18f9d52f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ - The Airflow 3.x scheduler container now supervises the scheduler instead of the dag-processor. Before, a dead scheduler left the Pod `Running` and `Ready` with nothing scheduling DAGs ([#847]). - The operator now watches all resources that it creates and early-exits the reconcile action when the cluster is marked for deletion ([#849]). +- The scheduler no longer prints the Airflow admin password to the container log ([#886]). ### Removed @@ -103,6 +104,7 @@ [#881]: https://github.com/stackabletech/airflow-operator/pull/881 [#884]: https://github.com/stackabletech/airflow-operator/pull/884 [#885]: https://github.com/stackabletech/airflow-operator/pull/885 +[#886]: https://github.com/stackabletech/airflow-operator/pull/886 ## [26.7.0] - 2026-07-21