# shdctl architecture and security

> Learn about shdctl trust boundaries, three usage modes, and what shdctl touches on the network and in your cluster

This information helps security teams review shdctl. It describes what shdctl touches on the network, in your cluster, and on disk, and the three ways to use it. Each mode has different trust-boundary implications, so pick the one that matches your security posture.

## What shdctl does (full capability map)

shdctl consists of five phases. Only **Render** (`release generate`) is required; every other phase is optional and you can replace it with your existing tooling. There are also two optional steps outside the phases: `cluster check`, which verifies your cluster meets the deployment prerequisites, and `secret export`, which rebuilds your secrets import file from a running cluster when the original is lost.

The three usage modes that follow are common combinations of these phases.

```mermaid
flowchart LR
  subgraph Setup[1. Setup]
    Manifest[manifest.yaml<br/>shdctl manifest init/validate]
  end
  subgraph Validate[Check cluster — optional]
    CheckCmd[shdctl cluster check<br/>read-only prereq validation]
  end
  subgraph Pull[2. Pull release]
    PullCmd[shdctl release pull<br/>credentials from shdctl configure]
  end
  subgraph Mirror[3. Mirror — optional]
    SyncCmd[shdctl artifact sync<br/>preflight/images/oras/verify]
  end
  subgraph Render[4. Render — required]
    GenCmd[shdctl release generate]
    SecGen[shdctl secret generate]
  end
  subgraph Apply[5. Apply — optional]
    SecDep[shdctl secret deploy]
    RelDep[shdctl release deploy<br/>--format helm or argocd]
    RelUn[shdctl release uninstall<br/>Helm releases only]
  end
  subgraph Recover[Recover — optional]
    SecExp[shdctl secret export<br/>reads secret values]
  end

  Manifest --> CheckCmd
  CheckCmd --> K8s[Customer K8s cluster]
  ACR[Unity ACR] --> PullCmd
  PullCmd --> ER[extracted-release/]
  ACR --> SyncCmd
  ER --> SyncCmd
  SyncCmd --> CR[Customer registry]
  Manifest --> GenCmd
  ER --> GenCmd
  GenCmd --> Charts[generated-charts/]
  Manifest --> SecGen
  ER --> SecGen
  SI[secrets.import.yaml] --> SecGen
  SecGen --> Secrets[secrets.yaml]
  Secrets --> SecDep
  Charts --> RelDep
  Charts --> RelUn
  SecDep --> K8s
  RelDep --> K8s
  RelUn --> K8s
  K8s --> SecExp
  SecExp --> SI
```

You can run `cluster check` at any point after the Setup phase. It reads from the cluster and doesn't change it, unless you pass `--probe-storage`, which creates and deletes temporary test PVCs. For more information, refer to [shdctl cluster command](./commands/cluster.md).

The following are the three usage modes:

* Mode A (recommended) uses every phase.
* Mode B is Mode A with the Mirror phase made explicit for air-gapped deployments.
* Mode C stops after Render; shdctl hands its outputs to the CD tooling you already run.

## Mode A — Recommended (CI)

Mode A runs shdctl only in your CI pipeline. It has two variants, depending on what performs the actual workload deployment:

* **Mode A (ArgoCD variant) — recommended.** shdctl renders ArgoCD app-of-apps charts, your pipeline commits them to Git, and shdctl applies a single bootstrap `Application` to the cluster. ArgoCD takes over from there. shdctl never holds a cluster-wide deployment credential — ArgoCD does.
* **Mode A (Helm variant).** shdctl renders Helm charts and runs `helm upgrade --install` directly for every chart in the release. There's no Git commit and no ArgoCD. The CI runner needs a cluster-wide kubeconfig, which is a weaker trust boundary. Use this variant only if you don't run ArgoCD.

### Mode A (ArgoCD variant)

Your `manifest.yaml` lives in your Git repo, typically the same repo ArgoCD reads from. The pipeline does the following:

