Skip to content

Secret Providers

A secret provider connects the imbi-api secrets broker to an external secret system. It is not a capability: an organization configures a provider in Admin, Secrets, not in an Integration, and a provider has no Integration credentials.

A plugin declares its provider classes in PluginManifest.secret_providers:

manifest = PluginManifest(
    slug='aws',
    ...,
    secret_providers=[AwsSecretsManager],
)

imbi-api creates one instance of each class when it first needs the provider type. Two plugins cannot declare the same type: the second one is ignored, and imbi-api logs an error.

Plugin Type Issues
AWS aws_secrets_manager Static and rotated secrets from the organization's account
GitHub github_app GitHub App installation tokens, one for each resolution

The contract

Subclass SecretProvider (imbi.common.plugins.secrets) and set three class attributes:

  • type — the provider type that the provider rows store
  • label — the name that the UI shows
  • capabilities — the SecretCapability values that the provider has. The broker refuses a binding whose credential class needs a capability that the provider does not have

Every provider must override validate_config and validate_locator: the base methods raise NotImplementedError. Then override health, validate_binding, resolve, renew, and revoke for the capabilities that the provider has. The base methods raise BrokerError (operation_not_supported, provider_lease_not_renewable, or provider_lease_not_revocable). The base check_config and check_locator do nothing: override them only when the provider must check a configuration or a locator against the external system.

Two optional methods are for providers that issue repository tokens:

  • repository(config, url) — return (host, owner/name) for the repository link of a project. The agent runner clones that repository. The default returns None.
  • check_key_host(host) — validate the host of a provider key (see below). The default raises ValueError.

Rules

  • Put secret material only in Material. It has no readable repr, and it cannot be pickled or serialized.
  • Translate every error of the external system into a BrokerError with a safe reason code. Never put a provider response, a header, or a value in an error or a log.
  • Use validate_model to validate a configuration or a locator. Its error names the fields only, never the input values.

The host

The constructor gets a SecretProviderHost, which gives the broker services to the provider:

  • graph() — the graph of imbi-api
  • provider_key(...) — read a provider key: a binding that only imbi-api reads, for example the private key of a GitHub App. No grant gives a provider key to a task.
  • check_provider_key(...) — check that a binding is a provider key for a host and App id, without reading it

API reference

SecretProvider

SecretProvider(host: SecretProviderHost)

The provider contract.

Subclasses set :attr:type, :attr:label, and :attr:capabilities, and override the methods of the capabilities they have. The broker creates one instance for each type, with its :class:SecretProviderHost.

Source code in libraries/common/src/imbi/common/plugins/secrets.py
def __init__(self, host: SecretProviderHost) -> None:
    self.host = host

check_config async

check_config(
    config: dict[str, Any], organization_id: str
) -> None

Check a validated configuration against the provider.

Called when a provider is created and when its configuration changes.

Raises:

Type Description
InvalidConfiguration

The configuration is not safe to use.

Source code in libraries/common/src/imbi/common/plugins/secrets.py
async def check_config(
    self, config: dict[str, typing.Any], organization_id: str
) -> None:
    """Check a validated configuration against the provider.

    Called when a provider is created and when its configuration
    changes.

    Raises:
        InvalidConfiguration: The configuration is not safe to use.

    """

check_key_host

check_key_host(host: str) -> str

Return the normalized host of a provider key that this provider reads (provider_key_usage.host).

Raises:

Type Description
ValueError

The host is not allowed, or the provider reads no provider keys (the default).

Source code in libraries/common/src/imbi/common/plugins/secrets.py
def check_key_host(self, host: str) -> str:
    """Return the normalized ``host`` of a provider key that this
    provider reads (``provider_key_usage.host``).

    Raises:
        ValueError: The host is not allowed, or the provider reads
            no provider keys (the default).

    """
    del host
    raise ValueError(f'The {self.type} provider reads no provider keys')

check_locator async

check_locator(
    config: dict[str, Any],
    locator: dict[str, Any],
    organization_id: str,
) -> None

Check a validated locator against other bindings.

Called when a binding is written, and for each binding of a provider when its configuration changes.

Raises:

Type Description
InvalidConfiguration

The locator is not safe to use.

