Skip to content

Web UI (kopia server)

Kopia ships a built-in web UI — an HTML view of a repository's snapshots, policies, sources, and tasks. Kopiur exposes it declaratively: set spec.server on a Repository (or ClusterRepository) and the operator runs kopia server start in a Deployment and puts a Service in front of it. There is no enabled bool — the presence of the spec.server block is what turns it on, and removing the block tears everything back down.

Kopiur creates the workload and the Service only. Routing the Service to the outside world (an Ingress/HTTPRoute) is yours to wire — see Exposing the Service.

When would you use this?

The UI is an interactive surface for a human. Reach for it when you want to:

  • Browse and verify snapshots, policies, and sources visually, without the kubectl plugin.
  • Restore ad hoc through the UI — pick a snapshot, mount it, pull a file.
  • Give an operator a point-and-click view of a repository's contents — ideally read-only, so browsing can't accidentally delete a backup.

You do not need it for normal operation. Scheduled backups, restores, and maintenance all run headless in short-lived mover Jobs — the UI is never on that path. Because it is a long-lived pod that holds the repository decryption key (see the warning below), only run it where you actually want interactive access, and tear it down when you're done.

The UI holds the decryption key

By default the UI is full read/write/delete, and the server pod always holds the repository decryption key — even in read-only mode. Setting readOnly blocks mutation but not reading: anyone who can reach the UI can still read and restore every backup. So treat exposing the UI exactly like exposing the repository itself: keep it ClusterIP (the default), put authentication in front of it, and restrict who can reach the Service with a NetworkPolicy.

The spec.server surface

Field Type Default What it does
auth externally-tagged enum (generate | secretRef | insecure) generate UI login. Omitted ⇒ operator-generated credentials. Never defaults to no-auth.
readOnly bool false Read-only UI — connect the repository read-only so the UI cannot create/delete/alter backups (browse + restore only). Forced on when the Repository has spec.mode: ReadOnly.
service.type enum(ClusterIP|NodePort|LoadBalancer) ClusterIP How the Service is exposed. Routing outside the cluster is your job.
service.port int 51515 Listen + Service port.
service.annotations map Applied to the Service — the seam for your ingress/LB controller.
resources ResourceRequirements Requests/limits for the server pod.
securityContext SecurityContext hardened default Override the default hardened container security context.
namespace string ClusterRepository only, required — which namespace the server objects land in (a cluster-scoped owner has no implicit namespace).

There is no enabled field: presence of spec.server is "on", absence is "off". See the full field reference.

How to deploy it

spec.server is just a field on a Repository, so it deploys like any other CRD edit — kubectl apply, or through GitOps (Flux/Argo). The smallest form adds the block to a repository and takes the safe defaults (operator-minted credentials, ClusterIP):

# Smallest web-UI form on a namespaced Repository (docs/server.md)
#
# Adding a `spec.server` block turns the kopia web UI on (there is no `enabled`
# bool — presence is "on", absence is "off"). This is the minimal form: take the
# safe defaults (operator-minted credentials, ClusterIP Service, port 51515).
#
# The Secret named here must already exist in this namespace (the repository's
# backend auth + encryption password). For a complete server example with
# HTTPRoute/NetworkPolicy templates see deploy/examples/25-repository-server-ui.yaml.
#
# Field shapes verified against crates/api: externally-tagged backend
# (`backend.s3`), and `server.auth.generate` (externally-tagged auth enum).
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
  name: nas-primary
  namespace: apps
spec:
  backend:
    s3:
      {
        bucket: my-backups,
        endpoint: s3.amazonaws.com,
        region: us-east-1,
        auth: { secretRef: { name: nas-primary-creds } },
      }
  encryption:
    passwordSecretRef: { name: nas-primary-creds, key: KOPIA_PASSWORD }
  server:
    auth: { generate: {} } # operator mints UI credentials into an owned Secret

Apply it like any other manifest — kubectl apply -f, or through GitOps.

What the operator creates for you

Once the repository is Ready, the controller materializes — all named <repo>-kopia-ui and labeled app.kubernetes.io/name=kopiur-server, app.kubernetes.io/instance=<repo>:

