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

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, where the ConfigController manages a SQLite-backed store of ConfigEntry objects defined in 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

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.

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:


# 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. 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:

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:

{
    "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, 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 declares its client_secret as follows:

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:

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, 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.
  • Write-gate protection requires the config:write:secret tag for any secret modification, enforced by 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 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. 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →