Skip to content

Commit fec5b37

Browse files
authored
fix: Set FERNET_KEY to prevent connections from crashing webservers (#695)
* fix: Set FERNET_KEY to prevent connections from crashing webservers * changelog
1 parent 788b33d commit fec5b37

File tree

5 files changed

+54
-16
lines changed

5 files changed

+54
-16
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
Previously, the operator would always use the same name for the executor Pod template ConfigMap.
2626
Thus when deploying multiple Airflow instances in the same namespace, there would be a conflict over the contents of that ConfigMap ([#678]).
2727
- For versions >= 3 custom logging initializes the RemoteLogIO handler to fix remote logging ([#683]).
28+
- Prevent Airflow connections from breaking in combination with Airflow 3.
29+
This was achieved by setting the `AIRFLOW__CORE__FERNET_KEY` env var ([#695]).
2830

2931
### Removed
3032

@@ -42,6 +44,7 @@
4244
[#690]: https://github.com/stackabletech/airflow-operator/pull/690
4345
[#691]: https://github.com/stackabletech/airflow-operator/pull/691
4446
[#692]: https://github.com/stackabletech/airflow-operator/pull/692
47+
[#695]: https://github.com/stackabletech/airflow-operator/pull/695
4548

4649
## [25.7.0] - 2025-07-23
4750

rust/operator-binary/src/airflow_controller.rs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ use crate::{
9393
},
9494
authorization::AirflowAuthorizationResolved,
9595
build_recommended_labels,
96-
internal_secret::{ENV_INTERNAL_SECRET, ENV_JWT_SECRET, create_random_secret},
96+
internal_secret::{
97+
FERNET_KEY_SECRET_KEY, INTERNAL_SECRET_SECRET_KEY, JWT_SECRET_SECRET_KEY,
98+
create_random_secret,
99+
},
97100
v1alpha1,
98101
},
99102
env_vars::{self, build_airflow_template_envs},
@@ -476,8 +479,8 @@ pub async fn reconcile_airflow(
476479
}
477480

478481
create_random_secret(
479-
airflow.shared_internal_secret_name().as_ref(),
480-
ENV_INTERNAL_SECRET,
482+
&airflow.shared_internal_secret_secret_name(),
483+
INTERNAL_SECRET_SECRET_KEY,
481484
256,
482485
airflow,
483486
client,
@@ -486,15 +489,29 @@ pub async fn reconcile_airflow(
486489
.context(InvalidInternalSecretSnafu)?;
487490

488491
create_random_secret(
489-
airflow.shared_jwt_secret_name().as_ref(),
490-
ENV_JWT_SECRET,
492+
&airflow.shared_jwt_secret_secret_name(),
493+
JWT_SECRET_SECRET_KEY,
491494
256,
492495
airflow,
493496
client,
494497
)
495498
.await
496499
.context(InvalidInternalSecretSnafu)?;
497500

501+
create_random_secret(
502+
&airflow.shared_fernet_key_secret_name(),
503+
FERNET_KEY_SECRET_KEY,
504+
// https://airflow.apache.org/docs/apache-airflow/stable/security/secrets/fernet.html#security-fernet
505+
// does not document how long the fernet key should be, but recommends using
506+
// python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
507+
// which returns `jUm21LuA76YZmrIa9u4eXRg0h0P24MDC9IDOmDvJbfw=`, which has 44 characters, which makes 32 bytes.
508+
32,
509+
airflow,
510+
client,
511+
)
512+
.await
513+
.context(InvalidInternalSecretSnafu)?;
514+
498515
for (role_name, role_config) in validated_role_config.iter() {
499516
let airflow_role =
500517
AirflowRole::from_str(role_name).context(UnidentifiedAirflowRoleSnafu {

rust/operator-binary/src/crd/internal_secret.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ use crate::{airflow_controller::AIRFLOW_CONTROLLER_NAME, crd::v1alpha1};
1616
// Secret key used to run the api server. It should be as random as possible.
1717
// It should be consistent across instances of the webserver. The webserver key
1818
// is also used to authorize requests to Celery workers when logs are retrieved.
19-
pub const ENV_INTERNAL_SECRET: &str = "INTERNAL_SECRET";
19+
pub const INTERNAL_SECRET_SECRET_KEY: &str = "INTERNAL_SECRET";
2020
// Used for env-var: AIRFLOW__API_AUTH__JWT_SECRET
2121
// Secret key used to encode and decode JWTs to authenticate to public and
2222
// private APIs. It should be as random as possible, but consistent across
2323
// instances of API services.
24-
pub const ENV_JWT_SECRET: &str = "JWT_SECRET";
24+
pub const JWT_SECRET_SECRET_KEY: &str = "JWT_SECRET";
25+
// Used for env-var: AIRFLOW__CORE__FERNET_KEY
26+
// See https://airflow.apache.org/docs/apache-airflow/stable/security/secrets/fernet.html#security-fernet
27+
pub const FERNET_KEY_SECRET_KEY: &str = "FERNET_KEY";
2528

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

rust/operator-binary/src/crd/mod.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -454,13 +454,17 @@ impl v1alpha1::AirflowCluster {
454454
fragment::validate(conf_executor).context(FragmentValidationFailureSnafu)
455455
}
456456

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

461-
pub fn shared_jwt_secret_name(&self) -> String {
461+
pub fn shared_jwt_secret_secret_name(&self) -> String {
462462
format!("{}-jwt-secret", &self.name_any())
463463
}
464+
465+
pub fn shared_fernet_key_secret_name(&self) -> String {
466+
format!("{}-fernet-key", &self.name_any())
467+
}
464468
}
465469

466470
fn extract_role_from_webserver_config(

rust/operator-binary/src/env_vars.rs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ use crate::{
2121
AirflowAuthenticationClassResolved, AirflowClientAuthenticationDetailsResolved,
2222
},
2323
authorization::AirflowAuthorizationResolved,
24-
internal_secret::{ENV_INTERNAL_SECRET, ENV_JWT_SECRET},
24+
internal_secret::{
25+
FERNET_KEY_SECRET_KEY, INTERNAL_SECRET_SECRET_KEY, JWT_SECRET_SECRET_KEY,
26+
},
2527
v1alpha1,
2628
},
2729
util::{env_var_from_secret, role_service_name},
@@ -83,7 +85,7 @@ pub fn build_airflow_statefulset_envs(
8385
) -> Result<Vec<EnvVar>, Error> {
8486
let mut env: BTreeMap<String, EnvVar> = BTreeMap::new();
8587
let secret = airflow.spec.cluster_config.credentials_secret.as_str();
86-
let internal_secret_name = airflow.shared_internal_secret_name();
88+
let internal_secret_name = airflow.shared_internal_secret_secret_name();
8789

8890
env.extend(static_envs(git_sync_resources));
8991

@@ -100,7 +102,7 @@ pub fn build_airflow_statefulset_envs(
100102
env_var_from_secret(
101103
AIRFLOW_WEBSERVER_SECRET_KEY,
102104
&internal_secret_name,
103-
ENV_INTERNAL_SECRET,
105+
INTERNAL_SECRET_SECRET_KEY,
104106
),
105107
);
106108
// Replaces AIRFLOW__WEBSERVER__SECRET_KEY >= 3.0.2.
@@ -109,9 +111,19 @@ pub fn build_airflow_statefulset_envs(
109111
env_var_from_secret(
110112
"AIRFLOW__API__SECRET_KEY",
111113
&internal_secret_name,
112-
ENV_INTERNAL_SECRET,
114+
INTERNAL_SECRET_SECRET_KEY,
113115
),
114116
);
117+
118+
env.insert(
119+
"AIRFLOW__CORE__FERNET_KEY".into(),
120+
env_var_from_secret(
121+
"AIRFLOW__CORE__FERNET_KEY",
122+
&airflow.shared_fernet_key_secret_name(),
123+
FERNET_KEY_SECRET_KEY,
124+
),
125+
);
126+
115127
env.insert(
116128
AIRFLOW_DATABASE_SQL_ALCHEMY_CONN.into(),
117129
env_var_from_secret(
@@ -485,13 +497,12 @@ fn add_version_specific_env_vars(
485497
// cluster, but should also be cluster-specific.
486498
// It is accessed from a secret to avoid cluster restarts
487499
// being triggered by an operator restart.
488-
let jwt_secret_name = airflow.shared_jwt_secret_name();
489500
env.insert(
490501
"AIRFLOW__API_AUTH__JWT_SECRET".into(),
491502
env_var_from_secret(
492503
"AIRFLOW__API_AUTH__JWT_SECRET",
493-
&jwt_secret_name,
494-
ENV_JWT_SECRET,
504+
&airflow.shared_jwt_secret_secret_name(),
505+
JWT_SECRET_SECRET_KEY,
495506
),
496507
);
497508
if airflow_role == &AirflowRole::Webserver {

0 commit comments

Comments
 (0)