.. _arch-secrets-and-credentials:

========================
Secrets and Credentials
========================

Problem
=======

Workflows need secrets — registry tokens to push container images,
forge tokens for API access, user-defined API keys for deployment.
Today there is no mechanism to inject secrets into the build
environment beyond server-level environment variables
(``MYCI_FORGE_TOKEN``).

This blocks real-world use cases:

- Pushing a Nix-built container image to a forge registry from a
  workflow step.
- Deploying to external services that require authentication.
- Any workflow that needs credentials the operator has not
  pre-configured as a server-level env var.


Current State
=============

- ``MYCI_FORGE_TOKEN`` is a single server-level token, configured
  in the k8s deployment secret. Used for commit status reporting.
- No workflow-level ``secrets`` context exists.
- No secret store exists.

.. note::

   Secret masking is planned in sprint 19. Per-user forge tokens
   are planned in sprint 21.


Three Layers
============

Secrets support builds incrementally in three layers. Each is
independently useful.

Layer 1: Secret masking
-----------------------

Prevent known secret values from appearing in log output, JSONL
event files, and SSE streams.

- A ``SecretMasker`` sits between the ``mpsc`` receiver and the
  JSONL writer. It replaces known values with ``***`` before events
  touch disk or broadcast.
- The ``::add-mask::`` workflow command lets steps register
  additional values for masking at runtime.
- Initial masking list: forge token, OAuth credentials, any value
  from a future ``secrets`` context.
- File permissions on the data directory: ``0700`` for directories,
  ``0600`` for files — consistent with the existing token store.

Masking is string replacement of known values. It cannot prevent
encoding, splitting, or exfiltration of secrets via creative
workflows. This is a fundamental limit shared by all CI systems.

**Event envelope safety**: tokens and secrets must never appear in
event envelope fields (``RunStarted``, ``JobStarted``, etc.) — only
in ``Log`` event content where masking applies. Envelopes carry
metadata (repo, ref, SHA, job/step names) which is non-sensitive.

Layer 2: Per-user forge tokens
------------------------------

The server uses the authenticated user's forge token instead of a
single global token. This enables:

- Commit status reporting under the user's identity.
- Registry push using the user's credentials.
- Repository access scoped to what the user can see.

The user's forge token is obtained during OAuth login and
associated with their session. When a webhook triggers a run, the
server resolves the repository owner's token from the session store.

Open questions:

- **Persistence**: session-only (lost on server restart) vs stored
  (encrypted at rest). Session-only is simpler but requires
  re-login after restart.
- **Token scope**: the OAuth token may not have registry push scope.
  Users might need to grant additional permissions or provide a
  separate registry token.

Layer 3: Workflow secrets context
---------------------------------

A ``secrets`` expression context, matching the GitHub Actions model:

.. code-block:: yaml

    steps:
      - name: Push image
        run: skopeo copy ... --creds ${{ secrets.REGISTRY_TOKEN }}

Secrets are stored server-side, scoped per repository or
organization. They are injected into the run environment at
execution time and automatically added to the masking list.

**Storage**: a simple encrypted key-value store per repository.
Could use the existing ``TokenStore`` patterns (file-based,
permission-checked) initially. A proper secret management backend
(Vault, SOPS, k8s secrets) is a future option.

**API**: ``POST /api/repos/{repo}/secrets`` to create/update,
``DELETE`` to remove. Secrets are write-only — the API never returns
values, only names.

**Multi-user**: secret access follows repository permissions. A user
who can trigger runs on a repository can use its secrets. A user
who cannot see the repository cannot enumerate or use its secrets.


Registry Credentials
====================

Pushing container images to a forge registry is a specific instance
of workflow secrets. The workflow step uses ``skopeo copy`` or
``crane push`` with credentials from the secrets context.

For the common case of pushing to the same forge that triggered the
build, the per-user forge token (layer 2) may suffice — if the
OAuth scope includes registry write access. For external registries
or tokens with specific scopes, workflow secrets (layer 3) are
needed.

Example workflow:

.. code-block:: yaml

    jobs:
      build:
        runs-on: [nix]
        steps:
          - uses: actions/checkout@v4
          - name: Build image
            run: nix build .#server-image
          - name: Push to registry
            run: |
              ./result | skopeo copy \
                docker-archive:/dev/stdin \
                docker://${{ secrets.REGISTRY_HOST }}/myci/server:latest \
                --dest-creds ${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_TOKEN }}


Incremental Path
================

+---------+------------------------------+---------------------------+
| Layer   | What it enables              | Depends on                |
+=========+==============================+===========================+
| Masking | Safe log output              | Nothing                   |
+---------+------------------------------+---------------------------+
| Per-user| User-scoped forge access,    | OAuth login (done)        |
| tokens  | registry push via forge token|                           |
+---------+------------------------------+---------------------------+
| Workflow| Arbitrary secrets in         | Secret store, API,        |
| secrets | workflows                    | multi-user scoping        |
+---------+------------------------------+---------------------------+

Each layer is independently useful. Masking is a prerequisite for
the others in the sense that without it, injected secrets would leak
into logs.