Object Name Purpose
Deployment <repo>-kopia-ui Runs kopia server start (1 replica, Recreate strategy, mover image, TCP readiness/liveness probes).
Service <repo>-kopia-ui Fronts the Deployment on the configured port.
ConfigMap <repo>-kopia-ui The server's work spec (which repo, port, auth mode).
Secret <repo>-kopia-ui-auth generate mode only — the minted UI credentials (username/password).
$ kubectl get deploy,svc,cm,secret -n apps \
    -l app.kubernetes.io/name=kopiur-server,app.kubernetes.io/instance=nas-primary

The controller manages Deployments/Services/ConfigMaps/Secrets for this feature. For a namespaced Repository the objects carry an ownerReference to the repository; a ClusterRepository cleans them up via a finalizer instead (see ClusterRepository).

The server needs features.kopiaUi.enabled in the chart

Writing the generated-auth Secret (and, for a ClusterRepository, the cross-namespace credentials mirror) needs cluster-wide secrets create/patch/deleteoff by default (least privilege). Set features.kopiaUi.enabled: true in the Helm chart. Without it the repository's .status surfaces an actionable 403 naming the flag. See Feature permissions.

The server runs without in-pod TLS

The operator starts kopia with --insecure — i.e. plain HTTP inside the pod. That is deliberate: TLS termination belongs at your ingress/load balancer, not in the server pod. The credentials still protect the UI; just don't expose the raw Service to an untrusted network without TLS in front.

Try it end-to-end

Turn on the UI from a clean slate and prove it answers — 200 with auth, 401 without — without leaving the cluster. One apply-ready bundle, deploy/examples/tryit/server-ui.yaml: the apps Namespace, the backend Secret, and a Repository nas-primary with the minimal spec.server block.

apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
  name: nas-primary
  namespace: apps
spec:
  backend:
    s3:
      bucket: my-kopia-bucket
      endpoint: s3.us-east-1.amazonaws.com
      region: us-east-1
      auth:
        secretRef:
          name: nas-primary-creds
  encryption:
    passwordSecretRef:
      name: nas-primary-creds
      key: KOPIA_PASSWORD
  create:
    enabled: true
  # ── Turn on the web UI ────────────────────────────────────────────────────────
  # Presence of `spec.server` is "on" (there is no `enabled` bool). The minimal
  # form takes the safe defaults: operator-minted credentials into an owned
  # Secret, a ClusterIP Service, port 51515.
  server:
    auth: { generate: {} }

1. Fill in the credentials (AWS_* + KOPIA_PASSWORD) in the secret section, then apply the bundle and wait for the repository to be Ready — the server objects materialize only once it is:

$ kubectl apply -f deploy/examples/tryit/server-ui.yaml
$ kubectl -n apps wait --for=condition=Ready repository/nas-primary --timeout=2m

2. Confirm the operator created the server objects (deep). All named nas-primary-kopia-ui* and labeled for the instance:

$ kubectl -n apps get deploy,svc,secret \
    -l app.kubernetes.io/name=kopiur-server,app.kubernetes.io/instance=nas-primary
NAME                                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nas-primary-kopia-ui  1/1     1            1           40s

NAME                          TYPE        CLUSTER-IP     PORT(S)     AGE
service/nas-primary-kopia-ui  ClusterIP   10.96.12.34    51515/TCP   40s

NAME                              TYPE     DATA   AGE
secret/nas-primary-kopia-ui-auth  Opaque   2      40s

A 1/1 Deployment, a Service on 51515, and the nas-primary-kopia-ui-auth Secret (keys username/password) means the UI is up.

3. Read the minted credentials from that Secret:

$ kubectl -n apps get secret nas-primary-kopia-ui-auth \
    -o jsonpath='{.data.username}' | base64 -d; echo
kopia
$ kubectl -n apps get secret nas-primary-kopia-ui-auth \
    -o jsonpath='{.data.password}' | base64 -d; echo
<illustrative — your minted password>

4. Prove the UI answers (deep). Port-forward the Service and curl it — 200 with the credentials, 401 without:

$ kubectl -n apps port-forward svc/nas-primary-kopia-ui 51515:51515 &

# with the credentials  200:
$ curl -su 'kopia:<password>' http://localhost:51515/ -o /dev/null -w '%{http_code}\n'
200

# without them  401 (the UI never defaults to no-auth):
$ curl -s http://localhost:51515/ -o /dev/null -w '%{http_code}\n'
401

