기술 자료

Manifest reference

Reference for manifest.yaml, with an annotated example, per-chart value overrides, object storage, and a custom CA bundle
읽는 시간 22분최근 업데이트: 9시간 전

manifest.yaml
is the single input that drives every shdctl operation. Every command checks it against an embedded CUE schema, but only
shdctl manifest validate
rejects a key the schema does not define: every other command drops a misspelled key without a word and renders that field's default. Run
shdctl manifest validate
before
shdctl release generate
, as step 0 of the recommended pipeline does.
Commit it to version control. The manifest acts as a controlled input for release upgrades: bumping
releaseVersion
and any other field, then rerunning
shdctl release pull --clean-output
and
shdctl release generate
, produces a reviewable diff your team can approve before deployment, and ArgoCD or your CD picks it up from there.
The manifest contains no secrets. Secrets are generated separately by
shdctl secret generate
from
secrets.import.yaml
, which lives in your CI secret store or a vault, never in Git. If that file is ever lost,
shdctl secret export
rebuilds it from the secrets already in the cluster.

Initialize a manifest

If you don't already have a manifest, generate one interactively with
shdctl manifest init
.

Validate a manifest

Check a manifest against the embedded CUE schema with
shdctl manifest validate
. It is the only command that also rejects keys the schema does not define.

Annotated example

