Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
Previously, the operator would always use the same name for the executor Pod template ConfigMap.
Thus when deploying multiple Airflow instances in the same namespace, there would be a conflict over the contents of that ConfigMap ([#678]).
- For versions >= 3 custom logging initializes the RemoteLogIO handler to fix remote logging ([#683]).
- Prevent Airflow connections from breaking in combination with Airflow 3.
This was achieved by setting the `AIRFLOW__CORE__FERNET_KEY` env var ([#695]).

### Removed

Expand All @@ -42,6 +44,7 @@
[#690]: https://github.com/stackabletech/airflow-operator/pull/690
[#691]: https://github.com/stackabletech/airflow-operator/pull/691
[#692]: https://github.com/stackabletech/airflow-operator/pull/692
[#695]: https://github.com/stackabletech/airflow-operator/pull/695

## [25.7.0] - 2025-07-23

Expand Down
27 changes: 22 additions & 5 deletions rust/operator-binary/src/airflow_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ use crate::{
},
authorization::AirflowAuthorizationResolved,
build_recommended_labels,
internal_secret::{ENV_INTERNAL_SECRET, ENV_JWT_SECRET, create_random_secret},
internal_secret::{
FERNET_KEY_SECRET_KEY, INTERNAL_SECRET_SECRET_KEY, JWT_SECRET_SECRET_KEY,
create_random_secret,
},
v1alpha1,
},
env_vars::{self, build_airflow_template_envs},
Expand Down Expand Up @@ -476,8 +479,8 @@ pub async fn reconcile_airflow(
}

create_random_secret(
airflow.shared_internal_secret_name().as_ref(),
ENV_INTERNAL_SECRET,
&airflow.shared_internal_secret_secret_name(),
INTERNAL_SECRET_SECRET_KEY,
256,
airflow,
client,
Expand All @@ -486,15 +489,29 @@ pub async fn reconcile_airflow(
.context(InvalidInternalSecretSnafu)?;

create_random_secret(
airflow.shared_jwt_secret_name().as_ref(),
ENV_JWT_SECRET,
&airflow.shared_jwt_secret_secret_name(),
JWT_SECRET_SECRET_KEY,
256,
airflow,
client,
)
.await
.context(InvalidInternalSecretSnafu)?;

create_random_secret(
&airflow.shared_fernet_key_secret_name(),
FERNET_KEY_SECRET_KEY,
// https://airflow.apache.org/docs/apache-airflow/stable/security/secrets/fernet.html#security-fernet
// does not document how long the fernet key should be, but recommends using
// python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
// which returns `jUm21LuA76YZmrIa9u4eXRg0h0P24MDC9IDOmDvJbfw=`, which has 44 characters, which makes 32 bytes.
32,
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
7 changes: 5 additions & 2 deletions rust/operator-binary/src/crd/internal_secret.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ use crate::{airflow_controller::AIRFLOW_CONTROLLER_NAME, crd::v1alpha1};
// 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";
pub const INTERNAL_SECRET_SECRET_KEY: &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";
pub const JWT_SECRET_SECRET_KEY: &str = "JWT_SECRET";
// Used for env-var: AIRFLOW__CORE__FERNET_KEY
// See https://airflow.apache.org/docs/apache-airflow/stable/security/secrets/fernet.html#security-fernet
pub const FERNET_KEY_SECRET_KEY: &str = "FERNET_KEY";

type Result<T, E = Error> = std::result::Result<T, E>;

Expand Down
8 changes: 6 additions & 2 deletions rust/operator-binary/src/crd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,13 +454,17 @@ impl v1alpha1::AirflowCluster {
fragment::validate(conf_executor).context(FragmentValidationFailureSnafu)
}

pub fn shared_internal_secret_name(&self) -> String {
pub fn shared_internal_secret_secret_name(&self) -> String {
format!("{}-internal-secret", &self.name_any())
}

pub fn shared_jwt_secret_name(&self) -> String {
pub fn shared_jwt_secret_secret_name(&self) -> String {
format!("{}-jwt-secret", &self.name_any())
}

pub fn shared_fernet_key_secret_name(&self) -> String {
format!("{}-fernet-key", &self.name_any())
}
}

fn extract_role_from_webserver_config(
Expand Down
25 changes: 18 additions & 7 deletions rust/operator-binary/src/env_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ use crate::{
AirflowAuthenticationClassResolved, AirflowClientAuthenticationDetailsResolved,
},
authorization::AirflowAuthorizationResolved,
internal_secret::{ENV_INTERNAL_SECRET, ENV_JWT_SECRET},
internal_secret::{
FERNET_KEY_SECRET_KEY, INTERNAL_SECRET_SECRET_KEY, JWT_SECRET_SECRET_KEY,
},
v1alpha1,
},
util::{env_var_from_secret, role_service_name},
Expand Down Expand Up @@ -83,7 +85,7 @@ pub fn build_airflow_statefulset_envs(
) -> Result<Vec<EnvVar>, Error> {
let mut env: BTreeMap<String, EnvVar> = BTreeMap::new();
let secret = airflow.spec.cluster_config.credentials_secret.as_str();
let internal_secret_name = airflow.shared_internal_secret_name();
let internal_secret_name = airflow.shared_internal_secret_secret_name();

env.extend(static_envs(git_sync_resources));

Expand All @@ -100,7 +102,7 @@ pub fn build_airflow_statefulset_envs(
env_var_from_secret(
AIRFLOW_WEBSERVER_SECRET_KEY,
&internal_secret_name,
ENV_INTERNAL_SECRET,
INTERNAL_SECRET_SECRET_KEY,
),
);
// Replaces AIRFLOW__WEBSERVER__SECRET_KEY >= 3.0.2.
Expand All @@ -109,9 +111,19 @@ pub fn build_airflow_statefulset_envs(
env_var_from_secret(
"AIRFLOW__API__SECRET_KEY",
&internal_secret_name,
ENV_INTERNAL_SECRET,
INTERNAL_SECRET_SECRET_KEY,
),
);

env.insert(
"AIRFLOW__CORE__FERNET_KEY".into(),
env_var_from_secret(
"AIRFLOW__CORE__FERNET_KEY",
&airflow.shared_fernet_key_secret_name(),
FERNET_KEY_SECRET_KEY,
),
);

env.insert(
AIRFLOW_DATABASE_SQL_ALCHEMY_CONN.into(),
env_var_from_secret(
Expand Down Expand Up @@ -485,13 +497,12 @@ fn add_version_specific_env_vars(
// cluster, but should also be cluster-specific.
// 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(),
env_var_from_secret(
"AIRFLOW__API_AUTH__JWT_SECRET",
&jwt_secret_name,
ENV_JWT_SECRET,
&airflow.shared_jwt_secret_secret_name(),
JWT_SECRET_SECRET_KEY,
),
);
if airflow_role == &AirflowRole::Webserver {
Expand Down
Loading