Skip to content

feat(pro): packages upload to SMB file share distribution points - #462

Open
neilmartin83 wants to merge 7 commits into
mainfrom
feat/packages-upload-smb
Open

neilmartin83 wants to merge 7 commits into
mainfrom
feat/packages-upload-smb

Conversation

@neilmartin83

@neilmartin83 neilmartin83 commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

What

pro packages upload can now write to a Classic SMB file share distribution point, not only to JCDS or through the Jamf Pro upload endpoint (which only reaches a cloud DP).

  • --distribution-point <name> picks the DP. With no flag, a tenant with no cloud DP uses its master SMB DP (it used to be refused), on gateway and instance profiles alike. A tenant that has JCDS and a master file share still uploads to JCDS; name the share to override. AFP and any unrecognised connection_type are refused, never selected.
  • The file goes to <share>/Packages/<file> (Jamf's documented layout), written under a temporary name and renamed into place. A share with no Packages folder is refused rather than having one created.
  • Jamf Pro computes no hash for a file share, so the check is the size on the share.
  • Progress is printed on all three routes (SMB, JCDS, API): one line redrawn in place on a terminal, a line per quarter elsewhere, nothing under --quiet.

Delete and multi-file

  • pro packages delete and delete-multiple take --distribution-point and, on an SMB share, remove the package file as well as the record. The generated delete removes the record only, so the binary was left on a share Jamf Pro never cleans. The share is chosen by the upload's rule (named, else the master SMB DP when the tenant has no cloud DP) and the account is resolved the same way. The generated commands are untouched: a wrapper reads each record's file name first, checks the share before any record is deleted, lets the generated command run (ids, --name, --from-file, --ids, stdin, confirmation, cooldown), then removes the file of every record that now reads back 404. A bulk delete that fails part way leaves the files of the surviving records alone. A file already absent from the share is reported, not an error; a file that cannot be removed after its record is gone exits non-zero and names the file. --dry-run lists the files that would go.
  • pro packages upload-multiple <file>... uploads several files through the single upload (same route, same verification). The share and its account are resolved and checked once, so a prompted account is asked for once and a wrong one stops the run before any record exists. Every file is checked first, and two files with the same name (case-insensitive) are refused. A failed file does not stop the rest; the run exits non-zero and names each failure (--allow-partial-failure downgrades it). With several files, each record is printed as its own JSON document.
  • Fix to the single upload: --distribution-point on a tenant whose cloud DP is NONE was refused by the cloud-DP check before the named share was used. An explicit share now bypasses it.
  • Fix: a krb5.conf go-krb5 cannot split (a realm block on one line) panicked inside the client; it is now an error naming the file.

Credentials

Jamf Pro never returns the DP account's password, so it comes from, in order:

  1. JAMFSMB_USERNAME / JAMFSMB_PASSWORD (CI)
  2. an account saved by the new pro packages smb-setup (password in the keychain, only a keychain: reference in the profile's smb-credentials)
  3. a Kerberos ticket: KRB5CCNAME, else /tmp/krb5cc_<uid>, else on macOS a private 0600 copy of the signed-in user's ticket via kcc copy_cred_cache (removed after the upload)
  4. a prompt on a terminal; otherwise an error naming the options

No credential flag exists, nothing is read from stdin, and smb-setup always prompts, proves the account before saving, and is refused over MCP. The account, share and Packages folder are checked before the package record is created.

Dependency

github.com/hirochachacha/go-smb2/v2, pure Go (CGO_ENABLED=0 cross-builds unchanged). It is an untagged pseudo-version: upstream master is the /v2 module but has no tag. I asked upstream for one in hirochachacha/go-smb2#110. v1.1.0 has no Kerberos. I also looked at jfjallid/go-smb and rejected it (a security-tooling module that depends on several personal forks).

Tested

  • Samba (container) end to end: upload, replace, wrong password, missing Packages folder, no record left behind on any pre-flight failure.
  • Linux client with env-only auth, no TTY, no config: succeeds with JAMFSMB_*, exits 2 with a hint without.
  • Kerberos against a Samba AD DC: ticket-only upload on Linux, a supplied password beating a ticket, explicit FILE: cache; on macOS the real kcc/klist copy loaded by the SMB client's Kerberos reader.
  • smb-setup through a pty: wrong password refused, save, upload from the saved account, --remove.
  • JCDS and API routes against a real tenant, hash verified; -race on the progress code.
  • No-flag discovery on an instance profile (cloud DP reports NONE, a master SMB DP exists): found the DP and went to SMB, with no package record created before the connection check. Found by testing: before c25cfe9 only gateway profiles could read the cloud DP, so instance profiles went to the API route and polled for a hash that never came.
  • A signed build from the v1.35.0 tag (jamf-cli-internal) builds this branch.
  • Delete, delete-multiple (--ids and stdin) and upload-multiple against Samba on a real tenant, on an instance profile and a gateway profile: NTLM both, Kerberos for delete on both (cifs/localhost against a Samba AD DC). Also: --from-file with an id and a name, wrong password leaves the record, file already absent from the share, a cloud DP present with no flag (record only), re-upload without --yes failing per file, dry-run. Not run: Kerberos for delete-multiple and upload-multiple, which share the auth path.
  • make fmt, golangci-lint, go fix, lint-dead, verify-generated, verify-site-output, go test ./internal/....

Not tested

Windows as SMB server or KDC; NoMAD and Jamf Connect specifically; a full SMB session from macOS with Kerberos; the generated krb5.conf with DNS SRV KDC lookup.

Notes

  • A failure after the package record is created still leaves the record, as the other routes do; the pre-flight now catches the common causes first.
  • No CHANGELOG entry: nothing breaking. Wiki not updated.

🤖 Generated with Claude Code

neilmartin83 and others added 4 commits October 29, 2026 19:05
…oint

`pro packages upload --distribution-point <name>` copies a package to the
Packages folder of an SMB2/3 file share distribution point, through
hirochachacha/go-smb2/v2 (pure Go, so CGO_ENABLED=0 cross-builds are
unchanged). With no flag, a tenant with no cloud distribution point uses its
master SMB distribution point. AFP and any unrecognised connection type are
refused, never selected.

Jamf Pro never returns the share account's password, so it comes from, in
order: JAMFSMB_USERNAME/JAMFSMB_PASSWORD, the account saved in the profile by
the new `pro packages smb-setup` (password in the keychain, only a reference in
the config), a Kerberos ticket in a FILE cache (KRB5CCNAME, else
/tmp/krb5cc_<uid>), then a prompt on a terminal. No credential flag exists, and
--no-input fails with a hint rather than prompting. smb-setup is refused over MCP.

The account, share and Packages folder are checked before the package record is
created. The file is written under a temporary name and renamed into place.
Jamf Pro computes no hash for a file share, so the check is the size on the
share. Tested against Samba; not against Windows or a KDC.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…cket

A bound Mac, NoMAD or Jamf Connect keeps its ticket in the system's API
credential cache, which the SMB client cannot read. With no password supplied or
saved and KRB5CCNAME unset, copy the ticket-granting ticket to a private 0700/0600
file with the system's own kcc, take the realm from klist, and use the system
krb5.conf (else one generated from the realm with dns_lookup_kdc). The copy is
removed when the upload ends. Nothing found is not an error.

Tested against a Samba AD DC: the Kerberos path on Linux end to end, and on
macOS the real kcc/klist copy loaded by the SMB client's Kerberos reader.
The macOS discovery was not run against a full SMB session, nor against a
real DNS SRV lookup of a KDC.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A multi-gigabyte package used to copy in silence. The SMB, JCDS and API routes
now report bytes sent to stderr: one line redrawn in place on a terminal, a line
per quarter anywhere else, nothing under --quiet. --no-hints leaves it on, since
it is progress and not a hint.

The JCDS transfer sizes its parts from the file's length and reads them through
Seek and ReadAt, so the file is wrapped by embedding rather than by a plain
reader. The count is held at the total, because a part read again after a failed
attempt would otherwise report past 100%, and a rewind for an HTTP 429 retry
resets it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
neilmartin83 and others added 3 commits October 29, 2026 19:41
…on an instance profile

With no --distribution-point, a tenant with no cloud distribution point falls
back to its master SMB distribution point. That fallback only ran when the
cloud distribution point could be read through the Platform SDK, which an
instance (OAuth or token) profile does not have, so those profiles went to the
API upload endpoint, created a package record, and polled for a hash that never
came.

Read /v1/cloud-distribution-point over the profile's own client for that case.
Only an explicit NONE counts; a failed or unprivileged read stays silent and
takes the API route as before. The no-cloud-DP refusal now also names
--distribution-point.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…n SMB share

The generated delete removes the package record only, which leaves the binary
orphaned on a file share distribution point. Both commands now take
--distribution-point and select the share by the upload's rule (named, else the
master SMB distribution point when the tenant has no cloud distribution point).

The share is checked before any record is deleted. A file is removed only for a
record that reads back 404 afterwards, so a bulk delete that fails part way
leaves the files of the surviving records alone. A file already absent from the
share is reported, not an error.

A krb5.conf go-krb5 cannot split (a one-line realm block) panicked inside
NewKerberosCredential; it is now an error naming the file.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Each file goes through the single upload, so the route, the verification and
the record handling are unchanged. The SMB share and its account are resolved
and preflighted once, so a prompted account is asked for once and a wrong one
stops the run before a record exists. A failed file does not stop the rest; the
run exits non-zero and names each failure.

A named --distribution-point on a tenant with no cloud distribution point was
refused by the cloud-distribution-point check before the share was used; an
explicit share now bypasses it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant