Security
Network and path policy
How the outbound probe policy vets connectivity targets and how the path policy decides which caller-supplied files may be read, including symlink handling and the deny list.
Two tool families in Secronyx are gated by a dedicated policy that runs before any network or filesystem access: get_network_latency opens connections to caller-supplied targets, and get_app_config together with the lock-file readers open caller-supplied files. This page documents both policies as implemented in internal/netconfig/probepolicy.go and internal/pathpolicy/pathpolicy.go.
Outbound probe policy
get_network_latency (scope network) is the only tool that connects to a target chosen by the caller. The targets argument is a list of strings, each in one of three forms:
| Form | Probe | What is sent |
|---|---|---|
host or ip |
icmp |
one ICMP echo per attempt via the ping binary (ping -c 1 -W 2 <ip> on Linux) |
host:port |
tcp |
one TCP connect to ip:port per attempt |
http://host[:port]/... or https://... |
http |
one TCP connect to ip:port per attempt (80 or 443 when the URL has no port) |
Each target is attempted three times (packets_sent is 3 in every probe) and the result reports the minimum, maximum and average latency of the attempts that succeeded, plus the packet loss percentage.
The HTTP form is connect-only. splitProbeURL keeps the host and port and discards the path, query and any credentials in the URL; no request is written, so no redirect can be followed and no response body is read. The MaxRedirects field on the policy is reserved and unused for that reason.
Resolve first, then check, then connect
ProbePolicy.ResolveTarget is applied to every target before any packet:
- Trim the target, strip IPv6 brackets and any
%zonesuffix; an empty result is refused. - Refuse the cloud metadata hostnames
metadata.google.internal,metadata.azure.internal,metadata,instance-dataandinstance-data.ec2.internal, or any name ending in.plus one of those. This happens before resolution so a resolver cannot map them elsewhere. - Refuse anything in
--probe-deny-hostsby hostname. - If the target is an IP literal, use it. Otherwise require it to pass
isSafePingTarget(hostname characters only, at most 253 characters, no leading-) and resolve it with the policy timeout (5 seconds unless a policy sets another). - Check every resolved address. One bad answer refuses the whole target.
- Return the first address. The probe connects to that IP, or hands that IP literal to
ping, never the name. A DNS answer that changes between check and use therefore cannot redirect the probe.
Address rules
checkIP applies these rules to each address, in order:
| Address class | Default | Override |
|---|---|---|
Cloud metadata: 169.254.169.254, 169.254.170.2, fd00:ec2::254, 100.100.100.200, 192.0.0.192 |
refused | none; refused even in allowlist mode |
Listed in --probe-deny-hosts (IP or CIDR) |
refused | none |
Unspecified (0.0.0.0, ::) |
refused | none |
| Multicast or IPv4 broadcast | refused | none |
Loopback (127.0.0.0/8, ::1) |
refused | --probe-allow-loopback |
Link-local (169.254.0.0/16, fe80::/10) |
refused | none exposed on the command line |
Private (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7, 100.64.0.0/10) |
allowed | none exposed on the command line |
| Everything else | allowed | --probe-allow-hosts switches to allowlist mode |
Private ranges are allowed by default because operators probe their own services; there is no flag to refuse them from the command line, though --probe-deny-hosts can carve out specific ranges. Link-local is refused and cannot be enabled from the command line.
--probe-allow-hosts a,b,10.0.0.0/8 switches to allowlist mode: a target must match one entry by hostname, IP or CIDR as well as pass the address rules, and the metadata addresses stay refused. --probe-deny-hosts adds refusals in either mode. Entries are compared case-insensitively; a CIDR matches resolved addresses, an IP matches exactly, anything else is treated as a hostname.
What a refusal looks like
A refused target is never connected to and is never handed to ping, though a hostname is resolved before the address rules run. The tool call itself succeeds; the refusal is reported per probe in its error field, wrapped from ErrProbeRefused as target refused by probe policy: <target> (<reason>). The following is real output from a call with five deliberately bad targets:
169.254.169.254 | icmp | target refused by probe policy: 169.254.169.254 (cloud metadata address)
127.0.0.1 | icmp | target refused by probe policy: 127.0.0.1 (loopback address; enable with --probe-allow-loopback)
metadata.google.internal | icmp | target refused by probe policy: metadata.google.internal (cloud metadata hostname)
http://user:pw@[::1]:8080/path | http | target refused by probe policy: ::1 (loopback address; enable with --probe-allow-loopback)
-t | icmp | target refused by probe policy: -t (invalid hostname)
The other reason strings are unresolvable address, denied host <entry>, unspecified address, multicast or broadcast address, link-local address, private address, empty target, no addresses and not in --probe-allow-hosts.
# Default policy: private and public allowed, loopback and metadata refused
secronyx
# Let the host probe its own listeners
secronyx --probe-allow-loopback
# Allowlist: only the monitoring subnet and two named hosts
secronyx --probe-allow-hosts 10.20.0.0/16,db.internal,cache.internal
# Deny a range inside the default allowance
secronyx --probe-deny-hosts 10.99.0.0/16The ICMP form depends on a ping binary being on PATH and permitted to send echo requests; where it is not, the probe reports the lookup or ping exit error in its error field and the TCP and HTTP forms still work.
Path policy
get_app_config (scope software) and the fifteen lock-file tools (get_npm_lock, get_pip_lock, get_cargo_lock, get_go_sum, get_gemfile_lock, get_yarn_lock, get_pnpm_lock, get_poetry_lock, get_composer_lock, get_mix_lock, get_pubspec_lock, get_swift_resolved, get_podfile_lock, get_gradle_lock, get_conda_lock; get_pip_lock called with no path looks for Pipfile.lock and then requirements.txt) are the tools this policy governs. Each goes through the global policy — Check, or CheckWithExtraRoots for a lock reader falling back to the working directory — before any filesystem access, so a refused path is never stat'ed, let alone opened. Redaction of the contents is a second line of defence, not the access control.
One other tool takes a path from the caller: get_app_logs (scope logs) accepts a log file or directory. It is not covered by this policy and not affected by --allowed-paths; the log collector applies its own fixed directory allowlist (/var/log, /var/logs, /tmp, /home, /Users, /Library/Logs, C:\Windows\Logs, C:\ProgramData, C:\Users, C:\inetpub\logs) and rejects traversal separately.
Collectors that read fixed paths (/etc/environment for get_env_vars, /etc/fstab, /proc/<pid>/environ and so on) are not subject to this policy; those paths are hard-coded and cannot be steered by the caller. Registry tools take registry paths, which are a different namespace.
Roots
Reads are permitted only beneath an allowed root. Default() installs:
| Platform | Roots |
|---|---|
| Linux and macOS | /etc, /opt, /usr/local/etc, /srv, /var/lib, /var/www, /app |
| Windows | C:\ProgramData, C:\inetpub, C:\Program Files, C:\Program Files (x86) |
--allowed-paths /etc/myapp,/srv/config replaces the root list. It does not touch the deny list or the size cap, which stay in force under whatever roots you supply. Root comparison is by whole path component, so a root of /etc does not admit /etcetera, and it is case-insensitive on Windows.
A lock-file reader called with no path keeps its historical "look in the current directory" behaviour: the default file name is joined to the working directory, and the working directory is added as an extra root for that one call only. The deny list and size cap still apply, and the global policy is not widened.
Deny globs
Inside an allowed root, a path matching any deny glob is refused. The built-in list:
**/.ssh/** **/.aws/** **/.azure/** **/.gcloud/** **/.config/gcloud/** **/.gnupg/** **/.kube/**
**/.docker/config.json **/.netrc **/_netrc **/.git-credentials **/.pgpass **/.my.cnf **/.npmrc **/.pypirc
/etc/shadow* /etc/gshadow* /etc/security/opasswd /etc/sudoers*
/etc/ssl/private/** /etc/pki/**/private/** /etc/letsencrypt/**/privkey* /etc/krb5.keytab
**/*.pem **/*.key **/*.p12 **/*.pfx **/*.jks **/*.keystore **/*.kdb **/*.gpg **/*.asc
**/id_rsa* **/id_dsa* **/id_ecdsa* **/id_ed25519* **/*.keytab
**/*.tfstate **/*.tfstate.backup **/terraform.tfvars **/secrets.y*ml **/master.key
**/credentials.json **/service-account*.json
Glob semantics: ** matches any number of path components, * and ? match within one component, patterns are matched against the slash-separated absolute path, a pattern that does not begin with /, ** or a Windows drive letter may match at any depth, and matching is case-insensitive on Windows. The list is not configurable from the command line; DefaultDenyGlobs() returns a copy for code that builds its own Policy.
The deny list is a list of well-known names, not a classifier. A private key stored under an unusual name inside an allowed root is not caught by it; keep roots narrow rather than relying on the globs.
Symlinks, file type and size
CheckWithExtraRoots performs the checks in this order and stops at the first failure:
- Empty or whitespace path:
path is empty. - Clean and make absolute. Check the requested path against the roots (
path is outside the allowed directories) and the deny globs (path matches a denied pattern (<glob>)). - Resolve symlinks with
filepath.EvalSymlinks. If the resolved path differs, check it against the roots and deny globs again; a failure is reported with the prefixsymlink target:. A symlink planted under/etcthat points at/root/.ssh/id_rsais therefore refused, as is one pointing at/etc/shadow. statthe resolved path; anything that is not a regular file (a directory, a device, a FIFO, a socket) is refused withpath is not a regular file.- Refuse files larger than
MaxBytes, defaultDefaultMaxBytes(1 MiB):file exceeds the maximum readable size (<n> bytes, limit <cap>).
Only after all five does the tool open the file. Because the deny check runs before stat, a denied name is refused whether or not the file exists. The following is real output from get_app_config under the default policy, with the tool's readable flag, its error field and redaction_summary.total_redactions:
/etc/shadow readable=false path refused by policy: path matches a denied pattern (/etc/shadow*)
/root/.ssh/id_rsa readable=false path refused by policy: path is outside the allowed directories
/etc/ssl/private/x.pem readable=false path refused by policy: path matches a denied pattern (/etc/ssl/private/**)
/etc readable=false path refused by policy: path is not a regular file
/etc/hostname readable=true format=unknown total_redactions=0
<root>/etc/link.conf -> /etc/passwd, with --allowed-paths <root>
readable=false path refused by policy: symlink target: path is outside the allowed directories
Path traversal through .. is neutralised by the clean-and-absolute step before the root check, so /etc/../root/.ssh/id_rsa is evaluated as /root/.ssh/id_rsa.
For get_app_config a refusal is returned as a normal result with readable: false, not as a JSON-RPC error, so the audit event records a success result for the call; the refusal is visible in the tool output. The lock-file readers behave the same way: the call succeeds and the policy error appears in the result's error field, there without the path refused by policy: prefix, with an empty dependency list.
# Narrow the roots to one application's configuration
secronyx --allowed-paths /etc/myapp
# Several roots; the deny list and 1 MiB cap still apply under each
secronyx --allowed-paths /etc/myapp,/srv/myapp/config,/opt/myapp/etcInteraction with other controls
- Both policies are process-global and are installed in
mainbefore the tool registry is built, so they also apply to--querymode on the Command line. - Neither policy is a substitute for scope. A caller without the
networkscope cannot reachget_network_latencyat all, and one withoutsoftwarecannot reach the file readers; see Scopes and authorization. - File contents that pass the path policy are still redacted; see Redaction.
- Probe and file activity appears in the audit trail as
tools/callevents with the tool's parameters, including the targets and paths requested; see Audit logging. - In containers, the roots refer to the container's filesystem. If host paths are mounted for diagnostics, mount them under an allowed root read-only rather than widening
--allowed-pathsto the mount's parent; see Docker and Compose.
Built 2026-09-19. Source: levantar-ai/secronyx. Found a mistake? Tell us.