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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]).
Expand All @@ -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

Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion Cargo.nix

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +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"
rand = "0.9.0"
rstest = "0.26"
semver = "1.0"
serde = { version = "1.0", features = ["derive"] }
Expand Down
1 change: 0 additions & 1 deletion docs/modules/airflow/examples/example-airflow-secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ stringData:
adminUser.lastname: Admin
adminUser.email: [email protected]
adminUser.password: airflow
connections.secretKey: thisISaSECRET_1234
connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:[email protected]/airflow
# Only needed when using celery workers (instead of Kubernetes executors)
connections.celeryResultBackend: db+postgresql://airflow:[email protected]/airflow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ stringData:
adminUser.lastname: Admin
adminUser.email: [email protected]
adminUser.password: airflow
connections.secretKey: thisISaSECRET_1234
connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:[email protected]/airflow
# Only needed when using celery workers (instead of Kubernetes executors)
connections.celeryResultBackend: db+postgresql://airflow:[email protected]/airflow
Expand Down
3 changes: 0 additions & 3 deletions docs/modules/airflow/pages/getting_started/first_steps.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
1 change: 0 additions & 1 deletion examples/simple-airflow-cluster-dags-cmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ stringData:
adminUser.lastname: Admin
adminUser.email: [email protected]
adminUser.password: airflow
connections.secretKey: thisISaSECRET_1234
connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:[email protected]/airflow
# Only needed when using celery workers (instead of Kubernetes executors)
connections.celeryResultBackend: db+postgresql://airflow:[email protected]/airflow
Expand Down
1 change: 0 additions & 1 deletion examples/simple-airflow-cluster-ldap-insecure-tls.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ stringData:
adminUser.lastname: Admin
adminUser.email: [email protected]
adminUser.password: airflow
connections.secretKey: thisISaSECRET_1234
connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:[email protected]/airflow
# Only needed when using celery workers (instead of Kubernetes executors)
connections.celeryResultBackend: db+postgresql://airflow:[email protected]/airflow
Expand Down
1 change: 0 additions & 1 deletion examples/simple-airflow-cluster-ldap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ stringData:
adminUser.lastname: Admin
adminUser.email: [email protected]
adminUser.password: airflow
connections.secretKey: thisISaSECRET_1234
connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:[email protected]/airflow
# Only needed when using celery workers (instead of Kubernetes executors)
connections.celeryResultBackend: db+postgresql://airflow:[email protected]/airflow
Expand Down
1 change: 0 additions & 1 deletion examples/simple-airflow-cluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ stringData:
adminUser.lastname: Admin
adminUser.email: [email protected]
adminUser.password: airflow
connections.secretKey: thisISaSECRET_1234
connections.sqlalchemyDatabaseUri: postgresql+psycopg2://airflow:[email protected]/airflow
# Only needed when using celery workers (instead of Kubernetes executors)
connections.celeryResultBackend: db+postgresql://airflow:[email protected]/airflow
Expand Down
4 changes: 3 additions & 1 deletion rust/operator-binary/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,20 @@ 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
rand.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
snafu.workspace = true
strum.workspace = true
tokio.workspace = true
tracing.workspace = true
indoc.workspace = true

[build-dependencies]
built.workspace = true
Expand Down
25 changes: 24 additions & 1 deletion rust/operator-binary/src/airflow_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ use crate::{
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::{
Expand Down Expand Up @@ -346,6 +348,9 @@ pub enum Error {
ResolveProductImage {
source: product_image_selection::Error,
},

#[snafu(display("failed to create internal secret"))]
InvalidInternalSecret { source: crd::internal_secret::Error },
}

type Result<T, E = Error> = std::result::Result<T, E>;
Expand Down Expand Up @@ -470,6 +475,24 @@ pub async fn reconcile_airflow(
.await?;
}

create_random_secret(
airflow.shared_internal_secret_name().as_ref(),
ENV_INTERNAL_SECRET,
airflow,
client,
)
.await
.context(InvalidInternalSecretSnafu)?;

create_random_secret(
airflow.shared_jwt_secret_name().as_ref(),
ENV_JWT_SECRET,
airflow,
client,
)
.await
.context(InvalidInternalSecretSnafu)?;

for (role_name, role_config) in validated_role_config.iter() {
let airflow_role =
AirflowRole::from_str(role_name).context(UnidentifiedAirflowRoleSnafu {
Expand Down
101 changes: 101 additions & 0 deletions rust/operator-binary/src/crd/internal_secret.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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,
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<T, E = Error> = std::result::Result<T, E>;

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,
airflow: &v1alpha1::AirflowCluster,
client: &Client,
) -> Result<()> {
let mut internal_secret = BTreeMap::new();
internal_secret.insert(secret_key.to_string(), get_random_base64());

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>(
&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() -> String {
let serial_number = rand::random::<u64>();
let bytes = serial_number.to_le_bytes();
general_purpose::STANDARD.encode(bytes)
}
9 changes: 9 additions & 0 deletions rust/operator-binary/src/crd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -452,6 +453,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(
Expand Down
Loading