A Self-Hosted Deployment manifest with the most common fields looks like this. Keep
configuration.kubernetes.storage
and the
configuration.networking.ingress.traefik
block even though the schema marks them optional:
shdctl release generate
fails without them. A manifest written for an earlier release can also hold a
platform:
line: it's ignored, and shdctl warns about it until a future release rejects it, so delete it.
# manifest.yamlreleaseVersion: 2.0.0 # required: matches the release tag in Unity's registryartifactSync: # Set sourceRepository: `shdctl manifest schema` lists this value as its default, # but every `artifact sync` subcommand fails when the field is left out. sourceRepository: uccmpprivatecloud.azurecr.io concurrency: 5 # parallel `artifact sync` workers. Leave it out and # the sync runs one at a time (`verify`: four at a time)deployment: helm: concurrency: 4 # optional, default 1; deploy charts within the same wave # in parallel (helm format only). Waves remain sequential. # The `--concurrency` CLI flag overrides this when both are set. argocd: # defaults for `release generate --format argocd` repoURL: "git@github.com:your-org/your-argocd-charts.git" pathPrefix: "generated-charts" # the repo directory the rendered charts are committed # under, relative to its root. Keep it non-empty: an # empty prefix breaks every child application destinationServer: "https://kubernetes.default.svc" # required: generation fails without it targetRevision: "main" # the branch ArgoCD reads the charts fromconfiguration: # imageVariant: hardened # optional. When set, shdctl appends the matching # suffix (such as -hardened) to image versions that # declare the variant in versions.yaml. Omit for # default images. networking: appDomain: uam.example.com # the FQDN your users reach the app at # ipFamily: ipv6 # optional. "ipv4" (default) or "ipv6" for # single-stack IPv6 clusters. Omit for IPv4. # Optional. Reference an existing Kubernetes Secret (in the install namespace) # whose `ca-bundle.crt` key holds a PEM-encoded CA chain (root + intermediates). # Mounted into builtin workflow StorageTool containers so .NET trusts your # ingress TLS chain. Required only when your ingress uses a private CA not in # the default system trust store, such as an internal corporate or government PKI. # trustedCaSecretName: my-ca-bundle # IPv4 or IPv6 CIDRs allowed to reach the LoadBalancer. List your cluster's # own outbound (NAT egress) addresses alongside your users' and operators': # Unity Studio calls this deployment back at its public appDomain, so those # calls arrive from the cluster's egress address and are refused if it is # not here — Studio then fails with the rest of the deployment healthy. allowedIngressCIDRs: - "203.0.113.0/24" # users and operators - "198.51.100.10/32" # the cluster's own NAT egress address ingress: traefik: type: LoadBalancer tls: enabled: true certificate: traefik-tls-cert # name of the K8s Secret holding the cert # Optional. Pin specific nodePorts for traefik entrypoints. Omit (default) # to let Kubernetes auto-assign from the cluster's nodePort range — the # assigned port is stable for the life of the Service. Set explicit values # only when an external load balancer or firewall rule requires it. # nodePorts: # web: 32080 # HTTP entrypoint # websecure: 32443 # HTTPS entrypoint serviceMesh: istio: enabled: false kubernetes: namespace: asset-solutions # namespace where workloads are deployed dnsService: kube-dns # CoreDNS Service in kube-system, for example rke2-coredns-rke2-coredns on RKE2 docker: repository: registry.example.com # your registry (used after `artifact sync`), or # Unity's registry itself if you pull from it directly namespace: asset-solutions # subpath/namespace within your registry; the schema # refuses it when pulling from Unity's registry directly imagePullSecret: # K8s Secret holding registry pull credentials name: regcred # the older `imagePullSecret: regcred` spelling # still works, and logs a deprecation notice # generate: false # `secret generate` builds that Secret by # default; set false where something else in the # cluster creates and refreshes it, such as a # registry token refresher for short-lived tokens autoscaling: minReplicas: 1 # the platform services, Keycloak and Traefik: with 1, # a service is briefly down while its pod moves during a # node drain; 2 keeps them up through node maintenance maxReplicas: 10 storage: defaultStorageClass: gp3 # garage + license server only; other RWO volumes # use the cluster's default storage class, which # must exist readWriteManyStorageClass: efs # Optional. Annotations added to every pod the release creates. Values are # strings, so quote "true". To change or drop one for a single chart, use # that chart's overrides: at global.podAnnotations for the release's own # charts, at the chart's own pod-annotation key for third-party ones; null # removes it. # # Example: AKS with an HTTP proxy (httpProxyConfig) injects # HTTP_PROXY/HTTPS_PROXY/NO_PROXY into every pod, and platform services call # each other by short Service name, which no NO_PROXY entry matches — so # those calls go to the proxy and fail. This annotation makes AKS skip the # pod; nodes, and so image pulls, still use the proxy, but pods get no # internet access. The automation-manager job pulls the release manifest # from the registry itself, so give it the proxy (unless your registry is # reachable without it): # overrides: # automation-manager: # values: # env: # httpsProxy: http://<proxy>:<port> # noProxy: .svc,.cluster.local,localhost,127.0.0.1 # podAnnotations: # kubernetes.azure.com/no-http-proxy-vars: "true" transformations: parallelism: 30 # max concurrent transformation workflows monitoring: database: enabled: true prometheus: enabled: true # installs a Prometheus stack. If the cluster already # runs the Prometheus Operator, set enabled: false and # installedInCluster: true to add only ServiceMonitors logCollection: enabled: true # Loki + Alloy authentication: x509: # Mandatory mutual TLS (mTLS): every client reaching the deployment must # present a client certificate. Unity Studio's publisher has none, so # Studio publishing does not work while this is enabled in this release. enabled: false # caSecretName: x509-ca-cert # Secret holding the client CA; this is the default infrastructure: sizing: medium # small | medium (default) | large # singleNode: true # optional: collapse infrastructure components to # single-replica for ephemeral test clusters. # Not for production. # components: # per-component CPU/memory/storage overrides # postgresql: # storage: 400Gi # the data volume # backupStorage: 800Gi # the pgBackRest repository volume (backups + WAL). # # Default by sizing profile: 100Gi small, 800Gi # # medium, 1600Gi large. It can only grow, which # # needs allowVolumeExpansion on the StorageClass: # # a smaller value is refused. Ignored, with a # # warning, when objectStore.buckets.postgresBackups # # is set objectStore: provider: garage # "garage" (default) deploys the in-cluster # object store; "s3" uses a bucket you # own. See "Bringing your own object storage". # provider: s3 # endpoint: https://s3.us-east-2.amazonaws.com # scheme://host[:port], no path # region: us-east-2 # forcePathStyle: false # true for MinIO, Ceph RGW, StorageGRID # buckets: # transformationLogs: my-transformation-logs # transformation step logs # mongodbBackups: my-mongodb-backups # MongoDB nightly dumps # studioAssets: my-studio-assets # Unity Studio publications — needs a CORS rule + https # postgresBackups: my-postgres-backups # optional: PostgreSQL backups + WAL # overrides: # optional last-resort layer of extra Helm # <chart-name>: # values, per chart, applied after every layer # values: # the release ships. See "Per-chart Helm value # replicaCount: 3 # overrides" below before reaching for it.

Full schema reference

