3
samba
Justin Klein edited this page 2026-10-14 14:57:00 +00:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

samba

Deploys Samba (image ghcr.io/dockur/samba) as a single-replica StatefulSet that exports one SMB share per entry in samba.shares, each on its own PVC. smb.conf is rendered by the chart into a ConfigMap; the user database comes from a 1Password-backed Secret. SMB is not HTTP, so the release is published through a NodePort Service and there is no Ingress and no certificate.

Image ghcr.io/dockur/samba
Versions not listed on purpose — read the current pair off samba/Chart.yaml, see Chart index
Workloads StatefulSet (1 replica)
Storage one RWO PVC per share (<release>-<share>-volume-claim)
Ingress none — NodePort Service on 445/tcp and 139/tcp
External dependencies none
Secrets 1 × OnePasswordItem (users.conf)
Install-test ci/samba/ (fixtures + test values)

Prerequisites

  • 1Password operator in the cluster, plus one 1Password item — see the field below.
  • A storage class: samba.storageClass has no default that works everywhere (longhorn is shipped) and is enforced with a required — the render fails without it.
  • Reachable node IPs. The Service is of type NodePort, so the ports are open on every node of the cluster and reachable by anyone who can reach a node. There is no ingress-level authentication in front of it; the only gate is SMB authentication.
  • SMB3-capable clients. The rendered smb.conf refuses everything older or unencrypted — see The server refuses old and unencrypted clients.

Installation

The registry URL and its credentials live in 1Password (item jlab-registry-access: username, password, charts-url) and never in a repository or a shell history file.

helm repo add jlab "$CHARTS_URL" --username "$REG_USER" --password "$REG_PASS"
helm repo update

helm install samba jlab/samba \
  --namespace samba --create-namespace \
  -f my-values.yaml

Minimal my-values.yaml:

secrets:
  sambaUsersSecretRef: op://vault/samba-users

samba:
  storageClass: longhorn
  shares:
    - name: documents
      validUsers: alice,bob
      description: Shared documents
      size: 50Gi

helm install prints the chart's NOTES.txt, which renders the same information — node ports, claim names, per-share mount paths and the client command lines — against the values actually set. Read it; it is the per-release version of this page.

What the 1Password item must contain

The chart renders one OnePasswordItem; the operator turns it into a Kubernetes Secret whose keys are the field names of the 1Password item. This chart mounts exactly one key.

Values key Kubernetes Secret Fields
secrets.sambaUsersSecretRef <release>-samba-users-secret users.conf — the user database, one line per user

The field has to be spelled that way to the letter. A differently named field yields a Secret without the key and the pod does not start at all: it never gets past volume setup, so there is no log to read — the mount failure naming the missing key is in the pod's events (kubectl -n <namespace> describe pod <release>-samba-sfs-0).

The content, one line per user, with an optional sixth field for a home directory:

username:uid:groupname:gid:password

The entrypoint creates a real Unix user per line at start; the password is the SMB password, in clear text in the vault. Give the users gid 1000 — that is the pod's fsGroup and what the share volumes are group-owned by. A user with a different gid authenticates fine and then cannot write. Every name used in a share's validUsers must exist here; one that does not is not an error anywhere — the share simply refuses that login.

Note the exact values key: a shortened sambaUsersRef is not read by the chart and is rejected by the schema.

Configurable values

Global

Key Type Default Description
global object unset Helm's conventional cross-chart values. Unused by this chart; the schema keeps it open so an umbrella install cannot break on it.
scaleDown bool false Renders every workload with replicas: 0 without uninstalling. Takes precedence over samba.replicas. The share PVCs, the users Secret and the Service stay in place, so no data is lost and the node ports stay reserved.

secrets

Key Type Default Description
secrets.sambaUsersSecretRef string null Required, and enforced: the template wraps it in a required, so an unset value fails the render rather than installing a release that cannot start. op:// item path of the users.conf item.

samba

