# Authentication, config, and secrets
> Understand how a Flight authenticates to MotherDuck, expose configuration and secrets as environment variables, and follow service-account best practices.
A Flight reads three kinds of values at runtime: a **MotherDuck access token** that lets the Flight connect to your databases, a **config map** of non-secret key-value pairs surfaced as environment variables, and **Flight secrets** for sensitive values, also surfaced as environment variables. This page covers how each one works and what good defaults look like.

## MotherDuck token

A Flight authenticates to MotherDuck with an access token that the runtime injects into the Flight's environment as `MOTHERDUCK_TOKEN`. By default there is nothing to configure: create the Flight without `access_token_name` and MotherDuck uses a default access token for your user (labeled `MotherDuck Flights`), so the Flight runs with your identity and permissions.

A Flight connects with:

```python
import duckdb

def main():
    con = duckdb.connect("md:")  # picks up MOTHERDUCK_TOKEN automatically
    con.execute("SELECT current_user(), current_database()").show()

if __name__ == "__main__":
    main()
```

No credential handling in your code, no secrets in the source. The Flight inherits the token's identity and permissions: queries it runs show up against that user or service account, and the databases it can read or write are the ones that token can reach.

## Running as a specific token

To run a Flight as a different identity, typically a service account, pass the **name** (label) of a token you've already created in MotherDuck instead of relying on the default. List the tokens available to you with:

<MotherDuckSQLEditor
  database="docs_playground"
  title="List access token names"
  query={`SELECT token_name
FROM md_access_tokens();`} />

Run that query in the MotherDuck UI SQL editor or DuckDB CLI. The docs SQL editor uses the browser MotherDuck runtime, which can lag newer MotherDuck table functions.

The `token_name` value is what you pass when creating or updating a Flight. The parameter is `access_token_name` in SQL (`MD_CREATE_FLIGHT`, `MD_UPDATE_FLIGHT`) and `md_token_name` in the MCP tools (`create_flight`, `update_flight`). See [Authenticating to MotherDuck](/key-tasks/authenticating-and-connecting-to-motherduck/authenticating-to-motherduck/) for how to create tokens.

## Service accounts for production Flights

For Flights that run on a schedule, use a **service account** token rather than the default or a personal one. Personal tokens are tied to a user; if that user leaves or rotates their credentials, the Flight breaks. Service-account tokens are owned by the organization and survive personnel changes.

A good pattern:

1. Create a service account for the workload (for example, `flights-prod`).
2. Grant it the minimum set of database privileges the Flight needs.
3. Create an access token for the service account and label it descriptively (`flights-prod-ingest`).
4. Pass that token name to the Flight with `access_token_name`.

See [Service accounts](/key-tasks/service-accounts-guide/) for the full setup.

## Config: non-secret environment variables

The `config` field on a Flight is a map of string keys to string values. The runtime surfaces each entry as an environment variable.

Pass a region, a batch size, a feature flag, a destination table name — anything non-sensitive that you want to vary without editing the Python source:

```python
import os
import duckdb

def main():
    region = os.environ.get("REGION", "us-east-1")
    batch_size = int(os.environ.get("BATCH_SIZE", "1000"))

    con = duckdb.connect("md:")
    con.execute("USE warehouse")
    con.execute(f"INSERT INTO sales.metrics SELECT * FROM read_parquet('s3://incoming/{region}/*.parquet')")

if __name__ == "__main__":
    main()
```

In SQL:

```sql
CALL MD_UPDATE_FLIGHT(
    flight_id := '<flight_id>',
    config := MAP {'REGION': 'eu-central-1', 'BATCH_SIZE': '5000'}
);
```

In the MCP `update_flight` tool, `config` is a JSON object.

### Replace, not merge

Updating `config` replaces the entire map. If you have `{"REGION": "us", "BATCH_SIZE": "1000"}` and you call `update_flight` with `config = {"REGION": "eu"}`, the result is `{"REGION": "eu"}` — `BATCH_SIZE` is gone. To change one entry, send the full map with the one change applied.

### Override config per run

A run uses the Flight's stored `config` by default. To vary a value for a single run without editing the Flight, pass a `config` map to `MD_RUN_FLIGHT`:

```sql
CALL MD_RUN_FLIGHT(
    flight_id := '<flight_id>',
    config := MAP {'REGION': 'ap-south-1'}
);
```

You can only override keys that already exist in the Flight's stored `config`; a per-run override can't introduce a new key. Keys you don't override keep their stored values. The override applies to that run only and leaves the Flight's stored `config` and version untouched.

