PYXA calls out to scanners, cloud APIs, threat intelligence feeds (CISA KEV, VulnCheck, EPSS, Microsoft's update guide, CERT advisories, exploit catalogues), AI providers, ticketing systems, webhooks and the identity provider. Every one of those HTTP calls passes one check, core/egress.py. This page covers what that check does, how to run it, and what it cannot see.
What the guard does
PYXA wraps requests.Session.send once, at start-up (core/apps.py). Every connector, feed client and provider in the codebase sends through it, including the module-level helpers (requests.get() builds a Session) and each hop of a redirect. Before a request leaves, the guard:
- Matches the destination host against the approved list:
- built-in feeds:
www.cisa.gov,api.first.org,api.vulncheck.com,api.msrc.microsoft.com,www.cert.ssi.gouv.fr,wid.cert-bund.de, the Exploit-DB, Metasploit and PoC-in-GitHub sources, and the Anthropic and OpenAI APIs; - hosts named by configuration: every URL in a connector's settings, a cloud connector's default API host (only for a connector of that type), ticketing base URLs, AI provider URLs, webhook targets, the OIDC issuer and endpoints, each CSAF provider added under Threat intel > CERT advisories, and the distribution hosts that provider's metadata names (recorded when it is read, so documents served from another host than the metadata are admitted on the provider's word);
- hosts an administrator approved, on the page or with
PYXA_EGRESS_EXTRA_HOSTS.
- built-in feeds:
- Applies the mode:
- Observe (the default): the call is sent and recorded. A host outside the list is flagged on the page and logged as a warning.
- Enforce: a call to a host outside the list is refused before a socket opens, and so is a redirect that leads to one. The caller receives a
ConnectionErrornaming the host and where to approve it, so a connector's sync fails with that message instead of a timeout.
- Records the call against its host: calls, server errors and failed connections, refusals, calls sent with certificate verification off, bytes received, last status, last method and the PYXA module that made it.
- Logs one JSON line on the
core.egresslogger (see below).
Turning on enforce mode
- Run in observe mode for at least one full cycle of every schedule: daily feeds, each connector's sync, the monthly Patch Tuesday reads.
- Open Administration > Outbound connections. Every row marked "Not configured" is a host PYXA called that no configuration names. Approve the ones that are expected, such as a proxy or a regional API host. Find out why the others were called before going further.
- Switch the mode to Enforce. The worker picks up the change within 30 seconds.
- To keep the setting out of reach of the administration UI, fix it in the deployment:
# .env
PYXA_EGRESS_MODE=enforce
PYXA_EGRESS_EXTRA_HOSTS=proxy.example.com,*.vendor.exampleWith PYXA_EGRESS_MODE set, the page shows the mode and refuses to change it, so a compromised administrator account cannot switch back to observe.
Every change to the policy and every approval is written to the audit log (egress_policy_change, egress_destination_approve, egress_destination_revoke).
Is a destination compromised?
Most of these checks need no configuration.
| Risk | What PYXA does | Where to see it |
|---|---|---|
| A call to a host nobody configured (a tampered dependency, an injected URL) | Flagged in observe mode; refused in enforce mode | Outbound connections, row state; core.egress warning |
| A feed served from somewhere else after a redirect | Each redirect hop is checked like a new call | Row of the redirect target |
| TLS interception, a swapped certificate, a redirected DNS name | Daily certificate check: SHA-256 of the leaf certificate, issuer and expiry, with the previous fingerprint kept when it changes | Row state "Certificate changed"; egress_certificate_changed log event |
| Certificate verification switched off for a connector | Each such call is counted | Row state "N calls without certificate check" |
| A truncated, reformatted or tampered KEV catalogue | A read with more than 5% fewer CVEs than the last applied one is held, not applied | Feed integrity row; the sync fails with the counts |
| A connector URL pointed somewhere new | The new host is approved because configuration names it; the change is in the audit log | Audit log; the destination's first-seen date |
The certificate check runs as the maintenance job Check outbound certificates (enable it in Administration > Maintenance), or on demand with Check certificates on the page. It connects through the same HTTPS proxy and CA bundle that requests uses (HTTPS_PROXY, NO_PROXY, REQUESTS_CA_BUNDLE), so it sees the certificate PYXA's own calls are shown. Behind an intercepting proxy, that certificate is the proxy's, and its issuer is shown on the row.
A KEV read that was held is applied with:
docker compose exec django python manage.py sync_cisa_kev --accept-shrinkSending the log to a SIEM
Each call is one line on the core.egress logger at INFO. With LOG_FORMAT=json the whole record is JSON; the message itself always is:
{"event":"egress","method":"GET","host":"api.vulncheck.com","path":"/v3/index/vulncheck-kev",
"caller":"core.threat_intel.vulncheck:_get_page","approved":true,"mode":"enforce",
"tls_verified":true,"outcome":"sent","status":200,"bytes":48211,"ms":412}| Field | Meaning |
|---|---|
outcome | sent, refused (enforce mode) or error (no response; error names the exception) |
approved | The host is on the approved list |
caller | The PYXA module and function that made the call |
path | The URL path. The query string is never logged, because it can carry a key |
tls_verified | HTTPS with certificate verification on |
Other events: egress_certificate_changed (host, previous and current fingerprint) and egress_feed_held (feed, entries, baseline).
Suggested alerts: any outcome=refused; approved=false in observe mode; any egress_certificate_changed outside a known renewal; any egress_feed_held.
Limits
- SMTP, LDAP and the OpenVAS GMP socket are not covered. They do not use
requests. Their destinations are fixed by their own settings: the mail server, the directory server and the GMP host. - The guard runs inside PYXA's process. It stops a call PYXA's own code makes to the wrong place. It cannot stop code that has already taken over the process, which could open a socket directly. Pair it with an egress rule on the firewall or proxy that allows only the hosts on this page. Export the list from Configured destinations on the page.
- A configured host is trusted as configured. If someone changes a connector's URL, the new host is approved along with the change. The audit log records who made the change, and the destination's first-seen date shows when the new host was first called.
- The feed check is about size, not content. A catalogue that is the usual size but wrong is applied. Only the CISA KEV sync is checked today.
- Counters start when this version starts. Calls made before the upgrade are not on the page.
Files
| File | Role |
|---|---|
core/egress.py | Policy, the Session.send hook, audit, certificate check, feed shrink guard |
core/models_egress.py | EgressSettings, EgressDestination, FeedBaseline |
core/views_egress.py | The Administration page and its API |
core/management/commands/sync_cisa_kev.py | Applies the shrink guard; --accept-shrink |
core/tasks.py check_egress_certificates | Daily certificate check |
Generated from docs/outbound-connections.md in the PYXA repository.