Guides
FAQ
Twenty-five questions a security team asks before allowing an AI agent to read production hosts, answered from the code rather than the marketing.
These are the questions that come up in a security review of Secronyx, answered from the repository. Where the answer is "no, and here is the limit", it says so. Flags, messages and constants are quoted from the source; the Configuration reference has the full flag list and the Tool reference the full catalogue.
What the model can do
Can the AI run arbitrary commands on the host?
No. The MCP server exposes a fixed catalogue of registered tools — 530 in the current build, of which 521 register by default because the nine sensitive tools are opt-in — and tools/call dispatches only on a name in that map. A name that is not registered returns a JSON-RPC error, not an execution attempt:
{"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"Tool not found","data":"get_env_vars"}}There is no tool that takes a command string. Every process the server starts goes through internal/cmdexec, which builds an exec.Cmd from an argument vector; there is no sh -c or cmd /c anywhere in the tree. On Windows the collectors invoke powershell with -NoProfile (and, in the newer collectors, -NonInteractive) and a fixed script, and any caller-supplied value embedded in that script passes through cmdexec.PSQuote after being checked against an allowlist. ValidateIISName accepts only letters, digits, spaces, ., _ and -, so a hostile IIS site_name produces a refusal from the tool, not a command.
Can any tool write to the host, or change its state?
No tool performs a write, starts or stops a service, kills a process, or modifies configuration. The catalogue is collectors, plus two report builders (generate_system_report, generate_iis_report) that only aggregate collector output and return it as JSON. This is deliberate and permanent: remediation goes through your existing change process with the diagnosis attached. The practical consequence for incident response is that the model can describe a compromise in detail and contain nothing; see the incident triage pattern on the Use cases page.
Two qualifications. Collectors run as the server's own user, so OS-level least privilege still matters — run it as a dedicated low-privilege account. And two tools open outbound connections: get_network_latency, whose targets are policed (see below), and get_cloud_environment, which probes the fixed instance-metadata endpoints to identify the host.
What happens if a hostile log line prompt-injects the model?
Tool output is data the model reads, and a log line, a process command line or a config file can contain text crafted to steer a model. The server does not attempt to detect that. What it does is make the consequence bounded: the model's only capability is to call another registered tool, within the scopes its identity holds, and every one of those calls is read-only and audited. An injection can at worst cause the model to make queries the caller was already authorised to make, and the audit log shows which ones. The mitigation is architectural rather than filter-based, which is the honest position — filtering prose for injection is not a control a security team should rely on.
Does an empty result mean the finding is negative?
No, and this trips people up. A tool with no implementation for the running platform returns a well-formed empty result rather than an error, so that the same catalogue can be advertised on every host. get_journal_logs on Windows returns entries: [] with count: 0. Check the platform column in the Tool reference before reading an empty list as evidence of absence. Some tools also return a populated error field inside an otherwise valid result when they lacked the privileges to read a source — for example macOS PF status reports insufficient privileges to read PF status.
Can the model exhaust the host?
There are caps at three layers. A single tool result is capped at DefaultMaxResultBytes, 4 MiB, adjustable with --max-result-bytes; a result over the cap is replaced by an error rather than streamed. In the polling and hybrid agent modes, internal/agent/executor.go runs at most DefaultMaxConcurrent (4) queries at once, each under a deadline of DefaultQueryTimeout (30 s) clamped to MaxQueryTimeout (120 s); an overrun returns query "<name>" exceeded deadline of <d> with timed_out set rather than hanging. On the HTTP transport, rate limiting is on by default at 20 requests per second with burst 40, 32 requests in flight (503 Service Unavailable with body server busy beyond that), and an address is locked out after 10 authentication failures.
Exposure and authentication
What does the server listen on by default?
Nothing. The default transport is stdio: the process reads JSON-RPC from stdin and writes to stdout, and opens no socket. That is the configuration a desktop MCP client uses, and its security model is the operating system's — whoever can spawn the process and write to its stdin is the caller. A network listener exists only when you pass --transport http, and the default --listen is 127.0.0.1:8080.
What stops someone exposing it to the internet without authentication?
HTTPConfig.Validate in internal/mcp/http.go refuses to start. Binding to a non-loopback address with no authentication configured fails with:
refusing to listen on "0.0.0.0:8080" without authentication: bind to 127.0.0.1, or configure --token/--oidc-issuer/--auth-server, or pass --allow-unauthenticated for an isolated development network
Binding off-loopback with authentication but no TLS fails with a matching message about credentials crossing the network in plaintext. --allow-unauthenticated (or SECRONYX_ALLOW_UNAUTHENTICATED=1) overrides both and logs SECURITY WARNING: serving "<addr>" WITHOUT AUTHENTICATION ... on every start. A static --token shorter than 32 characters is refused on every bind, including loopback, and --allow-unauthenticated does not relax that. The Helm chart mirrors the same rules and will not render without mcp.tls or an explicit mcp.allowInsecure: true.
Which authentication methods are supported, and which should we use?
Six: a static bearer token (--token), OIDC JWT validation against the issuer's JWKS (--oidc-issuer with --oidc-audience), OAuth token introspection (--auth-server with --client-id/--client-secret), hashed API keys (--api-keys-file), SSH request signing (--ssh-authorized-keys or --ssh-ca-keys), and mutual TLS (--tls-client-ca, with --mtls-identity-file, --mtls-trust-domain and --mtls-crl). For human callers who already have a corporate identity, OIDC. For scheduled jobs, an API key with --expires and a CIDR pin. For incident responders, SSH certificates or mTLS, both of which prove possession of a key. A static token is a shared secret with wildcard scopes and belongs only in a lab. The details are on the Authentication page.
How granular is authorization?
Per tool, per identity. Every tool is registered with a scope label; there are 19 scopes. Two gates apply. At registration, --scopes and --enable-sensitive decide which tools exist at all — a withheld tool is absent from tools/list on every transport and is indistinguishable from a misspelling. At request time, an authenticated caller's grants are checked by IdentityAllows; a mismatch returns JSON-RPC -32003 with tool "<name>" requires scope "<scope>" and is audited as denied. A grant matches as *, the bare scope name, or mcp:tools:<scope>; the bare mcp:tools grants nothing, so an IdP can share a scope namespace with other resources. See Scopes and authorization.
What is in the sensitive scope, and why is it off?
Nine tools: get_auth_logs, get_env_vars, get_user_accounts, get_sudo_config, get_ssh_config, get_mac_status, get_certificates, get_process_environ and get_macos_tcc_permissions. They read the places credentials and account structure actually live. ScopePolicy.permits refuses to register them unless EnableSensitive is set, and that is true even if --scopes explicitly lists sensitive. Turn them on for a dedicated incident-response listener, not for routine support.
Does the server need root?
It does not require it, and should not have it. Running as a dedicated low-privilege user costs you coverage on a small number of collectors that read privileged sources — process environments, some firewall and audit subsystem state — which report an error field instead of failing. The container image runs as uid 1000 and the Helm chart sets runAsNonRoot: true. Decide the account's file and device access from the scopes you intend to register, not the other way round.
Can it be protected by a reverse proxy, and does that break the audit trail?
Yes to the proxy. Bind the server to 127.0.0.1 behind a TLS-terminating proxy and the exposure check is satisfied. Be aware that client IPs in the audit log and the rate-limiter buckets will then be the proxy's address for every caller, which also means every client shares one rate-limit bucket. --trust-proxy-headers makes the server read X-Forwarded-For / X-Real-IP instead; it is off by default because a client can forge those headers when the server is directly reachable. Only enable it when the proxy is the sole path in. CORS is likewise off by default — no Access-Control-Allow-Origin header is sent unless --cors-origin lists an origin, and Access-Control-Allow-Credentials is never set.
Data handling
Does the server send anything anywhere by default?
No. In the default stdio configuration the process makes no outbound connection until a tool that needs one is called. Outbound traffic happens in five places: the SaaS agent modes (--mode polling or --mode hybrid, which talk to --saas-url, --ws-url and --wakeup-url); OIDC, which fetches the issuer's discovery document and JWKS; OAuth introspection, which calls the authorization server per token unless the result is cached (--introspection-cache-ttl, default 60s); three vulnerability tools which query https://api.osv.dev/v1/query, https://services.nvd.nist.gov/rest/json/cves/2.0 and https://security-tracker.debian.org/tracker/data/json; and two collectors that reach the local network — get_cloud_environment, which probes the AWS, GCP and Azure instance-metadata endpoints (http://169.254.169.254, http://metadata.google.internal) to identify the host, and get_network_latency, whose targets are policed (see below). If the host must not make outbound calls, do not register those tools and do not use the agent modes.
Does redaction guarantee no secret reaches the model?
No, and treating it as a guarantee is the wrong posture. Redaction is on by default (--redact, default true) and replaces detected values with [REDACTED]. Detection is two-sided: field names matching a keyword list, and values matching secret patterns. A credential that is neither labelled nor shaped like a known token — a bare high-entropy string in an unlabelled config field — is not detected. The controls you actually rely on for the worst cases are scope policy and the path policy, which keep the file or tool out of reach entirely. Disabling it logs WARNING: output redaction is DISABLED; secrets in system state will be returned verbatim. The known limits are listed on the Redaction page.
Does --redact-provider gitguardian send our data to GitGuardian?
It can, and this is worth knowing before you enable it. The GitGuardian provider tries three detection methods in order: if UseAPI is set (it is, by default, in NewGitGuardianProvider) and an API key is present in the provider config or the GITGUARDIAN_API_KEY environment variable, it POSTs the candidate value to https://api.gitguardian.com/v1/scan; failing that, if the ggshield binary is on PATH it shells out to it; failing that it falls back to built-in patterns. With no API key and no ggshield installed, it is purely local pattern matching. If you want offline behaviour, leave the provider at default or make sure neither the key nor ggshield is present.
Can it read arbitrary files?
No. The tools that read a caller-supplied path (get_app_config and the lock-file readers) go through internal/pathpolicy, which resolves the path and its symlink target and requires both to be under an allowed root — /etc, /opt, /usr/local/etc, /srv, /var/lib, /var/www, /app on Unix; C:\ProgramData, C:\inetpub, C:\Program Files, C:\Program Files (x86) on Windows — replaceable with --allowed-paths. Deny globs for key material (**/*.pem, **/*.key, **/.ssh/**, /etc/shadow* and similar) apply inside every root, and the refusal happens before the file is opened. Files above 1048576 bytes are refused. The refusals read path is outside the allowed directories, path matches a denied pattern (<glob>) and file exceeds the maximum readable size. See Network and path policy.
Can it be used to probe our internal network?
Within a policy, but read the default carefully. get_network_latency is the sole tool that opens an outbound connection to a caller-influenced target, and internal/netconfig/probepolicy.go checks the resolved address first: the connection is made to the vetted IP, never to the name. Cloud metadata addresses and hostnames, loopback, link-local, multicast, broadcast and unspecified addresses are all refused, returning target refused by probe policy: <target> (<reason>). DefaultProbePolicy sets AllowPrivate: true, so RFC 1918 addresses are reachable out of the box — if that matters, narrow it with --probe-allow-hosts, which switches to allowlist mode, or add explicit denials with --probe-deny-hosts. Loopback can be permitted with --probe-allow-loopback. Metadata targets stay refused even in allowlist mode, which is the specific control against an injected model being used to mint cloud credentials.
What does the audit log record, and could the log itself leak secrets?
Each event is one JSON Lines record with timestamp, seq, event_id, optional correlation_id, action (tools/call for a tool invocation, auth/token_validation for an authentication decision), resource (the tool name), identity, client_ip, params, result (success, error or denied), error, duration_ns, metadata, prev_hash and hash. Results are not logged — only the arguments. params therefore contains what the caller sent, which is why redaction is on by default and why the audit file should be treated as sensitive: the provider creates it mode 0640, and it should be owned by the service account and shipped to your SIEM. The format is documented on the Audit logging page.
Is the audit log tamper-proof?
Tamper-evident, which is a weaker and more honest claim. Each event's hash is an unkeyed SHA-256 over the event's fields including prev_hash, writes use O_APPEND, and secronyx --audit-verify --audit-output <file> walks the chain and prints Audit verification OK: <n> events verified or one of hash chain broken at event <n> (seq=<m>), hash mismatch at event <n> (seq=<m>) or failed to parse event at line <n>. Because the chain is unkeyed, a party with write access to the file and knowledge of the format could regenerate a consistent chain. Shipping events off the host as they are written is what turns tamper-evident into tamper-resistant. Note also that the chain continues across rotation, so verifying a single rotated file in isolation correctly reports a break at event 1; verify the concatenation.
Can auditing silently stop?
Not silently. If the default provider cannot open its output file it logs WARNING: audit provider "default" could not open "<path>" (<cause>); audit events are being written to stderr instead and keeps writing to stderr; the following line, Audit logging enabled: provider=<name> output=<path>, shows provider=stderr when the fallback is active. In a read-only-root container this is the expected result of the default --audit-output /var/log/secronyx/audit.jsonl, which is why the Helm chart sets /dev/stdout. Turning auditing off entirely requires --no-audit and logs WARNING: audit logging is DISABLED.
Operations and assurance
How quickly can a compromised credential be contained?
It depends on the method, which is a reason to prefer some over others. An API key is revoked with secronyx apikey revoke, which sets disabled in the store; the authenticator re-reads the file when its mtime changes, polling at most once every two seconds, so revocation takes effect within seconds and without a restart; the caller then sees api-key: API key <id> is revoked. An SSH key line is removed from the authorized-keys file, or its expires= passes. A client certificate is added to the CRL named by --mtls-crl, giving mtls: client certificate serial <n> is revoked. An OIDC token cannot be revoked at the server, so bound its lifetime with --oidc-max-token-lifetime (default 24h) and consider --oidc-require-jti, which makes each token single-use via the replay cache. A static --token requires a restart, which is one more reason not to use one. In every case the blast radius is bounded by that identity's scopes, and the audit log shows exactly what was read.
Does the server keep any state on disk?
Very little, and none of it is collected system data. The audit log is the only record of activity. The analytics scope keeps a purely in-process sample history, capped at MaxSamplesPerResource (1440 samples per resource, roughly a day at one per minute) and lost on restart — which is why forecasts carry the caveat extrapolated from in-process samples, not from a persistent metrics store and a confidence capped at 90. The API key store is a JSON file of hashes; on Unix, start-up fails if it is group- or other-readable, with API key store "<path>" is readable by other users (mode <mode>); chmod 600 it (the check is skipped on Windows). The agent modes keep credentials and certificates under --config-dir (default ~/.secronyx). Tool results are not cached or persisted.
Can it run in an air-gapped environment?
Yes, with the obvious exclusions. The Linux release binaries are built with CGO_ENABLED=0 and -tags=netgo, so they are statically linked, and stdio or HTTP with API keys, SSH signing or mutual TLS needs nothing external. OIDC and OAuth introspection both need to reach the issuer, so they are unsuitable. The three vulnerability tools (get_vulnerabilities_osv, get_vulnerabilities_nvd, get_vulnerabilities_debian) will fail; the SBOM tools (get_sbom_cyclonedx, get_sbom_spdx) are local and will not. Leave the agent modes off. Use --scopes to omit what cannot work rather than letting callers discover it.
What assurance do we have about the build and its dependencies?
Releases for Linux amd64 and arm64 are built by the SLSA GitHub generator (builder_go_slsa3.yml) and every published asset gets a GitHub build-provenance attestation via actions/attest-build-provenance; macOS and Windows binaries are built natively on their own runners and do not carry SLSA provenance. The module is github.com/levantar-ai/secronyx at Go 1.23, with a deliberately small direct dependency set: the AWS SDK v2 pieces used by the SaaS agent modes, coder/websocket, golang-jwt/jwt/v5, google/uuid, prometheus/client_golang, golang.org/x/crypto and golang.org/x/sys. CI runs golangci-lint and gosec, and lefthook enforces the same gates plus tests before a push. Releases and versioning covers what each release contains; Installation covers verifying an asset before you run it.
What licence is it under, and how do we report a vulnerability?
The repository's LICENSE is the GNU Affero General Public License v3, an OSI-approved open-source licence: Secronyx is free and open source, for any use including commercial use, on any number of hosts, with every feature enabled. Running it unmodified asks nothing of you; if you distribute modifications, or offer a modified version to users over a network, AGPL asks you to publish those modifications under the same licence. Querying the agent from your own software over MCP or HTTP does not put your software under the AGPL. A commercial licence for closed-source embedding, or for hosting modified versions without publishing them, is offered in COMMERCIAL-LICENSE.md. Read Licensing and take your own advice before redistributing. Security issues go to security@secronyx.com under coordinated disclosure, with 90 days requested before public disclosure; see Reporting a vulnerability.
Related pages: Security model, Compared with alternatives, Compliance mapping, Troubleshooting, Use cases, and the Secronyx homepage.
Built 2026-09-19. Source: levantar-ai/secronyx. Found a mistake? Tell us.