The annotated example above covers the most common fields. For the complete field list, including every type, default value, constraint, and cross-field rule, print the schema your installed shdctl is using with
shdctl manifest schema
. Three of the defaults it prints are not applied when you leave the field out:
artifactSync.sourceRepository
,
artifactSync.concurrency
, and
deployment.argocd.destinationServer
. The example above sets all three.

Auto-discovery

shdctl searches upward from the current working directory for
manifest.yaml
. Pass
--manifest <path>
to override: that file must exist and parse, or the command stops. The search is best-effort, so a command that needs no manifest runs without one.

Per-chart Helm value overrides

Every Helm value shdctl renders comes from the release package: a chart's own
values.yaml
, a platform layer, and the per-chart values the release's chart template sets.
configuration.overrides
adds one more layer on top of those, per chart, under your control:
configuration: overrides: keycloak: # must name a chart in the release (see below) values: replicaCount: 3
shdctl release generate
merges each block into that chart's generated
values.yaml
last, so it wins over every layer the release ships. One thing still happens after the merge: on an IPv6 manifest (
configuration.networking.ipFamily: ipv6
)
global.ipFamily
is set to
ipv6
whatever an override says — set the manifest field rather than overriding that key. Reach for it when a chart needs a setting the manifest does not expose — but treat it as a last resort rather than a configuration style. These values are yours to maintain: the manifest schema does not describe them, and a chart's value keys can move between releases. Tell Unity what you had to override, so the setting can become a supported manifest field.
What to know before you use it:
  • A chart name that isn't in the release fails the command. The definitive list is the directories under
    generated-charts/
    after a run. In the release package itself, valid names are the
    helm_charts
    keys of
    extracted-release/common/versions.yaml
    merged with the
    extracted-release/platform/shd/versions.yaml
    overlay — most charts are declared only in the common file, so don't judge a name by the overlay alone. A typo stops
    shdctl release generate
    with
    configuration.overrides: unknown chart "..."
    instead of silently doing nothing.
  • Only the block's shape is validated, not what's inside it.
    shdctl manifest validate
    confirms that
    overrides.<chart>.values
    is a mapping; it cannot know whether a key exists in the chart. Misspell a key under
    values:
    and Helm ignores it without complaint. Read the generated
    values.yaml
    — or its diff, if you commit
    generated-charts/
    to Git — before deploying, and re-check your overrides after every release upgrade.
  • Maps deep-merge, and keys you don't name are left alone.
    settings: {logLevel: debug}
    changes
    logLevel
    and keeps every sibling under
    settings
    .
  • null
    removes a key
    , whatever it holds — a scalar, a whole map, a whole array. It is dropped from the generated
    values.yaml
    . That file is the chart's root
    values.yaml
    , so the chart sees no value at all — unless the key belongs to a subchart, whose own default then takes over, since those
    values.yaml
    files ship alongside. (An empty map
    {}
    is not a removal: it merges into the existing map and changes nothing. An empty array
    []
    does replace the array.)
  • Listing an array replaces the whole array. To change one element instead, write a mapping where the array sits, keyed by the element's name:
    configuration: overrides: elasticsearch-eck: values: nodeSets: master-nodes: podTemplate: spec: containers: elasticsearch: resources: limits: cpu: "2"
    The names depend on your topology — a single-node deployment has one nodeSet named
    single-node
    , not
    master-nodes
    — so read the names your release actually produced from the generated
    values.yaml
    . A key matches the element whose
    name
    it equals, or whose
    metadata.name
    it equals for elements shaped like Kubernetes objects; for an array whose elements have no name, use the index. An element that is a plain value — a string or a number — can be removed by index but not changed; to change one, replace the whole array. Only a mapping patches an array: a list written where the array sits replaces it outright, as above, and a scalar is ignored — leaving the array as the release shipped it. Elements you do not name are untouched, order is preserved, and arrays nested inside an element are patched the same way rather than replaced — so changing one field of a container or a volume claim does not mean restating it. A key naming an element the array does not contain fails
    shdctl release generate
    , and the error lists the names it found. To remove an element, set it to
    null
    ; to add one, replace the whole array. A
    null
    naming an element the array does not contain fails the same way, so a removal does not quietly become a no-op once a later release drops the element itself.
    That failure needs the array to still be there. If a release removes or renames the array itself, there is nothing left for your mapping to patch — the key is simply not in the release's values — so the mapping is written into the generated
    values.yaml
    as an ordinary map and Helm ignores it in silence. Nothing errors, because the syntax cannot tell a patch that an upgrade left stale from a map the chart genuinely reads: one more reason to read that file and re-check your overrides after every release upgrade.
  • The
    _arrayMerge
    key is not accepted.
    Earlier versions documented it for merging lists; under
    values:
    , it now stops
    shdctl release generate
    . Write the mapping form above instead.

Bringing your own object storage

The deployment needs S3-compatible object storage for three things: the logs of each transformation step, the nightly MongoDB backups, and the publications Unity Studio writes. By default it deploys an in-cluster store for all three, which needs no configuration and no outbound access — this is the only option on a cluster that cannot reach anything outside itself. PostgreSQL's backups are a fourth, optional consumer: they go to a PersistentVolume unless you name a bucket for them.
configuration.objectStore.provider: s3
uses buckets you own instead. The in-cluster store is then not deployed at all.
configuration: objectStore: provider: s3 endpoint: https://s3.us-east-2.amazonaws.com region: us-east-2 forcePathStyle: false buckets: transformationLogs: my-transformation-logs mongodbBackups: my-mongodb-backups studioAssets: my-studio-assets postgresBackups: my-postgres-backups

Field

Required on
s3

What it is

provider
—
garage
(default) for the in-cluster store,
s3
for your own.
endpoint
yes
scheme://host[:port]
, nothing more. See the constraints below.
region
yesThe region string the store expects — for AWS, the buckets' region. Stores that ignore it still need a non-empty value.
forcePathStyle
no (default
false
)
true
for stores that address buckets by path rather than by hostname — MinIO, Ceph RGW, StorageGRID. Only partly wired in this release; see the limitation below.
buckets.transformationLogs
yesBucket for the per-step logs a transformation writes. Not application logs.
buckets.mongodbBackups
yesBucket for the nightly MongoDB dumps.
buckets.studioAssets
yesBucket for the publications Unity Studio writes. The only one a browser reaches directly, so the only one needing a CORS rule, and it needs an
https
endpoint — see below.
buckets.postgresBackups
noBucket for the pgBackRest repository — PostgreSQL full backups and archived WAL. Naming it moves that repository off its PersistentVolume, which is then no longer provisioned. Requires an
https
endpoint.
Each key names one dataset with one producer, rather than a shared pool — so a bucket you point at
transformationLogs
holds nothing else, and a later dataset would arrive as a key of its own rather than moving into these.
postgresBackups
is the one bucket key that is optional, because pgBackRest cannot use an S3 repository over plain HTTP: a store reachable only over
http
has to be able to adopt this provider for step logs and MongoDB backups while PostgreSQL stays on its volume. Naming it with an
http
endpoint fails generation rather than silently leaving the bucket unused. Switching an existing deployment does not copy the volume's backup history — a fresh chain starts in the bucket — and with no local repository, WAL archiving depends on the store being reachable; pgBackRest spools WAL on the data volume meanwhile.
Create the buckets before you deploy, together with one access key pair that can read, write and delete objects in every one you name — backup retention deletes the dumps it ages out, and pgBackRest deletes the backups and WAL it expires.
shdctl secret generate
prompts for that access key ID and secret access key on this provider rather than generating them: a generated key could never authenticate against your store. A key scoped to only some of the buckets passes
secret generate
cleanly and then fails much later, the first time a backup or a transformation runs. A
--use-defaults
run with no
--import
file leaves the pair at the
TBD
placeholder and only warns, so that snapshot must not be deployed as-is.
buckets.studioAssets
needs a CORS rule, and no other bucket here does.
Unity Studio uploads and downloads publications straight from that bucket over presigned URLs, so the browser makes a cross-origin request from your Asset Manager domain to your object store. Allow origin
https://<your appDomain>
— the
configuration.networking.appDomain
from your manifest — with methods
GET
,
PUT
and
HEAD
, all request headers (
*
), and expose the
ETag
header. All four parts matter: a rule that allows no request headers refuses the preflight for an upload carrying
Content-Type
, which fails the same way as having no rule at all. Nothing in shdctl can check this for you, and the failure is invisible server-side: the publisher returns 201 and logs nothing while the browser is blocked, so a green deployment is not evidence the rule is right. Publish an asset and confirm it comes back.
buckets.studioAssets
also needs an
https
endpoint.
The browser follows the presigned URL itself, so an
http://
endpoint makes it an unencrypted request from a page served over
https://<your appDomain>
— mixed content, blocked by every current browser. shdctl does not refuse the combination the way it refuses
http
with
buckets.postgresBackups
, so it fails only in the browser, with a correct CORS rule in place. A store reachable only over
http
cannot serve Studio publishing in this release; the other buckets are unaffected.
Endpoint constraints. The endpoint is
scheme://host[:port]
and nothing else: no path, no query string, no fragment, and no
user:pass@
. All of those, and a missing scheme, fail
shdctl manifest validate
— the earliest check — and are refused again by
shdctl release generate
, which is the gate that still applies if you skip validation. They are refused rather than accepted-and-dropped: the component that writes step logs takes a host and port with no scheme and no path, so an endpoint with a path prefix would configure backups correctly and silently break step-log uploads, and an endpoint with credentials in it would put them in the values files you commit to Git. (The component that reads step logs back does take a full URL — the two differ, which is why one endpoint has to satisfy the stricter of them.) An IPv6 literal is supported, written bracketed:
http://[fd00::1]:9000
.
http
means the connection is not encrypted; use it only on a network you trust.
Limitations to check before you commit to this.
  • The endpoint must present a publicly trusted TLS certificate, or be reached over
    http
    . An endpoint whose certificate comes from a private CA — an on-premises appliance with an internal PKI, typically — is not supported in this release.
    configuration.networking.trustedCaSecretName
    covers your ingress, not this endpoint. That
    http
    fallback does not extend to
    buckets.postgresBackups
    or
    buckets.studioAssets
    .
    postgresBackups
    rejects
    http
    outright;
    studioAssets
    is not rejected but does not work over it, because the browser refuses the mixed content (above). So a store fronted by a privately-issued certificate has no supported path to PostgreSQL backups or to Studio publishing in this release. Leave
    postgresBackups
    out and PostgreSQL keeps backing up to its volume; Studio publishing has no equivalent fallback.
  • A store that requires path-style bucket addressing is only partly supported.
    forcePathStyle: true
    reaches the MongoDB backup, pgBackRest and Unity Studio clients; the transformation step-log clients have no path-style setting yet. So if you are adopting this on MinIO, Ceph RGW or StorageGRID, verify that a transformation's step logs are readable after switching — MongoDB backups succeeding is not evidence that they are — and tell Unity if they are not. On AWS S3, or any store serving virtual-hosted-style requests, leave it
    false
    and this does not apply.
  • One provider and one credential pair covers every bucket; per-bucket endpoints or a per-purpose provider choice are not supported.
Leaving
objectStore
out entirely, or setting
provider: garage
, is the in-cluster store and needs nothing else in the block. If you fill in
endpoint
,
region
,
forcePathStyle
or
buckets
but leave
provider
at
garage
, shdctl warns that they are being ignored rather than quietly deploying the in-cluster store; setting
configuration.infrastructure.components.garage
under
provider: s3
warns the same way, since there is no in-cluster store to size.
buckets.postgresBackups
is the one exception to that warning. Naming it while
provider
is
garage
fails
shdctl release generate
rather than warning: a warning would leave you believing PostgreSQL's backups are off-cluster when they are in fact still on their PersistentVolume, which is the one misreading of this block that costs you a backup you thought you had. So moving a deployment back to the in-cluster store means removing
postgresBackups
in the same edit that sets
provider: garage
.

Switching an existing deployment to your own buckets

Transformation step logs need nothing: they are transient and continuously garbage-collected. The MongoDB backup history and the Unity Studio publications do not move on their own. Copy both first, or the old dumps and every publication your users made go away with the in-cluster store: Studio publications are user data, and nothing else holds a copy. Replace
<namespace>
with your deployment's namespace, and run every step from one shell on a workstation that can reach the cluster.
Naming
buckets.postgresBackups
doesn't move PostgreSQL's backup history either: PostgreSQL starts a fresh backup chain in the bucket. Take a manual backup first if you need the old chain, and keep the backup volume until you're satisfied with the new one.
  1. Copy the backup history and the Studio publications with the MinIO client,
    mc
    . The deployment keeps running meanwhile:
    # the in-cluster store's own keys, read out of the clusterexport INCLUSTER_KEY=$(kubectl -n <namespace> get secret s3-api-storage-credentials -o jsonpath='{.data.GARAGE_ACCESS_KEY}' | base64 -d)export INCLUSTER_SECRET=$(kubectl -n <namespace> get secret s3-api-storage-credentials -o jsonpath='{.data.GARAGE_SECRET_KEY}' | base64 -d)# the key pair you created for your own bucketsexport MANAGED_KEY=<your-access-key-id>export MANAGED_SECRET=<your-secret-access-key>kubectl -n <namespace> port-forward svc/garage 3900:3900 &mc alias set incluster http://localhost:3900 "$INCLUSTER_KEY" "$INCLUSTER_SECRET"# your manifest's configuration.objectStore.endpoint; add --path on if it sets forcePathStyle: truemc alias set managed <your-object-store-endpoint> "$MANAGED_KEY" "$MANAGED_SECRET"mc mirror incluster/psmdb-backups managed/<your-backups-bucket>mc mirror incluster/studio-assets managed/<your-studio-assets-bucket>
  2. Right before you switch, copy again in a quiet window: with no MongoDB backup due, and nobody publishing from Studio. Anything written to the in-cluster store after this copy is not carried over.
    mc mirror
    copies only what changed since the first run, so this one is short. Then check that the counts match:
    mc mirror incluster/psmdb-backups managed/<your-backups-bucket>mc mirror incluster/studio-assets managed/<your-studio-assets-bucket>mc ls --recursive incluster/psmdb-backups | wc -lmc ls --recursive managed/<your-backups-bucket> | wc -lmc ls --recursive incluster/studio-assets | wc -lmc ls --recursive managed/<your-studio-assets-bucket> | wc -l
  3. Set
    provider: s3
    in your manifest, then run
    shdctl secret generate
    again, supplying your access key, and
    shdctl secret deploy
    , followed by
    shdctl release generate
    and
    shdctl release deploy
    . Between the secret deployment and the release deployment, step-log uploads and MongoDB backups fail: the running services hold your new key but still point at the in-cluster store. That is expected; keep the gap short, and both recover once the charts are deployed.
  4. With ArgoCD, the in-cluster store's application disappears from the release and is pruned on the next sync. With Helm, nothing removes it, and
    shdctl release uninstall
    skips it too: it keeps running, but nothing writes to it anymore. Run the two
    mc mirror
    commands once more, to pick up anything written during the switch, then remove it yourself with
    helm -n <namespace> uninstall garage
    . Either way, stop the port-forward with
    kill %1
    .
  5. The in-cluster store's PersistentVolumeClaims survive the switch, and they are the only remaining copy of anything you did not mirror. Keep them until you have seen a fresh MongoDB backup complete, a transformation run end to end, and a Studio publication appear in your own bucket. Then delete them:
    kubectl -n <namespace> get pvc | grep garagekubectl -n <namespace> get pvc -o name | grep garage | xargs -r kubectl -n <namespace> delete

