# How Music Assistant Manages Secret Values and Provider Credentials in Its Configuration System

> Learn how Music Assistant securely manages secret values and provider credentials with encryption and strict permission tags, keeping your data safe from plaintext logs and UI.

- Repository: [Music Assistant/server](https://github.com/music-assistant/server)
- Tags: internals
- Published: 2026-06-17

---

**Music Assistant encrypts all secret configuration values marked as `SECURE_STRING` and guards write operations with a strict permission tag, ensuring credentials never appear in plaintext logs or UI responses.**

The Music Assistant server implements a hardened configuration system that treats provider credentials as sensitive data requiring encryption, permission-controlled access, and UI masking from the moment they enter the system. Every setting—whether for a music provider, core controller, or player—is stored as a **`ConfigEntry`** within the central **`ConfigController`**, with special handling for entries typed as **`ConfigEntryType.SECURE_STRING`**.

## The ConfigEntry Architecture

All configuration data in Music Assistant flows through **[`music_assistant/controllers/config.py`](https://github.com/music-assistant/server/blob/main/music_assistant/controllers/config.py)**, where the `ConfigController` manages a SQLite-backed store of `ConfigEntry` objects defined in **[`music_assistant/models/config_entries.py`](https://github.com/music-assistant/server/blob/main/music_assistant/models/config_entries.py)**. When a provider declares a credential field—such as Spotify’s `client_secret` or Yandex Music’s `token`—it sets the entry type to `SECURE_STRING`.

### ConfigEntryType.SECURE_STRING

This enum value triggers the secret handling pipeline. Unlike standard strings, `SECURE_STRING` entries are:

- **Encrypted at rest** before persistence in the database
- **Gated by permission tags** on every write operation
- **Masked in API responses** replaced with the constant **`SECURE_STRING_SUBSTITUTE`** (`"*****"`) defined in **[`music_assistant/models/constants.py`](https://github.com/music-assistant/server/blob/main/music_assistant/models/constants.py)**

## The Write-Gate Security Layer

Before any secret value reaches the database, the system validates the caller's permissions in **[`music_assistant/providers/fastmcp_server/config_io/secret_handler.py`](https://github.com/music-assistant/server/blob/main/music_assistant/providers/fastmcp_server/config_io/secret_handler.py)**.

### Permission Tags

The `assert_secret_write_allowed()` function inspects every write request. If the target entry is a `SECURE_STRING` and the caller’s tag set lacks **`config:write:secret`**, the operation raises a `ToolError`:

```python

# music_assistant/providers/fastmcp_server/config_io/secret_handler.py

def assert_secret_write_allowed(
    entries: Mapping[str, ConfigEntry],
    key: str,
    tags: set[str],
) -> None:
    if is_secret_key(entries, key) and "config:write:secret" not in tags:
        raise ToolError(
            f"SECURE_STRING write requires config:write:secret tag (key={key!r})"
        )

```

This **least-privilege design** prevents accidental credential leaks from generic configuration endpoints.

## Encryption and Storage

Once the write-gate validates the request, the `ConfigController` encrypts the plaintext value using internal encryption helpers before storing it in the SQLite database via **[`helpers/database.py`](https://github.com/music-assistant/server/blob/main/helpers/database.py)**. The raw secret never touches the disk in plaintext.

## Frontend Masking and API Safety

When the UI or HTTP API requests configuration data, the controller’s `to_dict()` method iterates over entries and applies `_mask_secret()` to every `SECURE_STRING` value:

```python
def _mask_secret(value: str | None) -> str | None:
    return SECURE_STRING_SUBSTITUTE if value is not None else None

```

The resulting payload sent to the frontend looks like this:

```json
{
    "client_id": "my-app-id",
    "client_secret": "*****"
}

```

The Home Assistant/JS UI never receives the real credential. When a user opens a configuration dialog, secret fields appear empty; if they enter a new value, it is transmitted encrypted, and the UI receives only the masked placeholder in subsequent responses. Additional protection exists in **[`music_assistant/providers/fastmcp_server/config_io/differ.py`](https://github.com/music-assistant/server/blob/main/music_assistant/providers/fastmcp_server/config_io/differ.py)**, which masks secrets in diff previews to prevent credential leakage in logs.

## Provider Credential Flow

Provider modules declare credential fields by importing `ConfigEntryType` and constructing `ConfigEntry` objects in their `CONFIG_ENTRIES` list.

### Declaration

The Spotify provider in **[`music_assistant/providers/spotify/__init__.py`](https://github.com/music-assistant/server/blob/main/music_assistant/providers/spotify/__init__.py)** declares its `client_secret` as follows:

```python
from music_assistant_models.enums import ConfigEntryType
from music_assistant_models.config_entries import ConfigEntry

CONF_CLIENT_SECRET = "client_secret"

CONFIG_ENTRIES = [
    ConfigEntry(
        key=CONF_CLIENT_SECRET,
        type=ConfigEntryType.SECURE_STRING,
        label="Spotify client secret",
        requires_reload=True,
    ),
]

```

### Runtime Decryption

When Music Assistant initializes a provider, the `ConfigController` decrypts the stored secret and passes the plaintext to the provider’s authentication logic:

```python
secret = await config_controller.get_secret_for(provider_domain, "client_secret")
provider = SpotifyProvider(client_id=cid, client_secret=secret)

```

This decryption happens only within the trusted server environment, keeping credentials out of memory dumps and stack traces unless explicitly needed.

## Automatic Secret Refresh

Some providers, such as Yandex Music and Yandex Ynison, expose a **`SecretStr`** wrapper that supports automatic token refresh. When the wrapper’s `get_secret()` method returns a refreshed plaintext token, the `ConfigController` re-encrypts the new value and updates the database entry, still requiring the `config:write:secret` tag for the persistence step.

## Summary

- **ConfigController** centralizes all configuration logic in [`music_assistant/controllers/config.py`](https://github.com/music-assistant/server/blob/main/music_assistant/controllers/config.py), handling encryption, decryption, and access control.
- **SECURE_STRING type** triggers encryption at rest and masking in API responses, with the placeholder `"*****"` defined in [`music_assistant/models/constants.py`](https://github.com/music-assistant/server/blob/main/music_assistant/models/constants.py).
- **Write-gate protection** requires the `config:write:secret` tag for any secret modification, enforced by [`secret_handler.py`](https://github.com/music-assistant/server/blob/main/secret_handler.py) in the fastmcp_server provider.
- **Provider integration** allows modules to declare credential fields using `ConfigEntryType.SECURE_STRING`, receiving decrypted values only at runtime.
- **Audit safety** ensures secrets never appear in UI payloads, log diffs, or database exports in plaintext form.

## Frequently Asked Questions

### How does Music Assistant prevent secrets from leaking in API responses?

The `ConfigController.to_dict()` method automatically replaces every `SECURE_STRING` value with `SECURE_STRING_SUBSTITUTE` (`"*****"`) before serializing the configuration. This masking occurs in the controller layer, ensuring that HTTP endpoints and WebSocket payloads never contain plaintext credentials.

### What permission is required to update a provider’s API key or password?

Callers must possess the **`config:write:secret`** tag. The `assert_secret_write_allowed()` function in [`secret_handler.py`](https://github.com/music-assistant/server/blob/main/secret_handler.py) validates this tag for every write operation targeting a `SECURE_STRING` entry. Without this tag, the system raises a `ToolError` and rejects the update.

### Where are provider credentials actually stored in Music Assistant?

Encrypted credentials reside in the SQLite configuration database managed through [`helpers/database.py`](https://github.com/music-assistant/server/blob/main/helpers/database.py). The raw plaintext only exists transiently in memory during provider initialization or when the `ConfigController` decrypts the value for authentication purposes.

### Can Music Assistant automatically refresh expired OAuth tokens without exposing them?

Yes. Providers like Yandex Music implement a `SecretStr` wrapper that handles token refresh internally. When the wrapper detects an expired token and obtains a new one, the `ConfigController` encrypts and stores the refreshed value transparently, maintaining the same security guarantees as manual credential entry.