Envase Vault Security Model ============================================================================= This document describes how |ev| controls access to the secrets it manages. It is intended for developers integrating with |ev|, for reviewers assessing the service, and as a record of the access decisions the service deliberately makes. .. contents:: Index Trust Boundary ----------------------------------------------------------------------------- |ev| serves a **restricted set of internal** |en| **applications and services**. Credentials to access |ev| are issued only to those clients. |ev| has no external, customer-facing, or self-service clients, and customers never communicate with |ev| directly. This is the primary access control for the service. Every other control described in this document operates **inside** that boundary and assumes the caller is already a trusted |en| client. The consequence worth stating plainly is that |ev| does not attempt to defend one authorized client from another. It defends the secrets it holds from everything outside the boundary. Access Paths ----------------------------------------------------------------------------- |ev| exposes two interfaces, and they are governed by **two different control planes**. A client uses one or the other, not both. REST API ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The REST API is fronted by |awsgw|_ and protected by an |awscog|_ authorizer. A caller must present a valid access token obtained through the standard |auth|_ process, and the token must carry the scope required by the route it is calling. Authorization is enforced at the gateway, before the service code is invoked. Each environment has its **own** |awscog|_ user pool. A token issued for one environment is not accepted by any other. AWS Lambda Functions ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The |awslb|_ functions are **not** behind the |awscog|_ authorizer, and OAuth scopes do not apply to them. Access is governed entirely by |awsiam|_: the caller's execution role must be granted permission to invoke the specific function. These functions are not reachable from the public internet. .. important:: Because the two interfaces are governed by different controls, restricting a client's scopes on the REST API does not restrict what that client can do through the function interface, and the reverse. A client deployed inside the |en| AWS account should be granted |awsiam|_ invoke permission only for the functions it actually uses. Authorization Scopes ----------------------------------------------------------------------------- The REST API defines two scopes: ``vault/secrets`` Required by every route under ``/secrets``. Grants list, read, create, update, and delete access to **all standard secrets** in the environment. ``vault/organizations`` Required by every route under ``/organizations``. Grants list, read, create, update, and delete access to **all organization secrets for all organizations** in the environment. These scopes are the **complete** authorization granularity of the service, and that is by design. They separate company-level credentials from customer-level credentials. They are deliberately **not** per-secret, per-application, or per-organization. A client should be issued only the scope it needs. A client that reads organization secrets and never touches standard secrets should hold ``vault/organizations`` alone, and the reverse. This is the main lever available for limiting what any single credential can reach. Secrets Are Not Bound to a Creating Client ----------------------------------------------------------------------------- |ev| records no owning client for a secret, and performs no ownership check on read, update, or delete. This is a deliberate design decision, not an omission. The reason is that **the client that writes a secret is routinely not the client that reads it**. Secrets are commonly provisioned by an administrative client and consumed by one or more runtime services. Deriving ownership from the creating client and enforcing it on retrieval would break the service's normal usage pattern. The same reasoning applies to the concept of an owning *application*. Standard secrets represent |en| itself rather than any individual application, so there is no application to bind them to. Two services that both need to authenticate as |en| to the same external provider are expected to read the **same** standard secret. .. note:: Because of this, a reviewer should not expect to find per-caller authorization logic in the service, and its absence is not a defect. The authorization boundary for |ev| is the scope carried by the caller's token, combined with the restricted issuance of client credentials described in `Trust Boundary`_. Organization Secrets and Customer Data ----------------------------------------------------------------------------- Organization secrets hold credentials belonging to individual |en| customers, keyed by **organization id**. Any client holding ``vault/organizations`` can read the secrets of any organization. This is not a multi-tenant isolation concern, because **no customer is ever issued a client**. Organizations are a way of *organizing* customer credentials that |en| holds and manages on their behalf; they are not a security principal and they are not an access boundary. There is no path by which one customer could use |ev| to reach another customer's secrets, because customers have no access to |ev| at all. Organization secrets are nonetheless customer data, and the separate ``vault/organizations`` scope exists so that clients dealing only with company-level credentials are not granted access to them. Environment Isolation ----------------------------------------------------------------------------- |ev| is deployed to three isolated environments: development, staging, and production. The isolation is enforced in several places at once: * Each environment has a separate |awscog|_ user pool, so a token minted for one environment is rejected by the others. * Each environment has a distinct API endpoint and a distinct function prefix. * The environment is embedded in every secret's key and ERN, for example ``ern:vault:prd:my-provider-credentials``. * Every listing operation filters on the environment tag, so a request can only ever enumerate secrets belonging to its own environment. A credential compromised in development therefore cannot reach staging or production secrets. Handling of Secret Values ----------------------------------------------------------------------------- |ev| limits the exposure of secret values in its responses: * The **listing** routes return only metadata: the secret id, its description, its link, and its organization where applicable. They never return secret values, so listing cannot be used to harvest secrets in bulk. * **Create**, **update**, and **delete** responses omit the secret value. The value is deliberately discarded before the response is serialized, to avoid transmitting secrets that the caller already has. * Only an explicit **get** of a single secret returns the value. Deletion is not immediate destruction. |ev| does not request a forced delete, so a deleted secret enters the |awssm|_ recovery window and can be restored within that period. Secrets Manager Tags ----------------------------------------------------------------------------- Secrets created by |ev| are tagged in |awssm|_ as follows: ``en:service`` Always ``vault``. This identifies |ev| as the **managing service** for the secret, so that |ev| secrets can be distinguished from other secrets in the same |awssm|_ account. It does **not** identify a consuming application. ``en:environment`` The environment the secret belongs to. ``en:key`` The secret's identifier. ``en:organization`` Present **only** on organization secrets. Its presence or absence is what separates the two kinds of secret when listing. Accepted Risks ----------------------------------------------------------------------------- The design above accepts the following risk, which is recorded here so that it is understood rather than rediscovered: **A compromised client credential exposes every secret within that credential's scope, for that credential's environment.** A stolen token or client secret carrying ``vault/secrets`` can be used to read, overwrite, or delete all standard secrets in its environment, and likewise for ``vault/organizations`` and organization secrets. This risk is accepted because the alternative, binding secrets to a creating or consuming client, is incompatible with how |ev| is used, as described in `Secrets Are Not Bound to a Creating Client`_. The controls that address it are: * Restricting the issuance of |ev| client credentials to a small, known set of internal applications and services. * Issuing each client the minimum scope it requires. * Granting |awsiam|_ invoke permission on the function interface only where it is needed. * Rotating client credentials, and monitoring |ev| access through |awsct|_.