Key Type Default Description
samba.replicas int 1 Replica count; 0 is allowed. scaleDown wins over it. Anything above 1 does not work — see More than one replica.
samba.smbNodePort int 30445 Node port published for container port 445. null lets the API server assign a free port from the cluster's node-port range; read it back with kubectl get svc.
samba.netbiosNodePort int 30139 Node port published for container port 139. Same null behavior.
samba.shares list one entry with all fields null One SMB share per entry. Each entry drives a share section in smb.conf, a PVC and a volumeMount, so the list is part of the pod template — changing it restarts the workload by design. The shipped placeholder entry is not usable: name and size are enforced with required, so the chart's own defaults do not render.
samba.shares[].name string null Required per entry. Share name, mount path /shares/<name>, and the middle of the claim name <release>-<name>-volume-claim.
samba.shares[].size string null Required per entry. Size of that share's PVC.
samba.shares[].validUsers string null Comma-separated user list, rendered as valid users in the share section. An empty list does not mean "nobody" — see below.
samba.shares[].description string null Rendered as the share's comment.
samba.storageClass string longhorn Required (enforced with a required in the PVC template). Applies to all shares alike.
samba.env list [] Environment variables for the container. The chart sets none of its own, so this is the container's whole environment. Rendered through tpl; value, secretKeyRef, configMapKeyRef forms. smb.conf is fully rendered by the chart, so image variables that would generate a config do not override it.
samba.livenessProbe object TCP connect to port 445, period 10s, timeout 5s, 3 failures SMB is not an HTTP protocol, so the probes connect to the SMB port. See What a Ready pod does not prove.
samba.readinessProbe object TCP connect to port 445, period 10s, timeout 5s, 3 failures
samba.startupProbe object TCP connect to port 445, period 5s, timeout 5s, 30 failures (= 150s) Gates liveness and readiness until the port opens.
samba.resources.limitFactor number 3 Memory and ephemeral-storage limits are computed as factor × request.
samba.resources.requests object cpu: 0.1, memory: 50Mi, ephemeralStorage: 50Mi The ephemeral-storage request is not cosmetic here — see The root filesystem is ephemeral.
samba.resources.limits object unset An explicit limit always wins. A CPU limit is only rendered when set here.
samba.securityContext.pod object fsGroup: 1000 Passed verbatim into the pod spec. This is the gid the share volumes are group-owned by, and the gid users.conf must use.
samba.securityContext.container object allowPrivilegeEscalation: false, readOnlyRootFilesystem: false, caps drop ALL and add CHOWN, DAC_OVERRIDE, FOWNER, NET_BIND_SERVICE, SETGID, SETUID Passed verbatim into the container spec. A documented deviation from the repo hardening baseline, forced by the image — see below.

The whole resources and securityContext blocks, and each probe, may also be null or empty: the chart then falls back to exactly the defaults above — including the deviation, which the fallbacks reproduce verbatim (style guide §12).

There are no ingress.* values: this chart renders no Ingress.

networkPolicy

Inbound 445 and 139 — the pod ports — open to any source, and outbound DNS and nothing else.

enabled is the only value here, and that is deliberate. Both inbound rules are open to any source, so allowedNamespaces and probeSources would reach no rule at all — and a value that silently does nothing is worse than one that is absent, because it reads as a control.

Value Type Default Meaning
networkPolicy.enabled bool true Renders the policy. Only a boundary where the CNI enforces one — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured.

Operating notes

Access is a node port, not 445

SMB      <node IP>:30445    (container port 445)
NetBIOS  <node IP>:30139    (container port 139)

kubectl get nodes -o wide          # the IPs to connect to

That a node port is not 445 decides which clients can reach the server at all, because SMB clients differ in whether they let you name a port:

Client How
macOS Finder Cmd-K, then smb://NODE-IP:30445/SHARE
Linux mount -t cifs //NODE-IP/SHARE /mnt -o port=30445,vers=3.1.1,username=USER
smbclient smbclient //NODE-IP/SHARE -p 30445 -U USER
Windows Explorer not possible — a UNC path \\host\share carries no port and the client always dials 445

For Windows, either put a port forward in front of it on the client side, or publish 445 itself through a LoadBalancer Service managed outside this chart.

Inside the cluster none of that applies: the Service port is the normal 445, so a pod addresses the server as //<release>-samba-service.<namespace>.svc.cluster.local/SHARE.

Nothing UDP is published — NetBIOS name service (137/udp) and datagrams (138/udp) are not in the Service, so this server never shows up by itself in a client's network browser. Address it by node IP or by a DNS name you point at the nodes yourself.