The server speaks plain HTTP inside the pod (the operator starts kopia with --insecure); TLS belongs at your ingress/LB, never the raw Service.

Illustrative output

The CLUSTER-IP, the minted password, and the AGEs vary per run — the load-bearing facts are the 1/1 nas-primary-kopia-ui Deployment, port 51515, the nas-primary-kopia-ui-auth Secret, and 200/401.

Tear it down by removing the spec.server block and re-applying — the operator deletes the Deployment, Service, ConfigMap, and the generated Secret it owns:

$ kubectl -n apps patch repository nas-primary --type merge -p '{"spec":{"server":null}}'

Authentication

spec.server.auth is an externally-tagged enum — you set exactly one of three keys. It defaults to generate (never to no-auth).

Mode Shape When to use
generate (default) generate: { username? } Let the operator mint a random password. The simplest safe choice.
secretRef secretRef: { name, usernameKey, passwordKey } You manage the UI credentials yourself (e.g. a shared/SSO-fronted password).
insecure insecure: { acknowledgeInsecure: true } No login at all. A footgun; for throwaway/lab use only.

The operator creates a Secret <repo>-kopia-ui-auth once (keys username, password), pins its reference to status.server.generatedSecretRef, and never rotates it on later reconciles. The username defaults to kopia; set generate: { username: alice } to change it. Read the password with:

$ kubectl get secret nas-primary-kopia-ui-auth -n apps \
    -o jsonpath='{.data.password}' | base64 -d; echo

secretRef — bring your own credentials

Point at a Secret you own; all three keys are required:

server:
    auth:
        secretRef: { name: my-ui-creds, usernameKey: username, passwordKey: password }

insecure — no authentication

Disables the UI login entirely. It demands an explicit acknowledgement, so you can't reach it by accident:

server:
    auth:
        insecure: { acknowledgeInsecure: true } # required — the webhook rejects it otherwise

insecure exposes the whole repository with no login

With insecure, anyone who can reach the Service has full read/write/delete of every backup. The admission webhook rejects the mode unless you set acknowledgeInsecure: true. Only use it on an isolated network you fully trust, and pair it with a NetworkPolicy.

Read-only UI

Set spec.server.readOnly: true and the operator connects the server's repository read-only (kopia repository connect --readonly) before starting the UI. Every operation on that connection — and so everything the UI does — is then unable to mutate the repository: creating, deleting, or altering snapshots/policies/ maintenance is rejected. It's the right default for a point-and-click browse and restore surface where you never want a stray click to delete a backup.

server:
    readOnly: true # the UI cannot mutate the repository (browse + restore only)
    auth: { generate: {} }

The effective read-only state is spec.mode: ReadOnly OR spec.server.readOnly: true:

  • A Repository with spec.mode: ReadOnly already serves restores only — its UI is forced read-only and you don't need the field. Setting an explicit readOnly: false on such a repository is rejected by the webhook (a read-only repository can't serve a writable UI).
  • A normal ReadWrite repository (still taking backups via movers) gets a read-only UI by opting in with readOnly: true.

The reconciler pins the resolved value to status.server.readOnly. A complete, apply-ready read-only Repository (a ReadWrite repo with a read-only UI):

# Example 26 — A read-only Kopia web UI (`spec.server.readOnly`)
#
# Same as example 25, but the server connects the repository READ-ONLY: the
# operator runs `kopia repository connect --readonly` before `kopia server start`,
# so creating, deleting, or altering backups through the UI is rejected by the
# repository. This is the safe choice when you want humans to BROWSE and RESTORE
# through the UI but never mutate backups.
#
# ┌─ WHAT read-only DOES AND DOESN'T DO ───────────────────────────────────────┐
# │ DOES:    block create / delete / alter of snapshots, policies, maintenance  │
# │          through the UI (the repository connection rejects every write).    │
# │ DOESN'T: hide data — the server pod still holds the DECRYPTION KEY, so       │
# │          anyone who reaches the UI can still READ and RESTORE every backup.  │
# │          Read-only is not confidentiality: keep auth on and ClusterIP.      │
# │ NOTE:    kopia's UI does not grey out the (now non-functional) write/delete  │
# │          buttons; the actions simply fail at the backend.                   │
# └─────────────────────────────────────────────────────────────────────────────┘
#
# Effective read-only is `spec.mode: ReadOnly` OR `spec.server.readOnly: true`.
# A `Repository` with `spec.mode: ReadOnly` already forces the UI read-only (you
# don't need the field); use `server.readOnly: true` to get a read-only UI on a
# normal ReadWrite repository that still takes backups via movers. The effective
# value is pinned to `status.server.readOnly`.
#
# Verified against crates/api: `server.readOnly` is `Option<bool>` on ServerSpec.
---
apiVersion: v1
kind: Secret
metadata:
  name: nas-primary-creds
  namespace: apps
type: Opaque
stringData:
  AWS_ACCESS_KEY_ID: "REPLACE_ME"
  AWS_SECRET_ACCESS_KEY: "REPLACE_ME"
  KOPIA_PASSWORD: "choose-something-long-and-random"
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
  name: nas-primary
  namespace: apps
spec:
  # This repository still takes backups normally (mode defaults to ReadWrite);
  # only the *UI* is read-only, via server.readOnly below.
  backend:
    s3:
      bucket: my-backups
      endpoint: s3.us-east-1.amazonaws.com
      region: us-east-1
      auth:
        secretRef:
          name: nas-primary-creds
  encryption:
    passwordSecretRef:
      name: nas-primary-creds
      key: KOPIA_PASSWORD
  server:
    readOnly: true # the UI cannot mutate the repository (browse + restore only)
    auth:
      generate:
        username: kopia
    service:
      type: ClusterIP
      port: 51515
      annotations: {}

Read-only blocks mutation, not reading

readOnly stops the UI from changing backups; it does not make the UI confidential. The server pod still holds the repository decryption key, so anyone who can reach the UI can still read and restore every backup. Keep auth on and the Service ClusterIP regardless. Note too that kopia's UI does not grey out the (now non-functional) write/delete buttons — the actions simply fail at the backend.

Why a connection-level flag (not a server flag)

kopia 0.23 — the version Kopiur ships — has no kopia server start --readonly flag; that landed later upstream. Kopiur achieves the same guarantee with the read-only connection, whose read-only bit every later operation inherits. One side effect: kopia may log occasional errors if its internal scheduler probes for maintenance on a read-only connection. They're harmless (nothing can be written).

Exposing the Service

spec.server.service controls the Service; port defaults to 51515.

service.type Reach it from Notes
ClusterIP (default) inside the cluster Use kubectl port-forward or your own ingress. The safe default.
NodePort each node's IP A static high port on every node.
LoadBalancer an external IP Provisioned by your cloud/LB controller.

Kopiur creates the Service only — it never creates an Ingress or HTTPRoute. Point your own router at Service <repo>-kopia-ui on the configured port, and put TLS + (ideally) an additional auth layer there. The full example carries commented HTTPRoute and NetworkPolicy templates you can adapt. Use service.annotations to feed your ingress/LB controller (e.g. an external-dns hostname or an LB class).

Accessing the UI

For a quick look, port-forward the Service and open it locally:

$ kubectl port-forward -n apps svc/nas-primary-kopia-ui 51515:51515
# then browse http://localhost:51515 and log in with the credentials above

For ongoing access, route an Ingress/HTTPRoute to the Service (with TLS), and strongly consider a NetworkPolicy restricting who may reach it.

ClusterRepository server

A ClusterRepository is cluster-scoped and has no implicit namespace, so its spec.server block requires a namespace (the fields are otherwise identical, flattened in):

# Web UI on a cluster-scoped ClusterRepository (docs/server.md)
#
# A ClusterRepository has no implicit namespace, so its `spec.server` block
# REQUIRES `namespace` — which namespace the server Deployment/Service/ConfigMap
# (and any minted credential Secret) land in. The other server fields are
# identical to the namespaced Repository form, flattened in.
#
# Because a cluster-scoped object can't own namespaced children via an
# ownerReference, the controller tracks and cleans up the server objects with a
# finalizer + labels. If the repo credentials Secret lives in a different
# namespace than the server, the operator mirrors it next to the server pod
# (envFrom can't cross namespaces).
#
# Requires installScope=cluster. Field shapes verified against crates/api:
# externally-tagged `server.auth.generate`; `server.namespace` required here.
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: ClusterRepository
metadata:
  name: platform