Custom CA bundle for a privately-signed ingress

If your Asset Manager ingress uses a TLS certificate signed by a CA that is not in the default container trust store (for example a government or internal corporate PKI), the .NET StorageTool containers in the built-in Argo workflows will fail to validate downloads from your own ingress with
PartialChain
.
To fix this, create a Kubernetes Secret in the install namespace holding your CA chain, then reference it from your manifest:
kubectl -n <install-namespace> create secret generic my-ca-bundle --from-file=ca-bundle.crt=/path/to/your-ca-chain.pem
configuration: networking: trustedCaSecretName: my-ca-bundle
Requirements:
  • Single key inside the secret:
    ca-bundle.crt
    .
  • Value: PEM-encoded concatenation of the root and intermediate CAs needed to validate your ingress.
  • Plain
    BEGIN CERTIFICATE
    blocks only —
    BEGIN TRUSTED CERTIFICATE
    (OpenSSL trust-extended) is filtered out by .NET and won't be loaded.
  • Kubernetes Secret size limit is 1 MiB.
You may use any mechanism to populate the secret:
kubectl
, External Secrets Operator, sealed-secrets, etc. shdctl does not create or read it.
The same bundle is mounted into the Unity Studio publisher, which also calls your ingress, so a privately-issued ingress certificate needs nothing extra for Studio. It does not cover
configuration.authentication.x509.enabled: true
, which is a separate setting: that one turns on mutual TLS (mTLS), requiring every client reaching your deployment to present a certificate of its own, and Studio's publisher has none — so Studio publishing does not work while x509 client authentication is enabled in this release.