Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: docs

on:
pull_request:
paths:
- docs/**
- mkdocs.yml
push:
branches:
- master
paths:
- docs/**
- mkdocs.yml

permissions:
contents: read

concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Copy link
Copy Markdown

@kusari-inspector kusari-inspector Bot Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actions/checkout is pinned to a mutable version tag (@v4). If this tag is reassigned to a malicious commit, arbitrary code could run in your workflow. Pin this action to a specific immutable commit SHA.

- uses: astral-sh/setup-uv@v4
Copy link
Copy Markdown

@kusari-inspector kusari-inspector Bot Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

astral-sh/setup-uv is a third-party action pinned only to a mutable tag (@v4). Third-party actions carry additional supply-chain risk compared to GitHub-owned actions. Pin this action to a specific immutable commit SHA.

- name: Build docs
run: uv run --with mkdocs-material==9.7.6 mkdocs build --strict
- uses: actions/upload-pages-artifact@v3
Copy link
Copy Markdown

@kusari-inspector kusari-inspector Bot Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actions/upload-pages-artifact is pinned to a mutable version tag (@V3). Pin this action to a specific immutable commit SHA.

with:
path: site

deploy:
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/configure-pages@v5
Copy link
Copy Markdown

@kusari-inspector kusari-inspector Bot Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actions/configure-pages is pinned to a mutable version tag (@v5) and runs in a job with pages: write and id-token: write permissions. A supply-chain compromise here could enable unauthorized deployments or OIDC token abuse. Pin this action to a specific immutable commit SHA.

- id: deployment
uses: actions/deploy-pages@v4
Copy link
Copy Markdown

@kusari-inspector kusari-inspector Bot Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actions/deploy-pages is pinned to a mutable version tag (@v4) and runs with elevated permissions (pages: write, id-token: write). This is the highest-risk unpinned action in this workflow. We strongly recommend pinning this action to a specific immutable commit SHA before merging.

5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,7 @@ coverage.html
*.swp

# Claude
.claude/scheduled_tasks.lock
.claude/scheduled_tasks.lock

# Docs site build output
site/
215 changes: 215 additions & 0 deletions docs/concepts/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# Architecture

YACD is a Kubernetes-native development environment manager for Cardano. It is
built for people developing applications on Cardano, not for validators, stake
pool operators, or production network operators. This page explains how the
system is put together and why it is shaped the way it is. For step-by-step
instructions see the [developer](../developer/getting-started.md) and
[operator](../operator/installation.md) guides; for exhaustive field and flag
facts see the [reference](../reference/cardanonetwork.md) section.

## Two artifacts: operator and CLI

YACD ships as two cooperating pieces: a Kubernetes operator and a companion
`yacd` CLI. The split is deliberate, and it follows the grain of Kubernetes
itself.

The operator owns **declarative cluster state**. It reconciles long-lived
desired state: node workloads, generated or fetched configuration, genesis
material, supporting services, persistent volumes, Secrets, ConfigMaps,
Services, and the status that reports whether all of that is healthy. If you can
sensibly express it as "this environment should exist, with these services
enabled," it belongs to the operator. The operator watches its custom resources
across namespaces and continuously drives the cluster toward the spec.

The CLI owns **imperative developer workflow**. It compiles a single
developer-facing config file into the Kubernetes resources the operator
consumes, applies them, waits for readiness, prints connection details,
forwards endpoints to your host, and runs one-off actions like topping up a
wallet. These are ad hoc commands, not desired state.

The reason for the boundary is that Kubernetes is an excellent model for desired
state and a poor model for one-off actions. "This network should run with
db-sync enabled" is a natural fit for a controller that reconciles toward it
forever. "Top this address up right now" is not: encoding a single funding
request as a resource mutation would be awkward and would leave stale objects
behind. So durable state lives in CRDs and the operator, and transient actions
live in the CLI. The CLI is a Kubernetes client; it holds no privileged state of
its own and talks to the same API server any other client would.

## CardanoNetwork: the primary resource

The primary custom resource is `CardanoNetwork` (API group `yacd.meigma.io`,
version `v1alpha1`). It is intentionally environment-shaped rather than
node-shaped: it describes a Cardano *network* and the chain-access services
exposed beside it, even though the first runtime reconciles a single primary
node.

A `CardanoNetwork` owns the core Cardano substrate. Its spec carries the network
`mode` (`local` or `public`), the primary `node` workload (shared by both
modes), the mode-specific `local` or `public` block, and a `chainAPI` block for
the services exposed next to the node. The controller reconciles the node into a
workload backed by a PVC for the node database, publishes resolved network
identity and cluster-local Service endpoints into status, and tracks health
through a set of `metav1.Condition` entries (`Ready`, `NodeReady`,
`NodeSynchronized`, `OgmiosReady`, `KupoReady`, `ArtifactsReady`, and others).
Status reports only what the controller can
observe in-cluster, so consumers do not trust stale or hand-edited values.

Two chain-access services are enabled by default, because a raw `cardano-node`
is not enough to build against. [Ogmios](https://ogmios.dev) is the default
chain-access API: it gives YACD and developers a JSON/RPC interface for query,
submit, and evaluate without every client having to share the node's Unix
socket. [Kupo](https://cardanosolutions.github.io/kupo/) is the default chain
index. Funding is CLI-native: every local network is created with a
genesis-funded `faucet` wallet, and the `yacd wallet` verbs build and submit
funding transactions directly over Ogmios and Kupo — there is no in-cluster
faucet service. For the exact defaults, ports, and images, see the
[CardanoNetwork reference](../reference/cardanonetwork.md).

## Supporting-service CRDs: CardanoDBSync

Heavier services are modeled as **separate CRDs with separate controllers**,
rather than as ever-growing fields on `CardanoNetwork`. The first such service
is `CardanoDBSync`, which runs
[cardano-db-sync](https://github.com/IntersectMBO/cardano-db-sync) and its
Postgres database.

A supporting CRD references a same-namespace `CardanoNetwork` through its
`networkRef` and derives chain information from that network rather than
re-declaring it. This keeps each controller focused: the db-sync controller
reasons about db-sync, Postgres, and ledger state, while the network controller
reasons about the Cardano substrate. It also keeps ownership clean. Enabling
db-sync does not have to disturb the primary node, and the db-sync controller
owns its own workload, storage, database wiring, config, and status.

The default placement is a **dedicated follower node**. Rather than forcing the
primary node to share its Unix socket, the db-sync controller runs its own
follower `cardano-node` colocated with db-sync in one workload. The follower
joins the primary network over ordinary node-to-node TCP and exposes a local
socket that db-sync consumes inside the same Pod. This costs extra CPU, memory,
storage, and startup time, but it preserves controller isolation: the primary
node never restarts because you added an indexer.

A `primarySidecar` placement mode is the explicit exception. When the cost of a
duplicate node outweighs the benefit of isolation, db-sync can be composed
directly into the primary node Pod and consume the primary socket. That trades
isolation for a single node copy and rolls the primary workload when the
attachment changes; it is supported for local and non-mainnet public profiles,
but not for public mainnet. For the field-level details of both modes, see the
[CardanoDBSync reference](../reference/cardanodbsync.md) and the
[db-sync operator guide](../operator/db-sync.md).

## Why a Unix socket forces this shape

Many Cardano tools want direct access to a node's Unix socket, and that single
fact drives much of the architecture. A Unix socket is a local-filesystem IPC
object. It is not a cluster-wide endpoint and cannot be exposed through a
Kubernetes Service. The robust pattern is to share it *within one Pod* using an
ephemeral volume: the node data directory lives on a PVC, while the socket
directory is ephemeral and mounted by sidecars in the same Pod.

This is why Ogmios runs as a sidecar that mounts the node socket and re-exposes
chain access as a network API, and why socket-hungry services like db-sync
default to a colocated follower node instead of reaching across Pods. YACD
deliberately avoids RWX PVCs and hostPath socket sharing, which are fragile,
scheduler-sensitive, and a poor fit for shared clusters. The same constraint
shows up at the CLI boundary: tools that speak a network protocol can be
port-forwarded to your host, but a tool that needs the node socket directly
(notably `cardano-cli`) must run *inside* the node Pod.

## Local vs public networks

The two network modes differ mostly in where chain material comes from.

A **local** network is generated and owned by YACD. The controller produces
fresh genesis and node configuration in-cluster from the spec: network magic,
ledger era, slot and epoch timing, generated stake-pool topology, and a curated
genesis profile (for example a zero-fee preset for fast tests). The generated
artifacts are staged and served over HTTP from the node's PVC by a small
cardano-tools serve endpoint, alongside a `manifest.json`, so the node and any
follower nodes consume the same configuration from one source of truth. The
`ArtifactsReady` condition reports when that bundle is staged and being served.

A **public** network joins a known profile: `preprod`, `preview`, or `mainnet`.
Instead of generating genesis, the controller fetches the published
configuration for the profile. For most profiles the node can sync from genesis,
but mainnet is far too large to sync that way in a development setting. For
mainnet, the spec requires a [Mithril](https://mithril.network) bootstrap: an
init container uses a Mithril client to download and verify a Cardano database
snapshot, seeding the node's PVC so the node starts from a recent point instead
of from genesis. The same HTTP artifact-serving pattern applies, so resolved
configuration is reachable in-cluster.

!!! warning "Mainnet is gated"
A public mainnet `CardanoNetwork` requires a Mithril bootstrap, large
persistent storage, and an explicit opt-in (`--allow-mainnet` on the CLI).
It is not the default development path. See the
[CardanoNetwork reference](../reference/cardanonetwork.md) for the
bootstrap and storage fields.

## Host access: bridging the cluster to your host

The services a `CardanoNetwork` exposes are **cluster-internal** Service URLs.
That is correct for in-cluster consumers and for supporting controllers, but a
developer's tests and tools usually run on the host. The CLI's host-access verbs
bridge that gap.

`run`, `connect`, and `exec` are the three ways across the boundary. `run` wraps
a single command (the primary CI path), exposing host-usable Ogmios/Kupo URLs
through a small `YACD_*` environment contract so tests read ordinary environment
variables instead of parsing a YACD file. `connect` holds supervised
port-forwards open in one terminal while you work in another, writing the URLs to
an `endpoints.json` file instead of wiring an environment.

`run` and the wallet funding commands do not always port-forward. A
CardanoNetwork can advertise a directly reachable `externalURL` for Ogmios and
Kupo — `yacd devnet` pins one on `localhost` (a NodePort mapped to a host port),
and a platform team can front a shared cluster with an ingress and declare its
URL once. The CLI probes that URL and uses it when reachable, falling back to an
ephemeral port-forward otherwise; `connect` always forwards. Port-forwarding
stays the universal transport because it assumes nothing about how the cluster is
reached.

`exec` is different, and it exists because of the socket constraint above. Tools
that speak a network protocol can be forwarded, but `cardano-cli` reaches the
node over its local Unix socket, which cannot be forwarded as a TCP port. So
`exec` runs the command *inside* the primary node Pod, where
`CARDANO_NODE_SOCKET_PATH` and the `YACD_*` variables are already set. The rule
is simple: forward network APIs to the host with `run`/`connect`; run
socket-bound tools in-pod with `exec`. For the verb behaviors, the `YACD_*`
table, and the `endpoints.json` schema, see the
[CLI reference](../reference/cli.md) and the
[connecting tools guide](../developer/connecting-tools.md).

!!! note "Funding is CLI-native"
Local networks are created with a genesis-funded `faucet` wallet. The `yacd
wallet` verbs spend from it by building and signing transactions on the host
and submitting them over Ogmios and Kupo; wallet keys live in labeled
Kubernetes Secrets and never leave for a server-side signer. See the
[funding guide](../developer/funding.md) and the
[security model](security.md).

## Developer and operator workflows

The same system serves two overlapping audiences.

A **developer** typically authors one developer-facing config file, checks it
into a repository, and drives it with the CLI: render and apply the network,
wait for readiness, fund a wallet, and point tests at the forwarded endpoints.
They rarely hand-author CRDs; the CLI compiles their single config into the
decomposed Kubernetes resources the operator expects, giving them one pane of
glass while preserving clean resource boundaries in the cluster.

An **operator** installs and runs the YACD operator in a cluster (locally on
[k3d](https://k3d.io) or in a shared cluster), manages the manager Deployment
and its RBAC through the Helm chart, and reasons directly about CRDs, status
conditions, placement modes, and capacity for heavier services like db-sync.

The two workflows overlap because they act on the same resources through the
same API server. A developer's `yacd up` produces the very `CardanoNetwork` an
operator can inspect with `kubectl`; an operator's installed controller is what
makes a developer's config become a running network. The CLI is a convenience
layer over the cluster API, not a separate control plane. For the operator's
trust boundaries and the host-access trust gates, see the
[security model](security.md).
100 changes: 100 additions & 0 deletions docs/concepts/security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Security model

YACD is a development environment manager, and two facts shape its threat model:
wallet keys are real signing credentials, and the operator runs with cluster-wide
reach. This page explains the deliberate security decisions that follow: how
wallet keys are stored and signed with, what the cluster-scoped manager RBAC
means on a shared cluster, and how the chart's optional image-verification path
hardens the supply chain. For the concrete knobs, follow the links to the how-to
and reference pages.

## Wallet key custody

Funding a wallet means signing a real transaction, so wallet keys are credentials
worth protecting. YACD keeps custody simple and Kubernetes-native.

- **Keys live in labeled Kubernetes Secrets.** Each managed wallet is a
`<network>-wallet-<name>` Secret in the network's namespace, holding the payment
signing key, verification key, and address. The genesis-funded `faucet` wallet
has the same shape (`<network>-wallet-faucet`) and is created and owned by the
operator. Storing keys as Secrets means they inherit Kubernetes RBAC,
encryption-at-rest, and backup rather than living in a bespoke store.
- **The CLI signs locally; the cluster never signs for you.** `yacd wallet` reads
the source wallet's signing key, builds and signs the funding transaction on
your machine, and submits it over Ogmios. There is no server-side signing
endpoint and no faucet HTTP service to authenticate against, so no long-lived
spending credential is exposed inside the cluster.
- **`export` writes keys deliberately.** `yacd wallet export` is the only path
that puts raw key material on local disk; it writes `0600` files under a `0700`
directory and never prints keys to stdout.

Because there is no in-cluster faucet service, there is no Bearer token to
distribute and no token-to-URL trust gate to reason about. Keys stay in Secrets,
and signing stays on the host.

## Cluster-scoped manager RBAC

The chart binds the manager's ServiceAccount to a `ClusterRole`, not a namespaced
`Role`. The grant is broad by design: the operator manages `CardanoNetwork` and
`CardanoDBSync` objects and reconciles them into the core Kubernetes workloads
they need.

The `ClusterRole` grants, across all namespaces:

- `configmaps` and `persistentvolumeclaims`: create, get, list, patch, update,
watch
- `pods`: get, list
- `secrets`: create, delete, get, list, patch, watch
- `services`: create, delete, get, list, patch, update, watch
- `deployments` (`apps`): create, get, list, patch, update, watch
- `cardanonetworks` and `cardanodbsyncs` (`yacd.meigma.io`): get, list, watch,
plus get, patch, update on their `/status` subresources

The implication for a **shared cluster** is direct: a YACD install can create,
read, and delete Secrets, Services, ConfigMaps, PVCs, and Deployments in any
namespace, because that is the scope the reconcilers operate at. The `secrets`
verbs include `create` and `delete` because wallet keys and other runtime
material are managed Secrets. Treat the operator as a cluster-wide actor when you
decide where to install it; an isolated development cluster is the intended home,
not a multi-tenant production cluster shared with untrusted workloads.

RBAC creation is on by default but optional (`rbac.create`), and the role and
binding names are templated so an operator can substitute externally managed
RBAC. See the [installation guide](../operator/installation.md) and the
[configuration reference](../reference/configuration.md) for those values.

## Supply-chain image verification

The release workflow attests the manager image and the Helm chart with
GitHub-native (Sigstore keyless) attestations. The chart ships an **opt-in**
Kyverno `ClusterPolicy` that verifies the manager image attestation at Pod
admission time, closing the gap between "an image is signed somewhere" and "only
verified images run in this cluster".

The policy is **disabled by default** (`kyverno.imageVerification.enabled:
false`) because it depends on a running [Kyverno](https://kyverno.io)
installation. When you enable it, Kyverno intercepts pod admission and rejects
images that do not carry a valid keyless Sigstore attestation matching the
configured attestor.

The attestor defaults encode trust in the `meigma/yacd` release pipeline rather
than in a long-lived key:

- **issuer** `https://token.actions.githubusercontent.com` — the OIDC identity
must come from GitHub Actions.
- **subjectRegExp** restricts the signing workflow to `meigma/yacd`'s
`release.yml` running on a `v<semver>` tag, so a signature produced by any
other workflow or fork does not satisfy the policy.
- **Rekor** at `https://rekor.sigstore.dev` — the transparency log that records
the keyless signing event.
- **attestation** of type `https://slsa.dev/provenance/v1` with build type
`https://actions.github.io/buildtypes/workflow/v1` — the policy checks SLSA
provenance, not just a bare signature.

Because the signing identity is keyless and tied to a specific workflow on a
release tag, there is no signing key to leak or rotate; trust flows from the
GitHub OIDC identity and the public transparency log. The issuer, subject
pattern, Rekor URL, validation failure action, and the set of image references
the policy applies to are all configurable under `kyverno.imageVerification.*`.
To enable and tune verification, see the [installation guide](../operator/installation.md);
for the full value surface, see the [configuration reference](../reference/configuration.md).
Loading
Loading