Source code in libraries/common/src/imbi/common/plugins/secrets.py
async def check_locator(
    self,
    config: dict[str, typing.Any],
    locator: dict[str, typing.Any],
    organization_id: str,
) -> None:
    """Check a validated locator against other bindings.

    Called when a binding is written, and for each binding of a
    provider when its configuration changes.

    Raises:
        InvalidConfiguration: The locator is not safe to use.

    """

repository

repository(
    config: dict[str, Any], url: str
) -> tuple[str, str] | None

Return (host, owner/name) of the repository at url.

For a provider that issues repository tokens: the agent runner clones the repository and asks the provider for a token on that host. None: url is not a repository on the host of this configuration (the default).

Source code in libraries/common/src/imbi/common/plugins/secrets.py
def repository(
    self, config: dict[str, typing.Any], url: str
) -> tuple[str, str] | None:
    """Return ``(host, owner/name)`` of the repository at ``url``.

    For a provider that issues repository tokens: the agent runner
    clones the repository and asks the provider for a token on
    that host. ``None``: ``url`` is not a repository on the host of
    this configuration (the default).
    """
    del config, url
    return None

validate_binding async

validate_binding(
    config: dict[str, Any],
    locator: dict[str, Any],
    organization_id: str,
    binding_id: UUID | None = None,
) -> dict[str, typing.Any]

Return metadata of the bound secret, never its value.

Source code in libraries/common/src/imbi/common/plugins/secrets.py
async def validate_binding(
    self,
    config: dict[str, typing.Any],
    locator: dict[str, typing.Any],
    organization_id: str,
    binding_id: uuid.UUID | None = None,
) -> dict[str, typing.Any]:
    """Return metadata of the bound secret, never its value."""
    raise BrokerError('operation_not_supported')

validate_config

validate_config(
    config: dict[str, Any],
) -> dict[str, typing.Any]

Return the normalized configuration.

Raises:

Type Description
InvalidConfiguration

The configuration is not valid.

Source code in libraries/common/src/imbi/common/plugins/secrets.py
def validate_config(
    self, config: dict[str, typing.Any]
) -> dict[str, typing.Any]:
    """Return the normalized configuration.

    Raises:
        InvalidConfiguration: The configuration is not valid.

    """
    raise NotImplementedError

validate_locator

validate_locator(
    locator: dict[str, Any],
    credential_class: CredentialClass,
) -> dict[str, typing.Any]

Return the normalized binding locator. No provider call.

Raises:

Type Description
InvalidConfiguration

The locator is not valid.

Source code in libraries/common/src/imbi/common/plugins/secrets.py
def validate_locator(
    self, locator: dict[str, typing.Any], credential_class: CredentialClass
) -> dict[str, typing.Any]:
    """Return the normalized binding locator. No provider call.

    Raises:
        InvalidConfiguration: The locator is not valid.

    """
    raise NotImplementedError

SecretProviderHost

Bases: Protocol

The broker services that imbi-api gives each provider.

check_provider_key async

check_provider_key(
    organization_id: str,
    reference: str,
    *,
    host: str,
    app_id: str,
) -> object

Check that a binding is a provider key for host and app_id, without reading it.

Raises:

Type Description
BrokerError

The binding is not such a provider key.

Source code in libraries/common/src/imbi/common/plugins/secrets.py
async def check_provider_key(
    self,
    organization_id: str,
    reference: str,
    *,
    host: str,
    app_id: str,
) -> object:
    """Check that a binding is a provider key for ``host`` and
    ``app_id``, without reading it.

    Raises:
        BrokerError: The binding is not such a provider key.

    """
    ...

graph

graph() -> graph.Graph

Return the graph of imbi-api.

Raises:

Type Description
BrokerError

secret_provider_unavailable when the graph is not open.

Source code in libraries/common/src/imbi/common/plugins/secrets.py
def graph(self) -> graph.Graph:
    """Return the graph of imbi-api.

    Raises:
        BrokerError: ``secret_provider_unavailable`` when the graph
            is not open.

    """
    ...

provider_key async

provider_key(
    organization_id: str,
    reference: str,
    *,
    consumer: UUID | None,
    host: str,
    app_id: str,
    task_id: UUID | None = None,
) -> Material

Return the material of a provider key for one consumer.