spec:
  # backend / encryption / allowedNamespaces …
  backend:
    s3:
      bucket: org-kopia-repo
      endpoint: s3.us-east-1.amazonaws.com
      region: us-east-1
      auth:
        secretRef:
          name: platform-creds
          namespace: kopiur-system # explicit ns required for cluster scope
  encryption:
    passwordSecretRef:
      name: platform-creds
      namespace: kopiur-system
      key: KOPIA_PASSWORD
  allowedNamespaces:
    all: true
  server:
    namespace: kopiur-system # required: where the server objects land
    auth: { generate: {} }
    service: { type: ClusterIP, port: 51515 }

Because a cluster-scoped object can't own namespaced children via an ownerReference, the controller tracks and cleans up the server objects with a finalizer + labels instead. If the repository credentials Secret lives in a different namespace than the server, the operator mirrors it next to the server pod (envFrom can't cross namespaces). Changing server.namespace moves the server: the operator deletes the objects in the old namespace and recreates them in the new one (it tracks the last-applied namespace in status.server.namespace).

Filesystem backends require ReadWriteMany

For an object-store backend (S3, Azure, GCS, B2, …) the server connects over the network — no volume constraint. For a filesystem backend the server pod must mount the repository volume, and it is long-lived:

A filesystem-backed server needs a ReadWriteMany repo PVC

A long-lived server holding a ReadWriteOnce repo PVC would block every backup/restore/maintenance mover that needs the same volume. The operator therefore requires the repository PVC to be ReadWriteMany when spec.server is set on a filesystem Repository, and rejects the reconcile otherwise. Use an RWX-capable StorageClass (or an inline NFS export) for the repository volume, or keep the UI on an object-store repository.

Server permissions on an NFS-backed repo

Like a mover, the server pod mounts the filesystem repo read-write and writes to the backend — so it must be able to write the export. The server gets the same hardened pod defaults as movers (fsGroup: 65532), but fsGroup is a no-op on NFS. If the export is owned by a dedicated UID/GID, give the server the shared group via spec.server.podSecurityContext (mirrors moverDefaults.podSecurityContext):

spec:
  server:
    podSecurityContext:
      supplementalGroups: [3001] # the export's group; matches moverDefaults
  moverDefaults:
    podSecurityContext:
      supplementalGroups: [3001]

