apache / apache/airflow

airflowctl ignores `--env`/`AIRFLOW_CLI_ENVIRONMENT` on non-auth commands — always uses production credentials

Open
#70,519 0 comments 0 reactions 0 assignees View on GitHub
area:airflow-ctl kind:bug needs-triage
Dominant language
Python
Stars
46.9k
Forks
17.8k
Avg merge
2d 9h
Merged PRs (30d)
472

Description

### Airflow CTL Version

Latest stable version

### Airflow CTL Command

airflowctl auth login --env staging --api-url --api-token ; airflowctl dags list --env staging

### Keyring Backend / Version

macOS Keychain (`keyring.backends.macOS.Keyring`) 25.7.0

### Auth Type

Token

### What is the current behaviour?

`--env`/`-e` is accepted by every non-auth command (`dags list`, `dags trigger`, `connections list`, etc. — anything routed through `output_command_list` / `ARG_AUTH_ENVIRONMENT`) and parses fine into `args.env`. But it's never actually used.

`provide_api_client`'s wrapper only forwards `api_token` to `get_client()`:

```python
# airflow-ctl/src/airflowctl/api/client.py
def wrapper(*args, **kwargs) -> RT:
if "api_client" not in kwargs:
api_token = getattr(args[0], "api_token", None) if args else None
with get_client(kind=kind, api_token=api_token) as api_client:
return func(*args, api_client=api_client, **kwargs)
```

and `get_client()` constructs `Credentials` with no `api_environment`:

```python
def get_client(kind=ClientKind.CLI, api_token=None):
...
credentials = Credentials(client_kind=kind, api_token=api_token).load()
```

`Credentials.__init__` defaults `api_environment` to `"production"`. Since it's never overridden here, **every non-auth command silently operates against the `production` environment** — reading `production.json` for the API URL and the `api_token_production` keychain entry — no matter what `--env` you pass.

`auth login` doesn't go through this path; it builds `Credentials(..., api_environment=args.env)` directly, so login correctly targets the requested environment and stores the token under the right keychain key. The net effect: `auth login --env staging` succeeds and looks correct, but every subsequent command (`dags list --env staging`, etc.) silently talks to `production` instead, using a stale/unrelated production token — surfacing as `{'detail': 'Token Expired'}` or `{'detail': 'Invalid JWT token'}` with no indication that the wrong environment was ever contacted.

Reproduced: I confirmed by instrumenting `Credentials(client_kind=ClientKind.CLI, api_token=None).load()` exactly as `get_client` calls it — with `sys.argv` set to `dags list --env staging`, it resolves `api_environment="production"`, `api_url` to the production URL, and `api_token` to the production keychain entry.

Workaround: setting `AIRFLOW_CLI_ENVIRONMENT=staging` in the environment (read directly via `os.getenv` in `Credentials.__init__`, independent of the CLI arg) makes commands correctly target the intended environment. `--api-token` alone does **not** work around this, since it only overrides the token, not the URL — you end up sending the right token to the wrong server.

### What is the expected results?

`--env`/`-e` passed to any `airflowctl` command should resolve the API URL and stored credentials for that environment, consistently with what `auth login --env ` stores. `provide_api_client`/`get_client` should read `args.env` (or `AIRFLOW_CLI_ENVIRONMENT`) and pass it through as `Credentials(api_environment=...)`.

### Anything else?

This is especially dangerous when a user manages multiple environments (staging/prod/etc.) via `--env`: commands *appear* to target the environment you specified, but silently execute against production instead. A `dags trigger --env staging ...` would actually trigger a DAG in production.

Related: #58230 added `--api-token` to non-auth commands for headless use but didn't wire `--env`/`api_environment` through the same path, which is effectively what left this gap open.

### Are you willing to submit PR?

- [ ] Yes I am willing to submit a PR!

### Code of Conduct

- [x] I agree to follow this project's [Code of Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)

Contributor guide

Open the contributing guide

Research direction

Start in airflow-ctl/src/airflowctl/api/client.py, reading provide_api_client's wrapper and get_client alongside Credentials loading. Trace how args.env is available for non-auth commands, then add coverage for a command such as dags list to verify that the selected environment determines both the API URL and stored token rather than production defaults.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, authentication, cli
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.