This means you don't need one Flight per configuration. Define the keys once, then point a single Flight at a different region, date partition, or destination table for an individual run. Each run records the `config` it used, so [`MD_LIST_FLIGHT_RUNS`](/sql-reference/motherduck-sql-reference/flights/md-list-flight-runs) shows the exact values every run ran with:

```sql
SELECT run_number, status, config
FROM MD_LIST_FLIGHT_RUNS(flight_id := '<flight_id>')
ORDER BY run_number DESC;
```

In the MCP `run_flight` tool, pass `config` as a JSON object.

:::note
A Flight created before per-run config overrides shipped may report empty strings for its `config` values until you update the Flight once, which redeploys it.
:::

## Secrets: sensitive environment variables

For API keys, credentials for external services, and other sensitive values, use a **Flight secret** — a MotherDuck-stored secret of `TYPE FLIGHTS` that holds key-value pairs. Create one with [`CREATE SECRET`](/sql-reference/motherduck-sql-reference/create-secret#flight-secrets):

```sql
CREATE SECRET my_api_secret IN MOTHERDUCK (
    TYPE FLIGHTS,
    PARAMS MAP {
        'API_KEY': '<your_api_key>',
        'ENDPOINT': 'https://api.example.com'
    }
);
```

Attach it by name when creating or updating the Flight. Multiple secrets can be attached to a Flight:

```sql
CALL MD_UPDATE_FLIGHT(
    flight_id := '<flight_id>',
    flight_secret_names := ['my_api_secret', 'warehouse_secret']
);
```

At run time, each key in the secret's `PARAMS` map becomes an environment variable, alongside the `config` entries. The environment variable name is the secret name prefixed to the key. For a secret named `my_api_secret`, `API_KEY` becomes `MY_API_SECRET_API_KEY`:

```python
import os

def main():
    api_key = os.environ["MY_API_SECRET_API_KEY"]
    endpoint = os.environ["MY_API_SECRET_ENDPOINT"]

if __name__ == "__main__":
    main()
```

Unlike `config`, secret values aren't exposed in the Flight's metadata. Use secrets for anything sensitive and `config` for everything else. Like `config`, `flight_secret_names` is replaced on update, not merged.

In the MCP `create_flight` and `update_flight` tools, the parameter is `md_secret_names`.

### Updating and dropping Flight secrets

After updating a Flight secret with `CREATE OR REPLACE SECRET`, update the Flight itself to redeploy it before scheduled runs pick up the new secret values:

```sql
CALL MD_UPDATE_FLIGHT(
    flight_id := '<flight_id>',
    flight_secret_names := ['my_api_secret', 'warehouse_secret']
);
```

If you drop a secret that's still attached to a Flight, the Flight fails until you recreate the secret or update the Flight to detach it.

### Cloud storage credentials

You don't need a Flight secret for S3, GCS, or Azure access. Create a regular [cloud storage secret](/sql-reference/motherduck-sql-reference/create-secret) in MotherDuck, and the Flight's DuckDB connection resolves it automatically:

```python
import duckdb

def main():
    con = duckdb.connect("md:")
    # AWS S3 read using a secret stored in MotherDuck
    con.execute("INSERT INTO raw.events SELECT * FROM read_parquet('s3://my-bucket/events/*.parquet')")

if __name__ == "__main__":
    main()
```

The `read_parquet` call resolves the S3 credential through the MotherDuck secret store when a matching secret exists and is available to the user or service account behind the Flight token.

Avoid hard-coding credentials in `config` or in the Python source: anyone who can read the Flight can read those values.

## Limits to know about

- **Per-run overrides can only set existing keys.** A per-run `config` map passed to `MD_RUN_FLIGHT` overrides values for keys already defined on the Flight. It can't add a new key. To introduce a key, update the Flight's stored `config`.
- **Config is a flat string map.** Nested structures need to be serialized (JSON-encoded into a single string value, for instance). Strings only; no numbers, booleans, or lists.


---

## Docs feedback

MotherDuck accepts optional user-submitted feedback about this page at `POST https://motherduck.com/docs/api/feedback/agent`.
For agents and automated tools, feedback submission should be user-confirmed before sending.

Payload:

```json
{
  "page_path": "/key-tasks/flights/flights-authentication-config-and-secrets/",
  "page_title": "Authentication, config, and secrets",
  "text": "<the user's feedback, max 2000 characters>",
  "source": "<optional identifier for your interface, for example 'claude.ai' or 'chatgpt'>"
}
```

`page_path` and `text` are required; `page_title` and `source` are optional. Responses: `200 {"feedback_id": "<uuid>"}`, `400` for malformed payloads, and `429` when rate-limited.