Without it, the server CrashLoops on startup (it can't read/write the repo). See Security context → NFS filesystem repositories. The container-level spec.server.securityContext overrides the hardened container context (e.g. runAsUser) independently.

Inspecting status

The reconciler pins a status.server block (it never stores a password):

$ kubectl get repository nas-primary -n apps -o jsonpath='{.status.server}' | jq
Field Meaning
endpoint In-cluster address, <service>.<namespace>.svc:<port>.
namespace Namespace the server objects were last applied to (used to detect a namespace change).
authMode Resolved auth discriminant — Generate / SecretRef / Insecure.
readOnly Effective read-only state — true when spec.mode: ReadOnly or spec.server.readOnly: true.
generatedSecretRef generate mode only — the operator-owned Secret holding the UI credentials.

When the server is disabled, status.server is cleared to null.

Disabling it

Delete the spec.server block from the Repository manifest and re-apply it. The operator deletes the Deployment, Service, ConfigMap, and any generated Secret it owns:

$ kubectl apply -f your-repository.yaml   # the manifest with the spec.server block removed

Quick imperative form

To tear it down without editing the manifest, patch spec.server to null directly:

$ kubectl patch repository nas-primary -n apps --type merge -p '{"spec":{"server":null}}'

Re-apply your manifest afterward so your source of truth (especially under GitOps) doesn't put it back.

Full example

A complete, apply-ready Repository with spec.server (S3 backend, generate auth, plus commented HTTPRoute + NetworkPolicy templates):

# Example 25 — Browse a repository with the Kopia web UI (`spec.server`)
#
# Setting `spec.server` on a Repository (or ClusterRepository) makes the operator
# run `kopia server start` in a Deployment and expose it via a Service, so you can
# browse snapshots/policies through Kopia's built-in HTML UI. Only a Service is
# created — wiring an Ingress/HTTPRoute is left to you (commented below).
#
# ┌─ SECURITY ─────────────────────────────────────────────────────────────────┐
# │ By default the UI is full read-write-DELETE. Anyone who can reach it can     │
# │ READ, CREATE, and DELETE backups, and the server pod holds the repository    │
# │ DECRYPTION KEY. Set `server.readOnly: true` (or `spec.mode: ReadOnly` on the │
# │ Repository) for a no-mutation UI — see example 26. Read-only still exposes    │
# │ READING all backups, so keep auth + ClusterIP regardless. Defaults here are  │
# │ the safe ones:                                                               │
# │   * `auth: { generate: {} }` — the operator mints random UI credentials into │
# │     an owned Secret (`<repo>-kopia-ui-auth`) and pins the ref to             │
# │     `status.server.generatedSecretRef`. Read them with:                      │
# │       kubectl -n apps get secret nas-primary-kopia-ui-auth \                 │
# │         -o jsonpath='{.data.password}' | base64 -d                           │
# │   * `service.type: ClusterIP` — not exposed outside the cluster by default.  │
# │ The no-auth `insecure` mode exists but requires an explicit                  │
# │ `acknowledgeInsecure: true` (webhook-rejected otherwise). Strongly consider  │
# │ a NetworkPolicy restricting who may reach the Service.                       │
# └─────────────────────────────────────────────────────────────────────────────┘
#
# Filesystem backends: a long-lived server holds the repo PVC, so it would block
# backup/restore movers on a ReadWriteOnce volume — the operator requires the repo
# PVC to be ReadWriteMany when `spec.server` is set on a filesystem Repository.
# Object-store backends (shown here) connect over the network and have no such
# constraint.
---
apiVersion: v1
kind: Secret
metadata:
  name: nas-primary-creds
  namespace: apps
type: Opaque
stringData:
  AWS_ACCESS_KEY_ID: "REPLACE_ME"
  AWS_SECRET_ACCESS_KEY: "REPLACE_ME"
  KOPIA_PASSWORD: "choose-something-long-and-random"
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
  name: nas-primary
  namespace: apps
spec:
  backend:
    s3:
      bucket: my-backups
      endpoint: s3.us-east-1.amazonaws.com
      region: us-east-1
      auth:
        secretRef:
          name: nas-primary-creds
  encryption:
    passwordSecretRef:
      name: nas-primary-creds
      key: KOPIA_PASSWORD
  # The web UI server. Presence of this block enables it.
  server:
    # Default (and recommended): operator-generated credentials.
    auth:
      generate:
        username: kopia # optional; defaults to "kopia"
    # Alternatives:
    #   auth: { secretRef: { name: my-ui-creds, usernameKey: username, passwordKey: password } }
    #   auth: { insecure: { acknowledgeInsecure: true } }   # NO auth — footgun
    # readOnly: true   # no-mutation UI (browse + restore only) — see example 26
    service:
      type: ClusterIP # ClusterIP (default) | NodePort | LoadBalancer
      port: 51515
      annotations: {} # the seam for your ingress/LB controller
    # resources: { requests: { cpu: 25m, memory: 64Mi } }
    # securityContext: {}
---
# Bring-your-own routing (uncommented as an illustration). Kopiur never creates an
# Ingress/HTTPRoute — point your own at the Service `nas-primary-kopia-ui:51515`.
#
# apiVersion: gateway.networking.k8s.io/v1
# kind: HTTPRoute
# metadata:
#   name: nas-primary-kopia-ui
#   namespace: apps
# spec:
#   parentRefs:
#     - name: internal
#       namespace: gateway
#   hostnames: ["kopia.example.internal"]
#   rules:
#     - backendRefs:
#         - name: nas-primary-kopia-ui
#           port: 51515
#
# And a NetworkPolicy is strongly recommended to restrict who can reach the Service:
#
# apiVersion: networking.k8s.io/v1
# kind: NetworkPolicy
# metadata:
#   name: nas-primary-kopia-ui
#   namespace: apps
# spec:
#   podSelector:
#     matchLabels:
#       app.kubernetes.io/name: kopiur-server
#       app.kubernetes.io/instance: nas-primary
#   ingress:
#     - from:
#         - namespaceSelector:
#             matchLabels: { kubernetes.io/metadata.name: gateway }

See also