A provider key is a binding that only imbi-api reads, for example the private key of a GitHub App. Its provider_key_usage names the host, the app_id, and the consumer bindings that may use it. No grant gives it to a task.

Raises:

Type Description
BrokerError

The binding is not a provider key for this consumer, host, and App id, or its provider failed.

Source code in libraries/common/src/imbi/common/plugins/secrets.py
async def provider_key(
    self,
    organization_id: str,
    reference: str,
    *,
    consumer: uuid.UUID | None,
    host: str,
    app_id: str,
    task_id: uuid.UUID | None = None,
) -> Material:
    """Return the material of a provider key for one consumer.

    A provider key is a binding that only imbi-api reads, for
    example the private key of a GitHub App. Its
    ``provider_key_usage`` names the ``host``, the ``app_id``, and
    the consumer bindings that may use it. No grant gives it to a
    task.

    Raises:
        BrokerError: The binding is not a provider key for this
            consumer, host, and App id, or its provider failed.

    """
    ...

vendor_token async

vendor_token(
    organization_id: str,
    exchanger: TokenExchanger,
    target: dict[str, Any],
    *,
    agent_id: str,
) -> identity.AccessToken

Mint a vendor access token for an agent (WIF plan 4.4).

Core gets a new identity JWT from the organization's identity source and sets every claim. target must come from the binding of the agent, never from a request.

Raises:

Type Description
BrokerError

Core or the vendor refused: secret_provider_unavailable when a retry can work, else secret_materialization_failed. The reason is the :class:identity.IdentityError reason.

Source code in libraries/common/src/imbi/common/plugins/secrets.py
async def vendor_token(
    self,
    organization_id: str,
    exchanger: identity.TokenExchanger,
    target: dict[str, typing.Any],
    *,
    agent_id: str,
) -> identity.AccessToken:
    """Mint a vendor access token for an agent (WIF plan 4.4).

    Core gets a new identity JWT from the organization's identity
    source and sets every claim. ``target`` must come from the
    binding of the agent, never from a request.

    Raises:
        BrokerError: Core or the vendor refused:
            ``secret_provider_unavailable`` when a retry can work,
            else ``secret_materialization_failed``. The reason is
            the :class:`identity.IdentityError` reason.

    """
    ...

Material

Material(
    value: bytes,
    *,
    binary: bool = False,
    bearer: bool = False,
    credential_expires_at: datetime | None = None,
    provider_version: str | None = None,
    lease_id: str | None = None,
    renewable: bool = False,
    revocable: bool = False,
)

Secret material from a provider.

No readable repr, no pickling, and no dataclass or pydantic serialization, so the value cannot reach a log or a stored row by accident.

Source code in libraries/common/src/imbi/common/plugins/secrets.py
def __init__(
    self,
    value: bytes,
    *,
    binary: bool = False,
    bearer: bool = False,
    credential_expires_at: datetime.datetime | None = None,
    provider_version: str | None = None,
    lease_id: str | None = None,
    renewable: bool = False,
    revocable: bool = False,
) -> None:
    self.value = value
    self.binary = binary
    #: An OAuth access token: the client sends it in
    #: ``Authorization: Bearer``, not as an API key.
    self.bearer = bearer
    self.credential_expires_at = credential_expires_at
    self.provider_version = provider_version
    self.lease_id = lease_id
    self.renewable = renewable
    self.revocable = revocable

ResolveContext dataclass

ResolveContext(
    organization_id: str,
    task_id: UUID,
    project_id: str | None,
    purpose: str,
    ttl_seconds: int,
    credential_class: CredentialClass,
    binding_id: UUID | None = None,
    repository: str | None = None,
    agent_id: str | None = None,
)

What a provider may know about the request. No handle.

BrokerError

BrokerError(code: str, reason: str | None = None)

Bases: Exception

A broker failure with a code from :data:ERROR_STATUS.

reason is a safe reason code for audit events. Neither holds provider text or secret material.

Source code in libraries/common/src/imbi/common/plugins/secrets.py
def __init__(self, code: str, reason: str | None = None) -> None:
    if code not in ERROR_STATUS:
        raise ValueError(f'Unknown broker error code {code!r}')
    super().__init__(code)
    self.code = code
    self.reason = reason or code