> ## Documentation Index
> Fetch the complete documentation index at: https://docs.basaltic.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Set the repository policy

> Grants access to principals outside the owning account. With
principal `*` this makes the repository publicly pullable, which is
how a base-image repository is shared.

Replaced wholesale, never merged: a caller who removes a statement
from a policy they fetched expects the grant gone.

Every statement must name a principal, must use only `registry:`
actions, and must name this repository's own CRN (or `*`, which
means the same thing here). `not_actions` and `not_resources` are
refused — a negated set cannot be bounded to one repository by
inspection.

Cross-account access requires the caller's own IAM policy to allow
the action as well. A resource policy widens what this repository
permits; it cannot exceed what the caller's organization permits.




## OpenAPI

````yaml /api-reference/specs/registry.yaml put /v1/repositories/{repository_id}/policy
openapi: 3.0.3
info:
  title: Basaltic Registry API
  version: 1.0.0
  description: |
    Private container image registries, backed by Ceph.

    **This spec documents the control plane, not the pull path.** The
    registry serves two protocols on one host. Everything below is the
    `/v1` REST surface — repositories, images, lifecycle policies,
    resource policies — authenticated with a request signature or a
    console session, like every other Basaltic API. Separately, `/v2`
    implements the [OCI Distribution
    Specification](https://github.com/opencontainers/distribution-spec)
    v1.1, which is what `docker`, `podman`, `buildah`, `crane`, `skopeo`,
    and every Kubernetes node's container runtime speak. That surface is
    not described here because it is not ours to describe: its paths,
    status codes, headers, and error codes are fixed by the spec, and a
    client that finds anything else fails.

    **Getting a docker credential.** `POST /v1/authorization-token`
    returns a short-lived password. Feed it to `docker login`; the daemon
    then exchanges it at `/v2/token` for per-repository bearer tokens on
    its own, which is where the actual authorization decision is made.
    The login credential carries no authority of its own — revoking a
    policy takes effect within one bearer token's lifetime (five minutes)
    rather than at the end of a twelve-hour login.

    ```
    basaltic registry get-login-password --region sa-saopaulo-1 \
      | docker login --username basaltic --password-stdin registry.sa-saopaulo-1.basaltic.sh
    docker push registry.sa-saopaulo-1.basaltic.sh/my-account/api:v1
    ```

    **The account handle is part of the image reference.** A region
    serves one registry host, so an image lives at
    `registry.<region>.<domain>/<account_handle>/<repository>:<tag>`. That
    first path component is the only thing separating two tenants'
    `api` repositories, which is why every repository response includes a
    ready-made `uri` rather than leaving clients to assemble one.

    **Everything except a tag is immutable.** A blob is named by the
    sha256 of its bytes and a manifest by the sha256 of its own JSON, so
    pushing the same content twice is a no-op the client detects before it
    uploads a byte. A tag is the one mutable pointer — and a repository
    may forbid even that, with `tag_mutability: immutable`, so a deployed
    digest cannot be swapped under a running fleet.

    **Deleting by digest and deleting by tag are different.** Removing a
    digest removes the manifest and every tag pointing at it. Removing a
    tag leaves the manifest in place, untagged: another tag may still name
    it, and `docker rmi` of one tag must not destroy an image someone else
    is pulling by another. Untagged manifests are what the `untagged`
    lifecycle rule exists to sweep.

    **Layers are shared and counted once.** Blobs are deduplicated across
    every repository in an account, so a repository's `size_bytes` is the
    sum of the distinct blobs its manifests reference — not the sum of its
    images' sizes, which would count every shared base layer once per
    image. Deleting an image does not immediately free space: whether a
    layer is still needed is a question about every other repository in
    the account, and the answer comes from a background sweep.

    **Sharing a repository.** A repository is private to its account
    until you attach a resource policy. `PUT
    /v1/repositories/{id}/policy` is how another account — or, with
    principal `*`, the public — is granted pull access. Cross-account
    access requires BOTH the resource policy and the caller's own IAM
    policy to allow it, so one tenant cannot grant another tenant's
    employee more than that employee's organization permits.
  contact:
    name: Basaltic Support
    email: ping@basaltic.sh
  license:
    name: Proprietary
    url: https://basaltic.sh/terms
servers:
  - url: https://registry.{region}.basaltic.sh
    description: Regional API endpoint
    variables:
      region:
        default: sa-saopaulo-1
        description: Region code
security:
  - SignatureAuth: []
tags:
  - name: Authorization
    description: The credential `docker login` consumes
  - name: Repositories
    description: Repository lifecycle and settings, and the images and tags in one
  - name: Policies
    description: Resource policies and lifecycle policies
  - name: Encryption
    description: At-rest encryption of layer bytes
  - name: Pull-through cache
    description: Mirroring an upstream registry's images into a namespace of your own
paths:
  /v1/repositories/{repository_id}/policy:
    parameters:
      - $ref: '#/components/parameters/RepositoryId'
    put:
      tags:
        - Policies
      summary: Set the repository policy
      description: |
        Grants access to principals outside the owning account. With
        principal `*` this makes the repository publicly pullable, which is
        how a base-image repository is shared.

        Replaced wholesale, never merged: a caller who removes a statement
        from a policy they fetched expects the grant gone.

        Every statement must name a principal, must use only `registry:`
        actions, and must name this repository's own CRN (or `*`, which
        means the same thing here). `not_actions` and `not_resources` are
        refused — a negated set cannot be bounded to one repository by
        inspection.

        Cross-account access requires the caller's own IAM policy to allow
        the action as well. A resource policy widens what this repository
        permits; it cannot exceed what the caller's organization permits.
      operationId: setRepositoryPolicy
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RepositoryPolicyRequest'
      responses:
        '200':
          description: Policy stored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RepositoryPolicyResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  parameters:
    RepositoryId:
      name: repository_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
        example: a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
  schemas:
    RepositoryPolicyRequest:
      type: object
      properties:
        document:
          $ref: '#/components/schemas/RepositoryPolicyDocument'
      required:
        - document
    RepositoryPolicyResponse:
      type: object
      properties:
        policy:
          type: object
          properties:
            document:
              $ref: '#/components/schemas/RepositoryPolicyDocument'
            updated_at:
              type: string
              format: date-time
          required:
            - document
      required:
        - policy
    RepositoryPolicyDocument:
      type: object
      description: |
        A resource-based policy. Unlike an IAM policy, every statement must
        name a principal — the document is attached to a resource, so
        "who" cannot be implied by attachment.
      properties:
        version:
          type: string
          example: '2024-01-01'
        statements:
          type: array
          items:
            type: object
            properties:
              sid:
                type: string
              effect:
                type: string
                enum:
                  - allow
                  - deny
              principals:
                type: array
                items:
                  type: string
                description: |
                  Account CRNs, an org CRN (`crn:iam:::org/<org-id>`), or
                  `*` for the public.
                example:
                  - crn:iam::other-account:account/other-account
              actions:
                type: array
                items:
                  type: string
                description: |
                  Must all be `registry:` actions. A repository's owner
                  cannot grant `storage:GetObject` by writing it here, and
                  ignoring such a statement would leave a document that
                  reads as a grant and is not one.
                example:
                  - registry:BatchGetImage
                  - registry:GetDownloadUrlForLayer
                  - registry:ListImages
              resources:
                type: array
                items:
                  type: string
                description: |
                  Must be this repository's own CRN, or the bare `*` — the
                  same thing here, since the document is only ever
                  evaluated against this repository.
                example:
                  - '*'
              conditions:
                type: array
                items:
                  $ref: '#/components/schemas/PolicyCondition'
            required:
              - effect
              - principals
              - actions
              - resources
      required:
        - version
        - statements
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
            - request_id
          properties:
            code:
              type: string
              description: Error code identifying the type of error
              example: INVALID_INPUT
            message:
              type: string
              description: Human-readable error message
              example: Invalid request parameters
            request_id:
              type: string
              format: uuid
              description: Request ID for debugging
              example: 550e8400-e29b-41d4-a716-446655440000
    PolicyCondition:
      type: object
      properties:
        operator:
          type: string
          example: equals
        key:
          type: string
          example: basalt:ResourceTag/env
        values:
          type: array
          items:
            type: string
          example:
            - prod
        set_operator:
          type: string
          enum:
            - for_all_values
            - for_any_value
      required:
        - operator
        - key
        - values
  responses:
    BadRequest:
      description: Invalid request parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: INVALID_INPUT
              message: Invalid request parameters
              request_id: 550e8400-e29b-41d4-a716-446655440000
    Unauthorized:
      description: Authentication required or token invalid
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: UNAUTHORIZED
              message: Authentication required
              request_id: 550e8400-e29b-41d4-a716-446655440000
    Forbidden:
      description: Insufficient permissions
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: ACCESS_DENIED
              message: You don't have permission to perform this action
              request_id: 550e8400-e29b-41d4-a716-446655440000
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: NOT_FOUND
              message: Resource not found
              request_id: 550e8400-e29b-41d4-a716-446655440000
  securitySchemes:
    SignatureAuth:
      type: apiKey
      in: header
      name: Authorization
      description: >
        Request signing with an access key issued to a service account. An

        HMAC-SHA256 over a canonical form of the request, close to AWS SigV4.

        The `basaltic` CLI signs for you.


        Send `Authorization`, `X-Date` (UTC, `YYYYMMDDTHHMMSSZ`) and `X-Nonce`

        (random per request); add `X-Content-Sha256` to bind a body, and

        `X-Amz-Security-Token` when using temporary credentials.


        ```

        Authorization: BASALTIC-HMAC-SHA256
        Credential=<access_key_id>/<date>/<region>/basaltic/basaltic_request,
        SignedHeaders=host;x-date;x-nonce, Signature=<hex>

        ```


        `<region>` is the region code you are calling, or `global` for the
        global

        services. A signature is valid for 5 minutes from `X-Date`, and mutating

        requests are replay-guarded on the nonce.


        **Full signing procedure, including a working implementation:**

        https://docs.basaltic.sh/authentication


        ## Rate limits

        There is no global request budget. A limit applies only where an

        operation documents a `429`, and that operation says what it counts.

        Those responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`,

        `X-RateLimit-Reset` and, on a `429`, `Retry-After` — read them rather

        than hard-coding a number. Retrying before `Retry-After` is refused and

        extends the window. Everything else is bounded by quota, not by request

        rate.

````