* Checks out the manifest
* Validates the manifest and checks the cluster
* Pulls the release from the Unity registry
* Optionally mirrors artifacts to your registry
* Renders charts and Secret manifests
* Commits the rendered charts to Git
* Applies the Secrets and a single bootstrap ArgoCD `Application` to the cluster

From that point on, ArgoCD pulls charts from your Git repo and deploys workloads. The step numbers below are those of the [pipeline steps](./workflows.md#pipeline-steps).

```mermaid
flowchart LR
  subgraph CI[CI runner]
    direction TB
    V[shdctl]
    SI[secrets.import.yaml<br/>from CI secret store]
    KC[kubeconfig<br/>namespace-scoped RBAC<br/>+ cluster reads for step 0]
  end
  subgraph Customer
    Git[Customer Git repo<br/>manifest.yaml +<br/>generated-charts/]
    Argo[ArgoCD]
    K8s[K8s cluster]
    CR[Customer registry<br/>airgapped only]
  end
  subgraph Unity
    ACR[uccmpprivatecloud.azurecr.io]
  end

  Git -- checkout manifest.yaml --> V
  V -- "0. cluster check, read-only (KC)" --> K8s
  V -- 1. release pull --> ACR
  V -- 2. artifact sync (airgapped) --> CR
  V -- "3. release generate --format argocd" --> Charts[generated-charts/]
  V -- 4. secret generate (reads SI) --> Secrets[secrets.yaml]
  Charts -- 5. pipeline commits + pushes --> Git
  V -- 6. secret deploy (KC) --> K8s
  V -- "7. release deploy --format argocd (KC)" --> K8s
  Git -- 8. sync --> Argo
  Argo -- 9. apply workloads --> K8s
  K8s -- pulls images --> CR
```

Apart from the read-only `cluster check`, shdctl's cluster touch is limited to two namespace-scoped applies: the Secrets and the bootstrap `Application`. All workload deployment after step 7 in the diagram is ArgoCD pulling from Git. ArgoCD holds the cluster-wide deployment credential, not the CI runner.

### Mode A (Helm variant)

If you don't run ArgoCD, shdctl can deploy directly with Helm. Steps 5, 8, and 9 in the diagram from the ArgoCD variant don't apply because there's no Git commit and no ArgoCD reconcile. Step 3 drops `--format argocd`, and step 7 becomes `release deploy --format helm`, which runs `helm upgrade --install` for every chart in the release.

```mermaid
flowchart LR
  subgraph CI[CI runner]
    direction TB
    V[shdctl]
    SI[secrets.import.yaml<br/>from CI secret store]
    KC[kubeconfig<br/>cluster-wide deploy rights]
  end
  subgraph Customer
    Git[Customer Git repo<br/>manifest.yaml]
    K8s[K8s cluster]
    CR[Customer registry<br/>airgapped only]
  end
  subgraph Unity
    ACR[uccmpprivatecloud.azurecr.io]
  end

  Git -- checkout manifest.yaml --> V
  V -- "0. cluster check, read-only (KC)" --> K8s
  V -- 1. release pull --> ACR
  V -- 2. artifact sync (airgapped) --> CR
  V -- 3. release generate --> Charts[generated-charts/]
  V -- 4. secret generate (reads SI) --> Secrets[secrets.yaml]
  V -- 6. secret deploy (KC) --> K8s
  V -- "7. release deploy --format helm (KC)" --> K8s
  K8s -- pulls images --> CR
```

shdctl runs every chart's `helm upgrade --install` from CI. The CI runner kubeconfig must have rights to deploy every chart in the release — cluster-wide, not namespace-scoped. ArgoCD is recommended because it keeps that credential out of CI.

## Mode B — Air-gapped mirror

A bastion or staging host runs shdctl once to mirror Unity release artifacts into your private registry. After the mirror completes, the production cluster, ArgoCD, and your Git repository never reach the public internet. Image references in the rendered charts are rewritten to your registry at generate time.

```mermaid
flowchart LR
  Bastion[Bastion / staging host<br/>shdctl] -- pull (one-time) --> ACR[Unity ACR]
  Bastion -- artifact sync images/oras --> CR[Customer registry]
  Bastion -- release generate --> Charts[generated-charts/<br/>image refs rewritten to CR]
  Charts --> Git[Customer Git] --> Argo[ArgoCD] --> K8s[Customer cluster]
  K8s -- pulls images --> CR
```

Only the bastion contacts the Unity registry, and only at pull and sync time. After the sync completes, the deployment is fully internal. Same `--format helm | argocd` choice as Mode A; ArgoCD recommended. A Helm deploy stays internal only when the host running it has no Helm chart repository registered; refer to [What shdctl touches](#what-shdctl-touches).

**The mirror is scoped to your manifest.** `shdctl artifact sync images` copies the images your manifest's enabled features require, not the release's full image set — a deployment with Istio or monitoring turned off does not carry those images into your registry. Two consequences on an air-gapped bastion, where a missing image cannot be fetched at deploy time:

* **Changing a feature toggle means re-running `shdctl artifact sync images`** before you deploy. The images that feature needs were never mirrored.
* **`shdctl artifact sync verify` reports what a registry is missing** for a given manifest, and exits non-zero when anything required is absent — so a pipeline can gate a deploy on it. Run it after a sync, and again after any manifest change, before you deploy.

Pass `--all` to `shdctl artifact sync images` to mirror the full image set instead: appropriate for a registry shared by deployments with different feature sets, or where a later feature change must not require another trip to Unity's registry.

## Mode C — Generate-only (bring your own CD)

shdctl renders Helm chart values and Kubernetes `Secret` manifests from your `manifest.yaml` and the extracted release package, and then stops. Whatever CD tooling you already use, such as `helm`, `kubectl`, Flux, or custom pipelines, can apply `generated-charts/` and `secrets.yaml`. shdctl never holds a kubeconfig in this mode and never reaches your cluster. This is the minimum viable use of shdctl.

```mermaid
flowchart LR
  M[manifest.yaml] --> V[shdctl]
  R[release package<br/>extracted-release/] --> V
  V -- release generate --> Charts[generated-charts/]
  V -- secret generate --> Secrets[secrets.yaml]
  Charts --> CD[Your CD tool<br/>helm / kubectl / Flux /<br/>custom pipeline]
  Secrets --> CD
  CD --> K8s[Customer cluster]
```

If you want shdctl to run the Helm deploys itself with `shdctl release deploy --format helm`, that isn't Mode C — it's the [Mode A (Helm variant)](#mode-a-\(helm-variant\)) flow described previously, because shdctl then needs a kubeconfig with cluster-wide deploy rights. [Direct Helm deploy](./workflows.md#direct-helm-deploy) shows the commands; for the deploy flags, refer to the [release command reference](./commands/release.md#deploy-with-helm).

## What shdctl touches

* **Outbound network**:
  * Unity registry (`uccmpprivatecloud.azurecr.io`) — required for `release pull` and the source side of `artifact sync`.
  * Your registry — required for the target side of `artifact sync`. `artifact sync verify` reads it only: each release manifest the release lists, then one image-manifest lookup per image reference, writing and deleting nothing.
  * Helm chart repositories — only for `release deploy --format helm`, and only the repositories registered on the host that runs it (`helm repo list`). Updating each chart's dependencies makes Helm refresh every registered repository, and with `--concurrency` above 1 shdctl runs `helm repo update` up front. The release's charts resolve their dependencies from the package itself, so a host with no repository registered makes no such call.
  * Kubernetes API — used only for `secret deploy`, `release deploy`, `release uninstall`, `cluster check`, and `secret export`.
  * `shdctl secret export`: reads secret **values** — `kubectl get secret` across the target namespace, to rebuild the import file from what is deployed. It is the only command that copies secret values out of your cluster into a file. It needs a kubeconfig with the **`list`** verb on secrets in that namespace — an unnamed collection read is a `list`, not a `get`, so a Role granting only `get` is refused with `cannot list resource "secrets"`. Either way it is more than the `get`/`create`/`patch` a deployment pipeline needs, so run it as an operator rather than widening a CI role for it. It writes nothing to the cluster, and `--dry-run` prints the command without contacting it.
  * `shdctl cluster check`: read-only cluster access, unless you pass `--probe-storage`. It runs `kubectl version` for the server version, and `kubectl get` against nodes, storage classes, namespaces, API services, custom resource definitions, and a cluster-wide pod list (`-A`). The pod list confirms that the ArgoCD controller is running, and the check is reported as skipped if your credentials don't allow it. The command also attempts to read Karpenter node pools (`nodepools.karpenter.sh`), but only when a node pool has no running node, and it tolerates a failure such as a missing custom resource definition or denied access, because Karpenter is optional. With `--probe-storage` it additionally creates, waits on and deletes a labeled 1Gi test PVC in the target namespace for each class it probes — the cluster default, `defaultStorageClass` when that is a different class, and `readWriteManyStorageClass` — and, only on a probe bind failure, reads events in that namespace for diagnostics. A probe that binds provisions a real volume, which a class with `reclaimPolicy: Retain` keeps after the PVC is gone. With `--dry-run` it touches nothing and prints every call the checks would make. For more information, refer to [shdctl cluster command](./commands/cluster.md).
* **Credentials read**:
  * Unity registry credentials, for `release pull`: `~/.shdctl/config.json`, which `shdctl configure` writes. The file is readable only by your user but not encrypted. A `~/.vpctl/config.json` left by vpctl, the tool's name before 1.0.0, is also read, never written — and `shdctl configure delete` leaves it alone, so delete it yourself once its credentials have moved.
  * Docker credential store (`~/.docker/config.json`) for `artifact sync`: the `docker` CLI uses it for images, `oras-go` for ORAS artifacts and the release manifests that `sync images` reads from the source and `verify` from the target.
  * kubeconfig (default discovery and the current context; `secret deploy` and `secret export` take `--context` to name another — for every other command, choose it with `KUBECONFIG` or `kubectl config use-context`).
* **Files written** (inside the working directory, unless you point an output flag elsewhere):
  * `unity-private-cloud-shd-<version>.tar.gz` — the release archive `release pull` downloads (`--output`), kept after extraction.
  * `extracted-release/` — release archive contents.
  * `generated-charts/` — rendered Helm charts and ArgoCD `Application` manifests.
  * `secrets.yaml` — rendered Kubernetes `Secret` manifests, including the image pull secret named by `configuration.kubernetes.imagePullSecret` unless that block sets `generate: false`. Written owner-readable only (`0600`), on first write and on every re-run.
  * The persist file — created only when you pass `--persist <path>` to `secret generate`, at the path you choose. Holds your secrets in the clear, owner-readable only (`0600`).
  * `secret export`'s output, at `--output` (default `secrets.import.yaml`). Holds your secrets in the clear, owner-readable only (`0600`); an existing file is never overwritten without `--force`.
  * `manifest.yaml` — only from `manifest init`, at `--output`.
  * The CUE schema — only from `manifest schema --export <path>`, at that path.
* **Outside the working directory** — output flags aside — shdctl writes only the credential file `shdctl configure` keeps under your home directory (above), plus two temporary items in the system temp directory, each removed when its command finishes: `release pull`'s download directory and `cluster check --probe-storage`'s PVC manifest. The tools it drives keep their own state as usual — Docker's local image store, Helm's cache.
* **What shdctl never does**: send telemetry, call back to Unity, auto-update.
