← dashboard

ssh-pam-ninja

model: opus-4-8 c static analysis on 0 PR(s) seen
workspaceninjarmm jira projectNJ review targetsmaster · main · *_release · release/* thresholdsseverity ≥ minor · confidence ≥ medium · cap 12 max diff0.4 MB pollevery 300s

What this is

SSH one-time-password PAM gatekeeper (Linux C): a pam_ninja.so module in sshd's auth stack validates a time-based OTP (timestamp + SHA-256 over timestamp+shared secret, short TTL) to gate SSH reverse-tunnel access to Ninja infra; ships client OTP generators + tunnel scripts. Auth gate — a weakness is an SSH auth bypass.

Project primer

Durable orientation injected into every review as .ai-review/project.md.

ssh-pam-ninja — SSH one-time-password PAM gatekeeper (review primer)

A small Linux C security component that gates SSH (reverse-)tunnel access to Ninja
infrastructure with a time-based one-time password. A PAM module (pam_ninja.so) is loaded
into sshd's auth stack on the server; client-side generators mint the OTP and a thin script
opens the SSH tunnel. The whole thing is an authentication gatekeeper — a weakness here is an
SSH auth bypass, i.e. potential remote access to the host.

Architecture (durable map)

It reads a shared secret from a keyfile (path passed as a module argument in the PAM
stack), takes the presented password, and validates a time-stamped digest OTP: a
timestamp concatenated with a SHA-256 digest computed over timestamp + secret. It recomputes
the digest, compares, and checks the embedded timestamp falls inside a short TTL window
(freshness / replay defence). A debug/skew allowance may widen that window.

with a few-seconds TTL — the value a client feeds as the SSH password.

tunnel** (ssh -R … -N) as a locked-down, shell-less tun user.

(relative to pam_unix, pam_deny, pam_permit) and how sshd is wired to use it. The stack
ordering is the security policy — reordering changes what authenticates.

What matters most when reviewing here (durable risk areas)

anonymous client) is parsed and compared before access is granted. Scrutinize: digest
recomputation, constant-time comparison of the secret-derived digest, timestamp parsing,
and the TTL / replay window (too wide, or a debug allowance left on in prod, weakens it).

secret that reaches logs, world-readable key material, or a weak/hardcoded key is a critical
finding. Client and server must derive from the same secret and scheme.

strncpy/sprintf/strtoll-style parsing of attacker-controlled input — classic
buffer-overflow / off-by-one / missing-NUL / format-string territory. A parsing bug on the
pre-auth path is remotely reachable.

the timestamp at non-debug levels leaks material into syslog — treat as security-sensitive.

path is an auth bypass; every failure path must deny. Control-flow that can fall through to
"allow" is the highest-severity class here.

Security-sensitive zones

The pam_sm_authenticate OTP parse + verify path; keyfile/secret loading; the timestamp/TTL
freshness check; the client generators (must match the verifier's scheme); and the PAM-stack
ordering in etc/.

Compatibility surfaces (contracts that must not silently break)

ordering (timestamp + secret), digest algorithm, and the TTL must stay in lockstep between the
generators and the module — a change on one side breaks authentication (or, worse, weakens it).

module and secret live; a change to argument order or file paths is an operational break.

Conventions & gotchas

reviewed/shipped default**; flag debug artifacts on the auth path.

tunnels — changes that grant it more capability are security-relevant.

is operational (who can SSH-tunnel into Ninja hosts), not code-level. Depends only on system
PAM headers + OpenSSL (SHA-256).

_Orientation only — verify current specifics in the code; this describes durable architecture &
risk areas, not a snapshot._

Ecosystem map

Shared cross-project relationships, injected as .ai-review/ecosystem.md.

NinjaRemote / NinjaOne project ecosystem (relationship map)

How the monitored repos relate — so a change in one repo is reviewed with its blast
radius in mind (a change upstream of ncpeer can break ncpeer even if it looks local).
Orientation only; verify specifics in code.

NinjaRemote product

speak the NCRP wire protocol (protocol/format changes must stay compatible across
client and server versions).

Cloud-side relays of the ninja-remote system (AWS-hosted, Python/Go)

URLs, it pairs them and forwards frames verbatim. Signed join URLs are minted by the
Ninja backend — the signature scheme is a backend⇄relay contract, and the frame
semantics are a peer⇄peer contract.

a signed upload leg streams to a signed download leg through memory (never stored).
Two parallel implementations (Python ftsv2/ + Go fts_go/) must stay behaviorally
identical; the URL/signature scheme is shared-family with websocket-switch.
NinjaRemote file-transfer features ride on these relays when peers can't go direct.

Pulled INTO ncpeer (and sometimes nmsnj) — upstream of the products

or behaviour change here affects both consumers.

build against**. A dependency bump changes what those products link (security/ABI/build).

through the ncpeer_vcpkg overlay.

Largely independent

shares nccommon and ncpeer_vcpkg with ncpeer.

part of the remote-desktop family). Runs as a child process of the NinjaRMM agent (the
parent owns install/upgrade/policy/launch); NinjaFlow owns the CLI contract, config schema,
credential precedence, DB schema/migrations, and exit-code meanings the parent consumes
those are cross-process contracts. Submits telemetry to the Ninja Backend. Shares no code with
ncpeer/nmsnj; adjacent to nmsnj only in domain (network), not in implementation.

not part of any product's build. It consumes the SBOMs the other products' pipelines emit
(Ninja-Remote/ncpeer, NMS/nmsnj, NinjaFlow all appear in its scope maps), scans them for CVEs
and files Jira issues — a downstream *security-scanning* relationship, not a code dependency.
Shares no code with the others; has its own DB/schema and a toolbox submodule.

authentication component: its pam_ninja.so gates SSH reverse-tunnel access into
Ninja infrastructure with a time-based OTP. Shares no code with the other repos; its
blast radius is operational (who can SSH-tunnel into hosts), not code-level. The client OTP
generators and the module must agree on the OTP scheme + shared secret.

Review implications

these are *upstream* of ncpeer (and nccommon/vcpkg also of nmsnj) — weigh the consumer
side: API/ABI compatibility, build impact, and cross-platform reach.

contract.

Also in every review's context

The reviewer instructions CLAUDE.md (rules, house C++ style, output schema); the PR/ticket block pr.md (title, description, linked Jira + acceptance criteria, commits, changed files); the full diff.patch; and deterministic static_analysis.md (cppcheck on changed lines).