The server refuses old and unencrypted clients

smb.conf is fixed by the chart and strict on purpose:

server min protocol = SMB3      SMB1 and SMB2 are rejected at negotiation
server smb encrypt  = required  an unencrypted session is refused
server signing      = mandatory
security = user                 there is no guest access, ever

Current Windows, macOS and Linux clients satisfy all of it. What does not is the long tail — smart TVs, media players, scanners, NAS-to-NAS sync, old cifs kernels. Those fail at connect with a bare protocol or access error and no hint that the cause is on the server side. None of the four settings is exposed as a value; loosening them means editing templates/samba-config.configmap.yaml. Printing services are disabled in the same file.

An empty validUsers opens the share to everyone

A share without samba.shares[].validUsers renders valid users = with an empty list, and in Samba an empty valid-users list does not mean "nobody" — it means the restriction does not apply and every authenticated user gets in. Combined with writable = yes, which the chart sets on every share, that is full read/write for everyone in users.conf. Name the users explicitly if that is not what you want; NOTES.txt counts the unrestricted shares of the release and names them.

Changing the user list does not restart anything

Helm never sees the content of a 1Password-backed Secret, so editing the item in the vault changes no rendered object and nothing rolls. Added, removed and re-passworded users stay invisible to the running smbd until it is restarted by hand:

kubectl -n <namespace> rollout restart statefulset/<release>-samba-sfs

The operator's operator.1password.io/auto-restart annotation is not a substitute and is deliberately not set here. Verified against onepassword-operator 1.12.0, its restart handler enumerates exactly one workload type — DeploymentList — so on a StatefulSet the annotation is a silent no-op, and setting it would advertise a guarantee that does not exist (style guide §9; matrix-synapse and homeassistant are in the same position). Pointing secrets.sambaUsersSecretRef at a different item does roll the pod, because that changes the rendered CR.

smb.conf is the other half of the same rule and is covered: the pod template carries checksum/config over the rendered ConfigMap, because the file is mounted with subPath (never refreshed by the kubelet) and read once at start.

Every restart drops every open SMB session

This is a single pod, and SMB sessions live in its TCP connections and in lock databases under /var/lib/samba that go with the container. A restart disconnects every client at once: open handles die, in-flight writes are lost, and a client that had a file open sees an I/O error rather than a reconnect. SMB3 persistent handles do not save you here.

What triggers a restart is wider than it looks. Because of the checksum/config annotation, any edit to samba.shares rolls the pod — including adding a share that has nothing to do with the ones in use, or only fixing a description. So does a change to samba.env, the resources, the probes, the security context or the app version. Plan share changes for a window when nobody has files open.

The share volumes belong to this release

The PVCs are ordinary resources of the release, not volumeClaimTemplates, and carry no resource-policy annotation:

  • helm uninstall <release> deletes them, and with a Delete reclaim policy every file on every share goes with them. Back the volumes up first; an annotation added to the live PVC afterwards does not help, because Helm reads it from the release manifest.
  • Renaming a share is not a rename. The claim name is derived from the share name, so the next upgrade creates a new, empty claim and deletes the old one — data loss dressed up as a config edit. Copy the data out first, or leave the name alone.

The claim name <release>-<share>-volume-claim carries no chart name. That is a historical name this chart keeps deliberately, a documented exception to §1 of the style guide — renaming it would orphan the data.

The hardening is a documented deviation, forced by the image

The pod runs as root with a writable root filesystem and a non-empty capabilities.add. Every part of that is required by dockur/samba's entrypoint (samba.sh):

  • No runAsNonRoot: the entrypoint creates the Unix users from users.conf (groupadd/adduser/usermod), and smbd impersonates the authenticated share user per session. Both need a root start.
  • readOnlyRootFilesystem: false: the entrypoint writes /etc/passwd and /etc/group, creates the /etc/samba.conf healthcheck symlink, mkdirs /shared and builds the Samba TDB databases under /var/lib/samba.
  • The capability set is ALL dropped except what a multi-user root file server needs: NET_BIND_SERVICE (ports 445/139), SETUID/SETGID (per-session impersonation), CHOWN/FOWNER/DAC_OVERRIDE (managing share files on behalf of the users).

Do not "restore" the standard here — it breaks the file server. The README lists samba by name as one of the charts where the image cannot satisfy the baseline (style guide §5).

The root filesystem is ephemeral and accounted for

Shares are on PVCs, but everything the entrypoint writes outside them — /etc/passwd, the TDB databases under /var/lib/samba — lives on the container's writable layer. It is discarded on every restart, which is fine, because the users are rebuilt from users.conf each time. It counts against the container's ephemeral-storage limit (computed: limitFactor × requests.ephemeralStorage, so 150Mi by default). Exceeding that limit does not fail a request: the kubelet evicts the pod, and the reason shows on the pod, not in Samba's log.

What a Ready pod does not prove

All three probes are bare TCP connects to port 445. Green means one thing only: smbd is listening. It says nothing about whether users.conf parsed, whether the users were created, whether a password is right or whether any share is usable. A release can be fully Ready and refuse every single login.

User creation happens in the entrypoint, so that is where a malformed users.conf surfaces:

kubectl -n <namespace> logs statefulset/<release>-samba-sfs

The real check is a login from a client — the only thing that exercises authentication and the share list at once:

smbclient -L //NODE-IP -p 30445 -U USER

No shares configured means the server exports nothing

With samba.shares empty, smb.conf carries the [global] section and not a single share, no PVC is created and the pod mounts no data volume. smbd starts, the probes go green and a client can authenticate — and then sees an empty list.

More than one replica will not work

Every replica mounts the same ReadWriteOnce claims; there is no per-pod volume. Unless the storage class hands those claims to several nodes at once, the extra pods never start, and where they do, two independent smbd instances write the same files with separate lock databases — which is how SMB corrupts data. The Service would hand clients to either pod at random.

Both inbound rules are open on purpose

The ports are published as NodePorts (samba.smbNodePort, samba.netbiosNodePort) and the clients are machines on the LAN. After NodePort masquerading the source address is a cluster node, so a source selector could only ever be a list of node IPs — a restriction in appearance and a maintenance trap in fact. 445 is additionally the port all three probes open a socket against.

What the policy still buys: only 445 and 139 are reachable, so anything else this image binds is not.

Not covered, and it does not matter here: the NetBIOS name-service broadcasts on 137/138 UDP are not published by this chart's Service at all, and clients reach the share by address rather than by browse list.

A domain-joined Samba needs more

Kerberos (88) and LDAP (389 or 636) in networkPolicy.egress.extraPorts. A standalone one needs nothing.

The policy names pod ports, never the NodePorts

A NetworkPolicy is evaluated at the pod's network interface, after kube-proxy has already translated the NodePort to the target port. So a rule naming the NodePort matches nothing there: the object would be accepted, would read as strict, and — being the only rule for that port — would block every inbound connection instead.

This is the easiest mistake to make in a NodePort chart, which is why ci/run-networkpolicy-test.sh asserts every ingress port against the containerPorts of the pod spec the policy selects, and flags a number in the NodePort range explicitly.

The policy is only a boundary where the CNI enforces one

The API server accepts a NetworkPolicy whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.

Measured for this cluster: the CNI is Canal (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it exempts the kubelet's probe traffic, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.

Upgrades and backup

  • Back the share volumes up before an uninstall or a share rename — both delete claims, and the chart sets no resource policy that would hold them.
  • Growing samba.shares[].size only works if the storage class allows volume expansion. Shrinking is refused by Kubernetes and makes the next upgrade fail on the PVC instead of touching the data.
  • Every upgrade that touches the pod template disconnects all clients (see above). An app-version bump always does.
  • There is no schema or state migration: the users are rebuilt from users.conf at each start, so a rollback of the app version is a plain image change.
  • App major jumps go stepwise: last release of the old major line first, then the newest major — see Versioning.

CI

ci/samba/ holds test-values.yaml (one ci-share of 1Gi on local-path with validUsers: ciuser, and the op:// ref against the stub CRD) and fixtures.yaml (the dummy ci-samba-users-secret whose users.conf is a single line ciuser:1000:smb:1000:ci-password, in the image's username:uid:groupname:gid:password[:homedir] format). No settings.env — the default install timeout suffices. The install-test runs on every PR that touches the chart — see CI/CD.

See also