Product documentation

ALLOD | SWG

A self-hosted secure web gateway - inline firewall, DLP, CASB and ZTNA - running entirely in infrastructure you control.

Overview

ALLOD SWG inspects every outbound request from enrolled endpoints before it leaves your network. Traffic is routed through a proxy node that evaluates firewall, data-loss prevention, and cloud-app control rules in real time. No traffic ever transits Allod Solutions infrastructure.

Key properties:

  • All inspection runs on hardware you control - your cloud or bare metal.
  • Traffic is not retained unless a rule explicitly triggers logging.
  • The event log is encrypted per-device using HKDF-derived keys.
  • Allod Solutions has no access to your traffic, policy, or event log - by architecture, not by promise.

Architecture

ComponentRole
ControllerHosts the admin UI (:8080) and controller API (:8081). Stores firewall and DLP config. In single-node mode, runs the proxy as well.
Proxy nodeStateless CONNECT proxy (:8443). Polls the controller for config every 30 s and applies updates atomically - no connection drops during policy changes.
AgentLightweight daemon on each endpoint (macOS, Windows, Linux). Configures the OS proxy to route all traffic through the proxy.
ConnectorOptional daemon on private networks. Dials out to the controller so agents can reach internal resources without inbound firewall rules.

Data flow

  1. The agent configures the OS proxy to route all traffic through the proxy.
  2. The proxy decrypts TLS (when inspection is enabled), evaluates firewall, DLP, and CASB rules, then forwards or blocks.
  3. The proxy re-encrypts with a leaf certificate signed by the SWG CA (installed on each device by the agent or via MDM).
  4. Rule-triggered events are written encrypted to the event log. Non-matching traffic is not retained.
QUIC / HTTP-3 Full inspection (quic_mode: inspect) decrypts QUIC/HTTP-3 the same way ordinary TLS inspection works - a local per-device relay redirects outbound UDP/443, terminates HTTP/3 with a per-SNI certificate, and forwards through the same DLP/firewall evaluation and identity headers as any other inspected connection. Supported on Linux and Windows agents, live-verified end-to-end including the real OS-level redirect itself (nftables DNAT on Linux, WinDivert on Windows, not just the relay logic downstream of it) and real headless-browser tests confirming Chrome and Edge both complete the QUIC handshake and serve real HTTP/3 traffic over the installed CA. macOS agents still fall back to downgrade regardless of this setting - no redirect mechanism exists there yet. Firefox is a separate, browser-level limitation unaffected by any of this: it hard-rejects HTTP/3 over a non-public root CA by design (network.http.http3.disable_when_third_party_roots_found), on every platform - inspecting Firefox's own QUIC traffic isn't possible regardless of agent platform, only Chrome/Chromium-based browsers can be. quic_mode: downgrade itself - reject outbound UDP/443 so the browser retries over TCP, where ordinary TLS inspection applies - has parity across all three platforms: nftables/iptables on Linux, the Windows Firewall on Windows, and pf on macOS.

Quick start

Single node (SQLite)

Download the allodswg binary and create a minimal config:

# server.yaml
server:
  controller_api: ":8081"
  admin_ui: ":8080"

proxy:
  listen: ":8443"

ca:
  organization: "My Organisation"
  country: "SE"

data_dir: "./data"
firewall_config: "./firewall.yaml"
dlp_config: "./dlp.yaml"
allodswg -config server.yaml

The admin UI is at http://localhost:8080. Create an API key under Admin → API Keys, distribute the agent to endpoints with agent.yaml pointing at the controller URL.

CA distribution A self-signed CA is generated in data/ca.pem on first start. Push it to endpoints as a trusted root (via MDM or group policy) before enabling TLS inspection.

Scale out (multi-node)

Migrate the event store to PostgreSQL and deploy additional stateless proxy nodes:

# server.yaml addition
event_store:
  driver: "postgres"
  dsn: "postgres://swg:pass@db:5432/swg?sslmode=require"
# proxy.yaml (additional node)
controller_url: "https://controller.example.com:8081"
api_key: "sk_myrs_..."
listen: ":8443"
ca_path: "/etc/allodswg/ca"
config_poll_interval: "30s"

server.yaml reference

Passed to the controller via allodswg -config /path/to/server.yaml.

server:
  controller_api: ":8081"   # Used by proxy nodes and VRM
  admin_ui: ":8080"         # Web UI and REST API
  admin_ui_tls: ""        # HTTPS listen addr for the admin UI, e.g. ":443" - alternative to tls_mode below
  metrics: ":9090"          # Prometheus metrics (optional)
  tls_mode: "off"          # auto | custom | acme | off
  public_hostnames: []    # added to the admin UI's TLS cert SANs, e.g. ["swg.example.com"]
  auto_update_agents: true
  # Controller-API flood defense (protects the agent-facing management API,
  # port 8081, from connections that never present valid credentials) -
  # opt-in, 0/unset = disabled. See the Firewall section's flood-defense callout.
  api_headerless_rate_limit: 0
  api_headerless_rate_window_seconds: 0
  api_baseline_rate_limit: 0
  api_baseline_rate_window_seconds: 0
  api_global_fingerprint_rate_limit: 0       # catches a flood spread across many source IPs sharing one ASHFP1 fingerprint
  api_global_fingerprint_rate_window_seconds: 0
  acme:
    directory: ""          # ACME directory URL, e.g. Let's Encrypt production/staging
    email: ""
    domains: []
    eab_kid: ""           # external account binding, if your CA requires it
    eab_hmac_key: ""

proxy:
  listen: ":8443"
  upstream_dns: ""         # Override DNS resolver; empty = system
  idle_timeout: "5m"
  require_agent: false     # Reject non-agent connections
  identity_requirement: "none" # none | idp_user | platform_sso
  quic_mode: "bypass"       # bypass | downgrade | inspect (Linux/Windows only - see the QUIC / HTTP-3 callout below)
  quic_mode_unsupported: "downgrade" # downgrade | metadata - fallback on platforms where inspect isn't supported
  quic_listen: "0.0.0.0:8443"
  enable_h2: true
  smtp_inspection: false
  mail_retrieval_inspection: false
  network_write_inspection: false
  # Same flood-defense pattern as server.api_* above, but for the proxy's
  # own listener (protects against unauthenticated connection floods).
  max_connections: 0
  max_connections_window_seconds: 0
  headerless_rate_limit: 0
  headerless_rate_window_seconds: 0
  global_fingerprint_rate_limit: 0
  global_fingerprint_rate_window_seconds: 0
  credential_domains: []   # your corporate email domains, e.g. ["company.com"] - detects company credentials submitted to a non-approved host
  reserved_hostnames: []  # memorable proxy-only addresses (e.g. https://access) - see ZTNA & Connectors below
  block_page:
    org_name: ""
    logo_url: ""
    support_email: ""
    message: ""
    header_color: ""
    accent_color: ""

ca:
  path: "./data/ca.pem"
  regenerate: false
  algorithm: "ecdsa_p256"   # ecdsa_p256 | rsa_2048 | rsa_4096
  ca_validity_days: 365
  leaf_validity_days: 7
  organization: "My Organisation"
  country: "SE"
  # See "TLS & CA" below for how this is stored under Controller HA (event_store.driver: postgres).

logging:
  level: "info"             # debug | info | warn | error
  format: "text"            # text | json
  access_log: true
  access_log_file: ""
  access_log_max_size_mb: 0
  access_log_max_backups: 0
  access_log_ring_size: 0
  event_log:            # structured event export - see the Integrations section for how these compare to LogScale/Zeek
    file:
      enabled: false
      path: ""
      max_size_mb: 0
      max_backups: 0
    beats:
      enabled: false
      address: ""       # Filebeat/Logstash TCP input
    syslog:
      enabled: false

data_dir: "./data"
firewall_config: "./firewall.yaml"
dlp_config: "./dlp.yaml"
suricata_config: "./suricata.yaml"

event_store:
  driver: "sqlite"          # sqlite | postgres - postgres enables Controller HA: config, sessions, API keys, both CAs, and DLP encryption keys all move from local files into Postgres, shared across replicas. See "TLS & CA" below.
  dsn: ""

compliance:
  enabled: false
  require_compliance: false  # block non-compliant devices rather than just flagging them
  scripts: {}            # platform -> script text, see Device compliance below

dlp:
  s3:
    enabled: false
    endpoint: ""
    bucket: "allod-dlp"
    access_key: "${DLP_S3_ACCESS_KEY}"
    secret_key: "${DLP_S3_SECRET_KEY}"
    region: "us-east-1"
    retention_days: 90

threat_feed:
  enabled: false
  update_interval: "1h"

# Alert delivery for security events (firewall/DLP blocks, threat-feed matches, etc).
# See the Integrations section for the separate Slack/Teams/PagerDuty JIT-approval channels.
alerts:
  enabled: false
  webhook_url: ""

# DNS-exfiltration detection (tunneling/beaconing over DNS queries).
detections:
  dns_exfiltration:
    enabled: false

license:
  key: ""

# integrations: - see the dedicated Integrations section below for the full list
#   (allod_vrm, logscale, zeek, crowdstrike_falcon, misp, slack_approval,
#   teams_approval, pagerduty, idp_sync).

# Optional local LLM
# ai:
#   enabled: false
#   model: "llama3"
#   ollama_url: "http://localhost:11434"

Environment variables are expanded: ${VAR} in any string value is replaced at startup. Fields marked deprecated (device_auth, require_idp_user, block_quic) still work but new deployments should use their replacements shown above.

firewall.yaml reference

Path set by firewall_config: in server.yaml.

default_policy: "allow"          # allow | block
default_tls_inspection: false

# ZTNA: CIDRs pushed to agents for transparent interception
access_routes: []
#  - "10.0.0.0/8"

rules:
  - action: block
    type: category
    value: malware

  - action: allow
    type: domain
    value: "*.internal.example.com"
    process: curl

  - action: block
    type: region
    value: "RU,BY"

  - action: allow
    type: category
    value: cloud_storage
    tls_inspection: enforce

Top-level fields:

FieldTypeNotes
default_policyallow / blockAction taken when no rule matches.
default_tls_inspectionboolWhether to decrypt/inspect TLS by default when no rule overrides it.
access_routes[]stringZTNA: CIDRs pushed to agents for transparent interception via type: access rules.
virtual_subnetstring, optionalCIDR for ZTNA device virtual IPs. Defaults to 100.64.0.0/10 when empty.
rules[]RuleEvaluated in order; first match wins. See below.

Rule fields (every field below action/type/value is an optional AND-condition; empty string or * means "any"):

FieldValuesNotes
actionallow / block / alert / proxyalert allows the connection but also dispatches to external webhooks. proxy routes through a connector for private resources (used with type: access).
typedomain / category / region / country / ip / process / app / accessWhat value matches on. country is a legacy alias for region that only matches country codes, not continent prefixes. ip matches the destination IP/CIDR. process matches the initiating process name directly as the rule's primary match (equivalent to using the process scope field below, but as the rule type). app matches a recognized SaaS application identifier (e.g. google-drive, dropbox, slack), independent of which domain served it. access is ZTNA: allow/block TCP access to internal destinations.
valuestringDomain glob, category/app name, comma-separated ISO region codes ("RU,BY" or "cont:EU"), or for type: ip: comma-separated IPs/CIDRs each optionally suffixed :port or :port-range (e.g. "10.0.0.0/8:443", "10.0.0.0/8:80-443"); omitted port = any.
destinationstringOnly for type: access: comma-separated CIDR:port patterns, e.g. "10.0.0.0/8:22,10.0.1.0/24:*".
devicestringComma-separated device IDs (glob ok).
processstringMatch only traffic from this process name.
userstringOS username (comma-sep, glob ok).
groupstringIdP group name (comma-sep, glob ok); requires SCIM.
labelsstringContainer/pod labels, comma-separated key=value entries (value glob ok, e.g. "team=platform-*"); matches if any entry matches. Requires container-aware egress attribution (Linux, Docker/Podman; Windows and macOS via Docker Desktop are experimental - see below).
regionstringRestrict to destination region - ISO country code or cont:XX continent (comma-sep).
client_countrystringRestrict to the source device's own country - ISO country code or cont:XX (comma-sep).
source_ipstringRestrict to the connecting client's IP - comma-separated CIDRs/exact IPs.
ja3stringGlob match against JA3 TLS fingerprint (legacy, license-independent).
ja4stringGlob match against JA4 TLS fingerprint (requires the ja4 license feature).
ashfp1stringGlob match against ASHFP1 HTTP fingerprint (requires TLS inspection).
hasshstringGlob match against HASSH SSH client fingerprint.
mercurystringGlob match against Mercury-identified app name (fallback when ja4 isn't licensed).
referer_sourcestringClick-origin category, comma-separated (e.g. "webmail,social_media,chat").
referer_domainstringHostname glob matched against the referer (e.g. "*.gmail.com").
extension_initiatedboolMatch only requests carrying an Origin: chrome-extension://... or moz-extension://... header - i.e. actually initiated by a browser extension's own script, not a page or user directly. See below.
tls_inspectionbypass / enforceOverride the default TLS inspection setting for this rule. Empty inherits default_tls_inspection.
compliancepass / fail / unknownDevice compliance state at connection time; unknown = not yet checked.
authsso / jitEmpty = no step-up required. sso demands re-authentication; jit grants time-boxed access.
auth_ttlGo duration, e.g. "4h"Grant lifetime when auth is set. Empty defaults to 8h.
jit_approvalself / approvalOnly meaningful when auth: jit. self auto-grants; approval requires an approver to act.
jit_approversstringComma-separated approver addresses/webhook targets notified on a pending JIT request.
jit_approver_groupstringComma-separated IdP group name(s) (glob ok) authorized to decide a pending JIT request; requires SCIM.
sensitiveboolFlags this destination as worth protecting for screenshot/screen-share detection, independent of auth.

suricata.yaml reference

Path set by suricata_config: in server.yaml. See IDS/IPS (Suricata) for the supported rule syntax and how matches are evaluated.

enabled: false              # off by default - a brand-new detection surface shouldn't
                            # start blocking traffic until you turn it on deliberately

custom_rules_text: |
  alert http any any -> any any (msg:"internal test rule"; content:"/gate.php"; http.uri; sid:1000001;)

overrides:
  "84769458": alert    # silence a noisy sid from the curated feed
  "1000001": block     # promote one of your own custom rules to block
FieldTypeNotes
enabledboolSingle on/off switch for all Suricata rule evaluation - both your custom rules and the curated feed. Rules keep loading and refreshing either way; this only gates whether matches are ever acted on.
custom_rules_textstringRaw Suricata .rules text, one rule per line. Same content the IDS/IPS card on the Firewall admin page imports - editing this file and editing that card are two ways to reach the same state. A malformed line is skipped, not fatal - the rest of the file still loads.
overridesmap (sid → action)Per-rule action override, keyed by the rule's Suricata sid (as a string). Values: block / alert / allow. Applies regardless of which source (custom or curated feed) the sid came from - this is the only way to change what a curated-feed rule does, since that ruleset itself isn't yours to edit.

The curated abuse.ch URLhaus feed is never written to this file - it's licensed and cached separately (see /lic/v1/suricata-urlhaus) and reloads on its own schedule independent of this config.

dlp.yaml reference

Path set by dlp_config: in server.yaml.

mode: "headers"       # off | headers | selective | full
upload_threshold: 0   # Flag uploads over this size in bytes; 0 = disabled
min_size: 0

# post_detection_window: "15m"
# selective_categories:
#   - cloud_storage

keyword_lists:
  - name: "PII"
    description: "Personally identifiable information"
    keywords: ["personal number", "ssn", "passport"]

rules:
  - action: block
    direction: upload
    match: keyword_list
    value: PII

  - action: alert
    direction: upload
    inspect: host
    domain_category: cloud_storage
    match: glob
    value: "*"

  - action: block
    direction: upload
    inspect: body
    match: regex
    value: '\b4[0-9]{12}(?:[0-9]{3})?\b'

  - action: block
    direction: upload
    inspect: body
    match: builtin_pattern
    value: "@secret:aws_access_key"

Top-level fields:

FieldTypeNotes
modeoff / headers / selective / fulloff: raw-piped, zero overhead. headers: peeks at the first request's headers per TLS connection only - misses keep-alive follow-ups. selective: full inspection for domains in selective_categories, headers for everything else. full: parses every HTTP request/response pair.
upload_thresholdint64 (bytes)Auto-flags uploads over this size regardless of rule matches. 0 = disabled.
min_sizeint64 (bytes)Minimum size before any rule is evaluated. 0 = no minimum.
selective_categories[]stringDomain categories that get full inspection when mode: selective.
post_detection_windowGo duration, e.g. "15m"Only meaningful when mode is headers or selective: how long a device is escalated to full inspection after it triggers a DLP event.
buffer_and_forwardboolBuffers the full request/response body to disk before forwarding, enabling clean 403 block responses (instead of a mid-stream TCP reset) and inspection of the complete file rather than a 32 KB sample. Tradeoff: client/server waits for the full body to buffer first. Only applies when Content-Length is known and ≤ buffer_max_bytes.
buffer_max_bytesint64 (bytes)Max body size eligible for buffer-and-forward; larger bodies fall back to streaming inspection. Default when 0: 256 MB.
ai_prompt_contextstringFree-text appended to the Ollama DLP classification prompt so admins can describe what counts as sensitive for their org.
auto_delete_alert_downloadsboolOff by default. When on, the agent permanently deletes a downloaded file 24h after it was tagged DLP alert and left untouched (warns the user an hour before).
email_origin_retention_daysintHow long Cross-Protocol Attachment Lineage records are kept before pruning. 0 or negative = 30 days.
junk_mailbox_names[]stringExtends the built-in list of IMAP folder names treated as junk/spam (for nonstandard hierarchies, e.g. Dovecot's INBOX.Junk).
keyword_lists[]KeywordListname, optional description, keywords ([]string) - referenced from a rule's value when match: keyword_list.
rules[]RuleEvaluated in order; first match wins. If none match, no event is emitted. See below.

Rule fields (inspect/match/value/action define what's matched and what happens; every other field is an optional AND-condition scoping when the rule applies - empty string or * means "any"):

FieldValuesNotes
inspectfilename / content_type / host / body / ja4 / ashfp1 / entropyWhich field to examine. filename also matches filenames inside archives (requires full). body requires full. ashfp1 requires full or headers. entropy is the Shannon entropy of the body sample in bits/byte (0-8), used with match: gt/lt.
matchglob / regex / keyword_list / builtin_pattern / gt / ltglob: shell pattern (*, ?). regex: threshold sets the minimum match count (0/1 = any). keyword_list: value names a list under keyword_lists; threshold sets the minimum keywords that must match. builtin_pattern: value is one of the built-in detectors below. gt/lt: numeric comparison, value is a float (used with inspect: entropy).
valuestringThe glob/regex/list-name/pattern-id/number to match, per match above.
thresholdint0/1 = any match; >1 = minimum match/keyword count. Used with regex and keyword_list.
actionlog / alert / block / bypassWhat happens when this rule matches. log records and lets the transfer through; alert additionally dispatches to external webhooks; block aborts the transfer; bypass suppresses the event entirely (no log, no block).
directionupload / download / removable_write / local_write / screenshot / screen-share / print / network_share_read / network_write / credential / transform / key_generation / vdi_file_drop / clipboard_vdi_read / supply_chain_read / connect / dnsEmpty = any. Not limited to upload/download - matches the event's own direction tag, so a rule can scope to any of these channels specifically (e.g. removable_write for USB-drive policy - see DLP → Removable media).
devicestringComma-separated device IDs (glob ok).
processstringProcess name.
userstringOS username (comma-sep, glob ok).
groupstringIdP group name (comma-sep, glob ok); requires SCIM.
domainstringGlob matched against the destination hostname.
domain_categorystringDomain category name.
ja3stringGlob match against JA3 TLS fingerprint (legacy, license-independent).
ja4stringGlob match against JA4 TLS fingerprint.
ashfp1stringGlob match against ASHFP1 HTTP fingerprint.
mercurystringGlob match against Mercury-identified app name (fallback when ja4 isn't licensed).
hasshstringGlob match against HASSH SSH client fingerprint.
referer_sourcestringe.g. "webmail", "chat", "direct".
referer_domainstringHostname glob matched against the referer host.
regionstringISO country code or cont:XX continent prefix (same syntax as firewall region rules).
client_countrystringISO country code or cont:XX for the source device's own country.
source_ipstringComma-separated CIDRs/exact IPs matched against the event's client IP.
clipboard_source_appstringGlob matched against the app that owned the clipboard at copy time.
clipboard_source_pagestringGlob matched against the window title/browser tab at copy time.
compliancepass / fail / unknownDevice compliance state.
company_tenantyes / noWhether the destination is an allowed CASB tenant.
origin_domainstringGlob matched against the file's origin-sighting host. Empty origin never matches a non-empty pattern.
origin_localyes / noWhether the file originated from a ZTNA connector-routed internal resource.
origin_sharepoint_sitestringGlob matched against the file's origin SharePoint site/OneDrive slug.
origin_shared_drivestringGlob matched against the file's origin Google Shared Drive name.
origin_email_domainstringGlob matched against the sender domain recorded when this file's SHA-256 was previously seen as an inbound email attachment (see Cross-Protocol Attachment Lineage).
origin_email_taggedyes / noWhether this file's SHA-256 was previously seen as an inbound email attachment.
email_sender_internalyes / noWhether the SMTP MAIL FROM domain is one of the org's own SCIM domains - see Personal-account leak prevention.

Built-in patterns (match: builtin_pattern, used as value):

ValueDetects
@pci:credit_cardCredit card numbers
@pii:se_personnummerSwedish personal identity numbers
@pii:ibanIBAN bank account numbers
@pii:emailEmail addresses
@pii:ssn_usUS Social Security numbers
@secret:private_keyPEM-format private keys
@secret:aws_access_keyAWS access key IDs
@secret:github_tokenGitHub personal access tokens
@secret:slack_tokenSlack API tokens

agent.yaml reference

Placed alongside the agent binary. Default paths: /etc/allodswg/agent.yaml (Linux/macOS), C:\ProgramData\allodswg\agent.yaml (Windows).

controller:
  url: "https://proxy.company.com:8081"
  api_key: "sk_myrs_..."    # RoleEnrollment key from Admin → API Keys

agent:
  heartbeat_interval: "30s"
  startup_timeout: "30s"
  retry_interval: "10s"
  max_retries: 5
  daemon_mode: true
  verify_interval: 60       # seconds between self-checks that the proxy config is still applied
  proxy_addr: "proxy.company.com:8443" # the real SWG proxy the agent's local proxy forwards to
  local_proxy_port: 18443  # port the agent listens on locally; the OS proxy setting points here
  tunnel_ports: [9003]   # local ports reachable via the agent's reverse tunnel - see ZTNA & Connectors
  intercept_local_https: false # also intercept RFC 1918 HTTPS - for proxy dev/testing on a local network only

# Optional platform-level identity verification (e.g. Azure AD IWA on Windows).
# Must match the controller's own OIDC configuration; the device must already
# be enrolled in the corporate IdP.
oidc:
  issuer_url: ""
  client_id: ""

Distribute alongside the agent binary via MDM. The agent auto-installs the CA certificate on supported platforms.

proxy.yaml reference

Passed to stateless proxy nodes via allodswg-proxy -config /path/to/proxy.yaml. Not needed in single-node deployments.

controller_url: "https://controller.example.com:8081"
api_key: "sk_myrs_..."          # RoleProxy key
listen: ":8443"
ca_path: ""                # optional; empty = fetch the signing CA from the controller
data_dir: "/var/lib/allodswg-proxy"
upstream_dns: ""
idle_timeout: "5m"
access_log: true
access_log_json: false
config_poll_interval: "30s"   # how often to re-fetch firewall/DLP config from the controller
connector_listen: ":8444"  # where ZTNA connectors dial in via WebSocket; empty disables the connector hub on this proxy
proxy_url: "ws://tokyo-p1.example.com:8444" # this proxy's externally reachable URL, reported to the controller's connector registry - required when connector_listen is set

Firewall

Rules are evaluated top-to-bottom. The first match wins; unmatched traffic follows default_policy. Domain categories are managed in Admin → Categories and visible in the CASB view.

Process-aware filtering: When the agent reports the originating process, rules can target specific processes - e.g. allow curl to reach internal services while blocking a browser on the same domain.

Source IP scoping: any firewall or DLP rule can also be scoped to a source IP or CIDR range, alongside the existing device, user, and group scoping - restrict a rule to traffic from a specific subnet (an office network, a jump host) without needing a separate device or ZTNA-based condition to express it.

Container-aware egress: the agent identifies which container actually made a connection - instead of every container on the box being indistinguishable behind the host's own IP and the runtime's shim process. A labels rule matches on that container's own labels, so a policy can say "only the container labeled app=payment-gateway may reach api.stripe.com - every other container on this host is blocked from it," without needing a separate network segment or firewall per container. Works out of the box with whatever labels you already set via docker run --label or a Compose file's labels: - nothing extra to configure on the container side. k3s (pod name/namespace) support is planned but not yet available; this currently covers Docker and Podman.

Platform support: fully supported on Linux (Docker, Podman). Windows and macOS, both via Docker Desktop, are experimental: identification works when the agent transparently intercepts the connection, but on Windows a container's traffic can instead be routed through the agent's system-proxy path - which lacks the information needed to reliably attribute it back to the originating container. Don't rely on the labels condition as the sole enforcement layer for a security-sensitive policy on Windows or macOS yet; pair it with a process- or destination-based rule as a backstop. macOS support has not yet been validated against a real deployment.

Browser-extension request detection: a malicious or compromised browser extension with broad host permissions can read page content from any tab open in the browser - an internal CRM, say - and exfiltrate it via a request its own background script makes. That traffic comes from inside the browser process itself, so it looks identical to ordinary browsing to any process-based detection. Because TLS is already terminated here, every request's Origin header is checked for the chrome-extension:// (Chrome, Edge, Brave, Opera) or moz-extension:// (Firefox) scheme - a page a user is looking at never sets one, only a request the extension's own script makes does. An extension_initiated rule condition lets you block or alert on that signal for a given destination or category; independent of any rule, every extension ID seen is also logged fleet-wide and browsable on the CASB Extensions tab, so you can see what extensions are actually talking to the network before deciding what to restrict. Nothing is blocked by default. Safari extensions use a different mechanism and aren't covered.

Fingerprint matching: a ja4 rule matches the TLS client fingerprint and requires the JA4 license feature. Without that license, a mercury rule matches the same class of traffic by application name instead - a lower-fidelity but unlicensed fallback for the same use case. ja3 is a separate, independently combinable fingerprint condition derived from the same ClientHello - both a JA3 and a JA4 condition can scope the same rule at once, since some clients are only well-fingerprinted by one or the other.

DGA & typosquat detection: both run on-device against every resolved domain, with no feed to poll and nothing to keep in sync - they work offline and catch domains no threat feed has seen yet. DGA detection flags algorithmically generated hostnames, the kind malware families like Qakbot use for command-and-control. Typosquat detection matches candidate domains against your approved app catalog, catching lookalikes (e.g. rn swapped for m, 0 for o) that a static block-list would miss because they were never registered before.

Email authenticity checks: inbound mail (IMAP and POP3) is checked against your identity provider's user directory: a From: display name that matches a real employee while the address doesn't belong to them, an address on one of your own domains that matches no provisioned user or a disabled account, and a sending domain your own organization's SPF record doesn't authorize - all classic business-email-compromise signals. A self-learned check catches the same kind of impersonation for outside senders too - a display name (e.g. a courier or vendor brand) seen repeatedly from one domain over time, then suddenly appearing from a different one, without needing a maintained brand catalog. Alongside identity, messages are checked for SPF/DKIM/DMARC alignment, a sending domain registered only days ago, senders or links already flagged by threat intelligence, a lookalike/typosquat link in the body (the same check described above), and suspicious attachments or links - password-protected archives, a file whose real type doesn't match its extension, a macro-enabled document, a script file (even hidden inside a zip), links to a bare IP address or a shortened link, and a link serving a full HTML page directly from public cloud storage (Google Cloud Storage, S3, Azure Blob, and similar) - a known technique for evading domain-reputation checks, since these hosts are otherwise legitimate. A message whose plain-text part is trivial boilerplate while the real content only exists in its HTML part - a way of evading plain-text keyword inspection - is flagged the same way. So is a message whose subject or body contains an invisible/zero-width Unicode character embedded mid-word - splitting a word like "verify" so it renders normally to the recipient but no longer matches literal keyword or brand-name comparison; no legitimate mail generator produces this, so the pattern's presence is itself evidence. "Your own domains" aren't a separate setting to maintain - they're derived automatically from whatever domains are already present in your synced SCIM directory. Unlike the phishing-link checks above, these only ever raise an alert, never block delivery: several of these signals have a higher false-positive ceiling than link matching - shared mailboxes, service accounts, distribution lists, and external partners who happen to share a name with an employee can all trigger it, and mail shouldn't be silently dropped on that basis.

IMAP flagging instead of a desktop notification: for IMAP specifically, a message caught by any of the checks above - or the malware-hash match covered under DLP - is flagged in place with the standard $Phishing IMAP keyword (some mail clients, e.g. Thunderbird, already render it specially), so the signal sits right on the email instead of a desktop popup you'd have to correlate back yourself.

Rule order More specific rules must appear before broader ones. A block-all rule at the end will not prevent earlier allow rules from matching.

IDS/IPS (Suricata)

A Go-native engine that parses a practical subset of Suricata .rules syntax and evaluates it directly against traffic this proxy already decrypts - not an embedded Suricata process, no packet capture. Header/SNI/IP/port-only rules are checked at CONNECT time, in the same detector chain as JA4DB/DGA/typosquat matching (see Firewall); content/pcre/http.*-based rules are checked against the body once it's available, merged with DLP's own verdict via the same stricter-wins logic DLP uses internally. An explicit firewall allow/alert rule always takes precedence and skips Suricata evaluation entirely, same as every other heuristic detector.

Off by default. Toggle Enable IDS/IPS on the Firewall admin page (or enabled: true in suricata.yaml) to activate matching - importing rules never turns it on by itself.

Three independent rule sources: your own custom rules (added and edited one at a time on the Firewall page, or set in bulk via custom_rules_text in suricata.yaml), a curated, licensed feed of abuse.ch's URLhaus Suricata ruleset (tens of thousands of IOC-based rules, updated regularly), and Allod's own vendor-curated ruleset, delivered and refreshed the same way. Reloading one source never drops rules loaded under the other two. Neither curated feed is shown rule-by-rule in the admin UI - only a count and version each - the same treatment every other threat feed (MISP, SSLBL, MalwareBazaar) gets here; only your own custom rules are individually listed and editable.

Actions and the trust-tiered default: a rule's effective action resolves in this order - an admin override (set via the Firewall page's per-rule dropdown, an overrides entry in suricata.yaml, or PATCH /api/suricata/rules/{sid}) always wins; otherwise, a custom rule uses exactly the action you wrote in the rule text (alert/drop/pass); a curated-feed rule is pinned to drop (block) regardless of what the rule text itself says. This isn't an oversight - public Suricata rulesets, URLhaus included, conventionally ship every rule as alert and expect the operator to promote specific ones to drop themselves; respecting that text verbatim would make a curated feed alert-only forever in practice. Override a specific noisy sid down to alert if the pinned-block default is too aggressive for it.

Rule actionEffect
alertLogged and can independently trigger an admin alert. Never blocks.
dropBlocks the connection or request the same way a firewall/DLP block does.
passExplicitly allow - stops evaluating further Suricata rules for this request/connection. Only reachable via an admin override; no default policy ever resolves to it on its own.

Supported rule syntax

Header/5-tuple: protocol, source/dest IP (any, a literal IP, a CIDR, a [a,b,c] list, !-negation, and Suricata's $HOME_NET/$EXTERNAL_NET-style variables - treated as a wildcard match, since there's no vars.yaml of your own to resolve them against), source/dest port (same shapes, plus port ranges like 1024:), direction -> (<> is accepted and treated the same way - this proxy has no separate server-side vantage point).

KeywordBufferEvaluated
content (+ nocase, depth, offset, distance, within, endswith)Request/response bodyBody-peek time
pcreWhatever buffer it's chained to (default: body)Body-peek time
http.uri / http_uriRequest URIHeader time
http.method / http_methodHTTP methodHeader time
http.header / http_headerAll request headers, concatenatedHeader time
http.user_agent / http_user_agentUser-Agent header specificallyHeader time
http.host / http_hostHost header specifically (distinct from the full http.header dump)Header time
tls.sni / tls_sniTLS SNICONNECT time
dns.query / dns_queryDNS query nameParsed but inert - no DNS query text is wired into the body-check hook by default
file_dataSame as the body bufferBody-peek time

Both the modern dotted sticky-buffer form (http.uri; content:"...";) and the legacy underscore modifier form (content:"..."; http_uri;) are accepted. flow: direction qualifiers (to_server/to_client/from_client/from_server) scope a rule to the request or response side; established/not_established/stateless are accepted as no-ops (traffic reaching this proxy is definitionally established). msg, sid, rev, classtype, priority, reference, and metadata are parsed and shown in the admin UI, not evaluated.

Flowbits & flowint (multi-stage correlation): flowbits:set/isset/unset/toggle (plus the ,noalert modifier) and flowint:name,+,N/isset/>/</=/... are supported, scoped to one flow - here, one CONNECT tunnel's destination (i.e. one host a device is talking to), the same unit tls.sni and per-request body rules already operate within. This lets a low-severity rule mark a flow as suspicious without generating its own alert (flowbits:set,suspicious; flowbits:noalert;), and a second rule later in the same flow escalate only if that mark is already present (flowbits:isset,suspicious;) - real multi-stage detection (e.g. this connection first showed reconnaissance-looking traffic, and is now making a request matching a known exfil pattern) without either signal alone being reliable enough to act on by itself. State resets when the flow itself is torn down - it doesn't persist across separate connections, even to the same destination.

Explicitly out of scope

A rule using any of these keywords is still parsed and retained (so the rest of an imported ruleset doesn't fail over one line) but is excluded from evaluation and marked not evaluated in the admin UI, with a reason.

Keyword(s)Why
flowvar, xbitsArbitrary captured-value storage (flowvar) and cross-flow/cross-host state (xbits) - unlike flowbits/flowint above, these aren't scoped to a single flow, which is the only unit of state this engine tracks.
threshold, detection_filterRate-based suppression - no per-sid counters exist.
byte_test, byte_jump, byte_extractBinary offset arithmetic, meaningful mostly with raw packet access this proxy doesn't need.
base64_decode, base64_dataNot implemented.
luajit, luaNo embedded scripting.
iprepUse this proxy's own firewall/threat-feed/MISP matching instead - it duplicates that mechanism.

PCRE: pcre: patterns are compiled with Go's RE2 engine first; a pattern needing backreferences or lookaround (which RE2 can't represent) falls back to a pure-Go backtracking engine instead, so those constructs work too - only a pattern that fails to compile under both engines is marked unsupported rather than failing at match time. The fallback engine has no linear-time guarantee, so each match attempt against it is bounded by a short timeout to bound worst-case cost. In practice this affects very few real rules: the curated URLhaus feed uses pcre: in exactly zero of its rules (it's entirely content-based).

Authoring rules

Add rules one at a time on the Firewall admin page's IDS/IPS card - paste a single raw rule line into the input and it's parsed and added immediately; each listed rule has its own edit (in place, same raw-line input) and delete controls, the same pattern the firewall rules table above already uses. Bulk replace-all import (paste or upload a full .rules file, or set custom_rules_text in suricata.yaml) is API-only, not exposed in the UI - see /api-docs → SWG - IDS/IPS for the full request/response reference, including GET/PUT /api/suricata/rules (bulk, PUT replaces the entire custom rule set in one call and reports a per-line parse error without losing the rest of the file), the single-rule upsert-by-sid POST /api/suricata/rules/single the admin UI itself calls, per-sid PATCH/DELETE at /api/suricata/rules/{sid}, and the PUT /api/suricata/enabled toggle. Neither curated feed is affected by any of these - they're a separate source entirely (see above).

# Block a specific malicious download URL by host+path prefix:
alert http any any -> any any (msg:"malware download path"; http.host; content:"cdn.example-evil.com"; http.uri; content:"/payload"; depth:8; sid:1000001;)

# Block on SNI alone (no body access needed - matches at CONNECT time):
drop tls any any -> any any (msg:"known C2 domain"; tls.sni; content:"c2.example-evil.com"; sid:1000002;)

Data Loss Prevention

DLP inspects outbound HTTP/HTTPS request bodies for sensitive content entirely in-process on the proxy node - body data is never sent to Allod Solutions.

ModeWhat is inspected
offDisabled - connections are raw-piped with zero overhead.
headersPeeks at the first request's headers per TLS connection only. Lowest overhead; misses keep-alive follow-up requests.
selectiveFull inspection for domains in selective_categories, headers-only elsewhere.
fullParses every HTTP request/response pair on the connection - full body, clipboard, and file-path metadata. Catches all uploads and downloads; adds HTTP parsing overhead per request.

Post-detection escalation: A DLP hit can temporarily escalate the device to full inspection for a configurable window (e.g. post_detection_window: "15m"), providing increased visibility after a potential leak.

S3 body samples: When dlp_s3 is configured, rule-matching request bodies are stored encrypted in your S3-compatible bucket. You control retention and access - Allod Solutions has none.

Type verification: file-type rules check magic bytes, not just the extension or declared content-type - a renamed invoice.exe saved as invoice.pdf is still detected as an executable. Archive uploads (.zip, .7z) are inspected recursively, so keyword and regex rules also match against each entry's decoded content, not just the archive's own filename.

Data-type classification: a matching rule's Tag (or a YARA rule's target_tag) aggregates onto the event as a business-taxonomy category - PII, FIN, IP, CUST-DATA, CRM, or any custom value - independent of which rule wins the allow/block decision. This is separate from Type verification above: it classifies what kind of data moved, not what file format carried it. When Allod VRM integration is configured, tagged events are pushed to it and checked against each destination vendor's approved data scope in real time - see VRM → Data classification & risk.

Chat & email: the same rule set covers file uploads inside Slack, Discord, and Microsoft Teams - fetching surrounding message context with the session's own bearer token - and SMTP attachments extracted from intercepted mail, not only browser uploads. Links inside the same intercepted mail are separately checked for phishing, and the sender identity for impersonation of one of your own people or domains - see Firewall → Email phishing-link detection and Firewall → Email authenticity checks.

Personal-account leak prevention: a common blind spot when a work and personal email account are both configured in the same mail client (Outlook, Apple Mail, Thunderbird) is dragging a file - or forwarding a document - from the work inbox into the personal one. A DLP rule can require the outbound MAIL FROM address to belong to one of your own SCIM domains before an attachment is allowed through. Unlike a plain external-recipient check, this catches the leak even when the recipient is an internal colleague, since it looks at who the mail is from, not just where it's going. A blocked message gets a rejection naming the attachment and the account it was sent from, so the sender understands exactly what to fix instead of seeing a generic error.

Cross-Protocol Attachment Lineage: a file's SHA-256 is tagged the moment it's seen as an inbound email attachment (SMTP, IMAP, or POP3), and that tag follows the file forward - a DLP rule can block or alert on an outbound send of that same file, or restrict it to specific sender domains, entirely independent of what the outbound content-inspection rules see. This closes the gap where a file arrives externally, gets saved to disk, and leaves again through a channel that never re-triggers a content match.

Provenance: uploads are correlated with clipboard history, so a DLP event can show which application and window the pasted content came from. A file's SHA-256 is also tracked across events, so a blocked upload that matches a file downloaded days earlier is linked back to that download automatically.

Container origin: a flagged event shows which container actually made the connection - name, image, and labels - alongside the usual process and user. Without this, every container on a shared host looks identical to the proxy: just the runtime's own shim process on the host's one IP. With it, a DLP alert or firewall block traced back to a specific service (payment-gateway, image myrstack/payment-gateway:1.4.0) rather than a generic host process, which matters most on a dev or CI box running many short-lived containers side by side. Requires no setup beyond labeling your containers as you normally would (docker run --label, or a Compose file's labels:); Docker and Podman are supported today, k3s (pod name/namespace) is planned. Fully supported on Linux; Windows and macOS via Docker Desktop are experimental - see the platform-support note under Firewall → Container-aware egress, which applies equally to attribution here. See also that same section for using the identity in a blocking policy, not just after-the-fact attribution.

VDI/RDP boundary enforcement: a published RDP/Citrix/VDI desktop is meant to be a trust boundary, but the client software's own clipboard and drive redirection quietly cross it - syncing remote clipboard content and dragged-out files to local disk without ever generating a network request the proxy could inspect. Both are covered: a copy out of a remote session is checked before it can be pasted anywhere locally, using only a hash of the text, never the content itself, and a block clears the local clipboard. A file dragged out of a session is inspected exactly like an upload the moment it lands, and a block deletes it. Ordinary RDP/Citrix use isn't flagged by default - this is policy scoped to whichever clients and users you choose, the same rule engine as everywhere else.

Local transform tracking: zipping, encrypting, or otherwise transforming a sensitive file on disk before uploading it doesn't bypass DLP either - the agent tracks a file's sensitivity tag through common transform tools (archivers, GPG, cp, rsync), so an upload of the derived file still carries the original DLP verdict, even if it's zipped or encrypted first.

Live lineage recheck: long-lived connections - a sync client like OneDrive or Dropbox can hold one connection open for hours across many transfers - can outlive the point-in-time check made when the connection opened. For these, the proxy can also ask the agent live whether a previously flagged file is currently open in the uploading process. This is also the only DLP signal available for a certificate-pinned application (see TLS & CA), where content inspection is bypassed entirely to keep the app working - it can't see that traffic either, but can still confirm a flagged file was open in that process and raise an alert. This only catches files already flagged by an earlier check, not entirely new sensitive content no other check has seen.

Removable media: writes to USB and other removable drives are evaluated against the same DLP rule engine as network uploads - scope a rule to removable_write under Direction to write policy specific to physical media. A block action deletes the file from the drive; it's still uploaded to the controller as evidence first, from the original local file if one exists, or from the removable copy itself just before it's removed.

Print spooler DLP: printing never touches the network stack, so it's invisible to every channel above. The agent holds each new print job the moment it appears - before it's read at all - so a block verdict here is real prevention: the job is cancelled before it reaches paper, not just logged after the fact. The page content is evaluated against the same DLP rules as uploads, so no separate print-specific policy authoring is needed. Anything not blocked is released with, at most, a brief pause.

Print job provenance: printed jobs are also linked back to file provenance - if a print job's title matches a file already flagged sensitive elsewhere (for example, downloaded from a tracked SharePoint site), that verdict carries over. It can only make the outcome stricter, never weaker, than what the printed content matches on its own.

Beyond HTTP - SSH and Git: scp, sftp, rsync, ftp, and plain git over SSH don't speak HTTP, so the proxy has nothing to inspect on those channels. The agent instead watches these processes' file reads directly and evaluates the same rules that govern browser uploads against what's being read - one rule set covers both paths, with a matching event recorded either way. This path is alert-only: the read has typically already completed by the time it's observed, and there's no HTTP response to hold back. git push is the exception - the agent checks the destination remote against your CASB tenant restrictions and scans the files about to be pushed against DLP rules before the push proceeds, killing the git process outright when either check comes back block-severity.

Supply-chain install-time credential theft: a compromised package's postinstall script - or any process spawned during an npm/npx/yarn/pnpm/pip/poetry/cargo/gem/bundle/composer/go install, for as long as that install is actually running - is watched for two things: opening a well-known secret-bearing file (.env, SSH private keys, cloud credential files, shell history, .npmrc, and more), or reading several distinct files entirely outside the project being installed - a signal aimed at wholesale source-tree theft rather than any one named secret. A process whose own name is a known secret-scanning tool (TruffleHog, Gitleaks, detect-secrets, ggshield, secretlint) is flagged immediately, independent of what it goes on to read. All three signals block by default: the reading process and the installer process that spawned it are both killed, aborting the install outright rather than only the one read - a legitimate build step that genuinely needs one of these paths (registry auth from .npmrc, for example) is expected to be the rare exception, exempted with a process-scoped firewall allow rule naming the installer (see Firewall) rather than a permissive default. On Linux this runs entirely on the agent's existing fanotify/netlink event stream, no added latency; on macOS and Windows, which only poll for new file activity every 200-500ms, a connection opened by a process still inside an active installer session is briefly held (700ms) before being allowed to dial out, giving that poll time to catch and kill a scanner that read a secret moments before trying to exfiltrate it. Only file paths and process names are ever collected - never file content or environment-variable values.

Origin-aware rules: rules can also match on where a file originally came from (origin_domain, origin_local), not just where it's currently headed. This check runs after the real-time allow/block decision, so it never blocks retroactively - a match can only raise a logged event to an alert, never the reverse.

SharePoint & OneDrive site awareness: downloads from SharePoint or OneDrive are attributed to their source site automatically, whether the file came through a browser download or was synced silently in the background by the OneDrive client - both feed the same rule field, so one rule covers either case.

Google Shared Drive awareness: the same idea, for Google Drive - a file synced in the background by Google Drive for Desktop is attributed back to the Shared Drive it came from, so a DLP rule can give a specific Shared Drive a standing sensitivity tag the same way it can for a SharePoint site.

SMB/CIFS network-share provenance: a file copied from a mapped network drive never touches the proxy, so it's invisible to the mechanisms above - but the agent still records which network share it came from, on Linux, macOS, and Windows alike. A file copied off a finance file share still carries that origin if it's later uploaded to personal cloud storage, even when the upload itself looks completely ordinary.

Sensitive-context protection: screenshot content and screen-sharing are inspected only while a device is accessing a destination explicitly marked sensitive - never as general surveillance. A destination becomes sensitive via any of three sources: a firewall rule flagged sensitive: true, an active step-up SSO/JIT grant, or - when Allod VRM integration is configured - an app's RoPA data classifies Confidential or Restricted. While that context is active, screenshots are OCR'd locally, and only the extracted text - never the image itself - is scanned through the normal DLP pipeline. Screen-sharing is detected without any OS-level screen-capture hooks.

Screenshot context: alongside the OCR'd text, a screenshot event carries the foreground window's process and title (or the active browser tab's URL) at the moment of capture, plus the domain the device was browsing. This is forensic-timeline context, not an additional inspection surface: it's still gated on the same sensitive-context requirement as the rest of this section, and best-effort (empty on a headless session).

Email security

ALLOD SWG intercepts SMTP (outbound), IMAP, and POP3 (inbound) mail with the same TLS MITM it uses for HTTP/HTTPS, so email runs through the same policy engine as everything else - not a bolt-on integration or a separate product. The individual checks live under Firewall and DLP since they reuse those engines directly; this section is a map of everything email-related in one place.

CheckWhat it does
Phishing-link detectionTyposquat/lookalike and cloaked-link checks on every link in a message body. Blocks on POP3 retrieval, alerts on outbound SMTP, flags on IMAP.
Email authenticity checksBusiness-email-compromise signals against your IdP directory, self-learned brand-impersonation detection, SPF/DKIM/DMARC alignment, sending-domain age, threat-intel matches, invisible-Unicode content evasion, and suspicious attachments/links/content. Alert-only.
IMAP flaggingA hit on any of the above (or a DLP malware-hash match) sets the standard $Phishing IMAP keyword in place, instead of a desktop notification.
Attachment DLPSMTP attachments run through the same DLP rule engine as browser uploads - keyword/regex, file-type verification, malware-hash matching, recursive archive inspection.
Personal-account leak preventionA DLP rule can require the outbound MAIL FROM address to belong to one of your own SCIM domains before an attachment is allowed through - catches a work file crossing into a personal account configured in the same mail client.
Cross-Protocol Attachment LineageA file tagged as an inbound email attachment carries that tag forward - a DLP rule can block or restrict its later outbound send, independent of what content inspection alone would catch.
Supply-chain install-time credential theftAny process spawned during an npm/yarn/pip/cargo/gem/composer/go install is watched for reading secrets or mass-enumerating files outside the project. Blocks by default - kills both the reader and the installer.
User-reported spam/junkObserves a user marking a message spam/junk in Gmail webmail or via IMAP, and logs it as a DLP event with an independently-toggleable admin alert.

User-reported spam/junk: when a user marks a message as spam or junk from their own mail client, SWG observes and logs it - Gmail webmail's sync-protocol thread mutation and REST modify calls, and IMAP MOVE/COPY to a junk folder or a junk-flag STORE. This is detection only: the provider has already performed the move, so nothing is blocked or altered - the signal is logged as a DLP event and can independently trigger an admin alert, giving security visibility into what users are self-triaging as spam without waiting on a desktop notification or manual report.

CASB

Every connection is classified against the built-in app catalog. Unseen apps are automatically reported to Allod VRM for triage when SWG integration is configured.

  • Tenant restrictions: Allow corporate tenants of a SaaS app while blocking personal accounts using domain-level rules.
  • Upload controls: Combine DLP category rules to block uploads to unsanctioned cloud storage while allowing approved services.
  • Shadow IT visibility: The admin dashboard shows all observed apps across the fleet, ranked by usage.
  • Data up / data down: every tab (per process, Corporate IT, Shadow IT, Extensions) reports the bytes that actually crossed each device's connection to the proxy over the last 30 days - up is device to internet, down is internet to device, TLS overhead included. Every connection is measured, inspected or not: TLS passthrough, cert-pinned apps, QUIC/HTTP3 and plain HTTP count the same way as TLS-inspected traffic. App totals cover browsers and desktop clients alike, so an app used only through its desktop client still shows as in use under Corporate IT. The per-process tab lists non-browser processes; browser traffic is attributed to the apps and extensions it reaches. Extensions are credited only with the requests they made themselves. The figures sent to Allod VRM are the same totals the dashboard shows. Totals are updated every five minutes.
  • ZTNA resource visibility: private resources reached over a ZTNA connector tunnel appear in the same dashboard's Corporate IT tab, not Shadow IT - they're admin-sanctioned by virtue of the connector registration itself - with a "Local" badge and a fixed "Internal" category, whether or not Allod VRM integration is configured. One unified view of application usage, public SaaS and private internal tools together, rather than a second dashboard for internal resources. In a multi-proxy deployment, a resource reached via cross-proxy connector fallback is still counted but without its resource name attributed.
  • Shared account detection: when three or more distinct people are observed using the same non-SSO login, the account is flagged as shared - a common indicator of an unmanaged team password. This isn't limited to catalogued apps: login-form submissions are inspected on any site, so the same detection covers shadow IT with no catalog entry at all. Sightings that match a device's own IdP-verified identity are excluded, so a person's own legitimate SSO-linked account never counts against it.
  • Internal-tool discovery: shadow-IT visibility above only covers apps in the public SaaS catalog. Hosts under your admin-configured internal DNS zone with no catalog match - a self-hosted git server, an internal wiki - are separately surfaced for triage as Allod VRM asset candidates. Matching is DNS-zone-based rather than IP/CIDR-based: private ranges like 10.0.0.0/8 are exactly what home routers commonly default to, and a CIDR match would risk sweeping employees' home devices into the queue.
  • Extensions tab: every browser extension seen making its own network request - see Firewall → Browser-extension request detection - is aggregated fleet-wide by extension ID: connection count, device count, distinct destination domains contacted, and data transferred over the last 30 days. Visibility here doesn't depend on having written any blocking rule first.

Device compliance

Compliance checks are not a fixed checklist - you supply the script. Upload an arbitrary shell script per operating system under Admin → Compliance. The controller signs each script with an Ed25519 key before distributing it to agents; the signed payload is scoped to the target OS, so a script written for Linux cannot be replayed against a Windows or macOS agent.

Agents run the signed script locally and report pass/fail back to the controller. Setting require_compliance: true gates network access at the point a device connects - a non-compliant device is blocked before it can reach the proxy at all. Compliance state is also available as a scoping condition on firewall and DLP rules, so a rule can apply only to devices currently failing their check.

Why scripts, not presets A fixed checklist (disk encryption on/off, OS version ≥ X) can't express organisation-specific requirements. A script can check anything the OS exposes - a registry key, a running process, a config file's contents - without waiting on a product release to add the check you need.

ZTNA & Connectors

Private resource access does not require a VPN. Add CIDRs to access_routes in firewall.yaml - the controller pushes these to enrolled agents, which route matching traffic through the proxy and then through a connector tunnel.

Connectors run inside your private network and dial out to the controller. No inbound firewall rules are needed on the private network side. Deploy multiple connectors per network for redundancy.

Reverse tunnels: the same connector protocol also runs in the other direction - an internal server can reach a port on an enrolled agent's machine (e.g. attaching a debugger to a developer's laptop) without either side opening an inbound rule. The agent must explicitly expose the port first via tunnel_ports in agent.yaml; a dial to any other port is refused. In a single-node deployment, where the proxy and connector Hub run in the same process, this reaches the agent's virtual IP directly - no separate connector daemon needs to be deployed just to bridge the Hub back to itself.

Split-tunnel by default Only traffic matching the CIDRs configured in access_routes takes the extra connector hop to reach a private network. General internet traffic still goes through the proxy for inspection like everything else - it just connects straight to its destination from there, with no connector involved.

Step-up SSO & JIT access

A firewall rule - domain or ZTNA access rule alike - can require a fresh interactive login or a time-boxed grant before traffic is allowed. This is enforced at the same proxy CONNECT chokepoint shared by SaaS domains and ZTNA resources: when no active grant exists, the proxy serves a challenge page instead of forwarding the connection.

authBehaviour
ssoRequires a fresh interactive IdP login (step-up re-authentication) before the connection proceeds.
jitRequires a time-boxed access grant. jit_approval: self auto-grants on request; jit_approval: approval holds the request for an approver.

Grant lifetime defaults to 8 hours and is configurable per rule via auth_ttl (a Go duration, e.g. "4h"). Approval-required requests notify the approvers listed in jit_approvers through the alerting webhook dispatcher's jit_access_requested event and, when configured, through interactive Slack and Microsoft Teams approval buttons or a PagerDuty page - see Integrations. Admins can also approve or deny from Admin → Access Requests.

Self-service pre-provisioning: a public self-service page lets a user request a JIT grant before opening a non-HTTP client - a database tool, an SSH client - that can't render the proxy's browser-based challenge page itself. Reach it at the short, memorable https://access address - not a real DNS name, a proxy-side construct every enrolled device already resolves and trusts, since it already routes through and trusts this proxy. https://login works the same way, pointing at the login form - most useful for LDAP-only customers (see Identity → LDAP as a login backend) who have no cloud IdP bookmark to start from otherwise.

Best-effort approval integrations Slack, Teams, and PagerDuty notifications are each independently optional. A down chat integration never blocks issuing or denying a grant - the admin UI path always works.

Identity (SCIM / LDAP / OIDC)

SWG syncs users and groups from your identity directory to enable user-aware policy, identity-verified device tokens, and per-user event attribution. Two sync modes are available.

Push (IdP → SWG)

Your IdP pushes changes to SWG's SCIM 2.0 endpoint as they happen. Enable under Admin → Identity → SCIM push and paste the endpoint URL into your IdP's SCIM connector. Supported: Okta, Microsoft Entra ID, Authentik, and any RFC-7644-compliant IdP.

Pull (SWG polls IdP)

Use pull mode when the IdP cannot reach your SWG instance (e.g. behind NAT or a firewall). Configure under Admin → Identity → Pull from IdP.

ProviderRequired credentials
scim - Generic SCIM 2.0SCIM v2 base URL, Bearer token
authentik - Authentik REST APIAuthentik base URL, API token
entra - Microsoft Entra IDTenant ID, Client ID, Client secret
Required Graph permissions: User.Read.All, Group.Read.All, GroupMember.Read.All
ldap - LDAP / Active DirectorySee below

LDAP / Active Directory

Required fields: LDAP URL (ldap://host:389 or ldaps://host:636), Bind DN, Bind password, User base DN, Group base DN.

Attribute defaults (override under Advanced / Attribute mapping):

FieldDefaultActive Directory override
User filter(objectClass=inetOrgPerson)(objectClass=person)
Group filter(objectClass=groupOfNames)(objectClass=group)
Username attruidsAMAccountName
Email attrmailmail
Display name attrcncn
Member attrmember (DN-style)member
posixGroup schemas Set Member attr to memberUID. SWG resolves both DN-style (Active Directory, groupOfNames) and UID-style (posixGroup) membership references automatically.
Active Directory disabled accounts The userAccountControl ACCOUNTDISABLE bit is read during sync. Disabled AD accounts are imported as inactive and cannot be linked to devices or used in policy.

LDAP as a login backend

The same LDAP connection configured above can also serve as a direct login backend, not just directory sync - useful for customers with no cloud OIDC/SAML identity provider to point at. Enable Backend: ldap under Admin → Settings → Login to switch admin/approver console login, step-up SSO/JIT grant issuance, device-identity binding, and allodctl CLI login over to a real LDAP bind-as-user against the directory. Everything downstream of login - session issuance, grant and JIT-request creation, device verification - is unchanged; only how a credential is checked is swapped in. Password-based login at /admin/login remains available regardless of backend.

OIDC (admin SSO)

Enable SSO for admin UI login under Admin → Settings → OIDC. The configuration is stored in data/oidc.yaml and takes effect immediately without a restart. Password-based login remains available as a fallback at /admin/login.

# data/oidc.yaml
enabled: true
issuer_url: "https://idp.example.com/application/o/allodswg"
client_id: "allodswg"
client_secret: "your-client-secret"
redirect_url: "https://swg.example.com/auth/callback"
admin_group: "swg-admins"   # optional - restrict login to members of this group

Event log & retention

Events are written only when a rule triggers. Regular allowed traffic is not stored. Each event is encrypted with an HKDF-derived key tied to the originating device - a breach of one device's events does not expose others.

  • Article 15 export: Admin → Users → Export data - produces a JSON file for subject access requests.
  • Article 17 erasure: Admin → Users → Erase - deletes all events for a user without vendor involvement.
  • Retention window: Configured in Admin → Settings. Events older than the window are deleted automatically.

Integrations

All integrations are configured under Admin → Settings → Integrations and take effect immediately without a restart. Credentials are stored in server.yaml under the integrations key.

CrowdStrike LogScale

Streams proxy events to a LogScale (formerly Humio) repository in real time. Each event includes process name, destination, user, verdict, and DLP action.

integrations:
  logscale:
    enabled: true
    url: "https://cloud.humio.com"
    token: "your-ingest-token"

Zeek

Publishes HTTP events to a Zeek Broker WebSocket endpoint as Allod::http_request records. Useful for feeding SWG visibility into an existing Zeek/SIEM pipeline.

integrations:
  zeek:
    enabled: true
    broker_addr: "zeek.internal:9997"
    broker_tls: false
    broker_topic: "allod/http"

CrowdStrike Falcon EDR

Pushes blocked indicators (domains and IPs) to Falcon as custom IOCs with action=prevent. Triggered by any heuristic block: JA4DB, SSLBL, DGA, typosquatting, or threat feed.

integrations:
  crowdstrike_falcon:
    enabled: true
    base_url: "https://api.crowdstrike.com"
    client_id: "abc123..."
    client_secret: "your-secret"

MISP

Polls a MISP instance for threat indicators and blocks matching traffic at the proxy. Supported attribute types: domain, hostname, ip-dst, ip-src, url, ja3-fingerprint-md5 (client JA3, or server JA3S when submitted under a MISP ja3s object), jarm-fingerprint, ja4-fingerprint, user-agent, uri, filename, filename|sha256, sha256, port.

integrations:
  misp:
    url: "https://misp.internal"
    key: "your-api-key"
    interval: "30m"
    to_ids_only: true
Indicator typeBlock behaviour
domain / hostname / urlBlocked at category resolution, before firewall rules
ip-dst / ip-srcBlocked at IP resolution, same priority as URLhaus / Feodo
ja3-fingerprint-md5Blocked at CONNECT, alongside SSLBL
ja4-fingerprintBlocked at CONNECT, alongside JA4DB (no license required)
jarm-fingerprintBlocked on cached JARM result - first connection to a new IP is not checked
portBlocked at CONNECT time before any HTTP is processed
user-agentBlocked per HTTP request (H1 and H2); returns 403
uriBlocked per HTTP request path+query (H1 and H2); returns 403
filename / filename|sha256 / filename|md5 / filename|sha1 / filename|sha512Blocked on upload (after body peek) and download (before body is streamed); returns 403
sha256Blocked on upload after body peek (first 32 KB); not checked on downloads

Indicators are cached to disk (data_dir/mispfeed_cache.json) and loaded at startup so blocking is active before the first poll completes. Attributes with tags containing "phishing" are classified as phishing; all others as malware. Set to_ids_only: true (the default) to import only attributes flagged as IDS indicators.

TLS verification MISP connections use the system certificate store. For self-signed certificates, add your CA to the system trust store rather than disabling verification.

Reporting back: the integration isn't read-only. From any event's detail view in the admin UI, an admin can push that event to the configured MISP instance as a new event - so an indicator your own fleet discovered enriches the shared instance instead of staying siloed in SWG's log.

Allod VRM

Connects SWG to an Allod VRM instance for CASB app classification. Authentication uses the shared license key - no separate API key is required.

integrations:
  allod_vrm:
    url: "https://vrm.example.com"

Alerting

Security events - DLP hits, firewall blocks, threat-feed matches, compliance failures, JIT access requests, and more - can be pushed to one or more outgoing webhooks in real time, independently of the event log sinks below. Configure webhooks under Admin → Settings → Integrations → Alerting; each has its own URL, format, enabled flag, and event-type filter.

FormatPayload
genericRaw JSON event object - the default, for custom receivers.
slackSlack incoming-webhook Block Kit message.
teamsMicrosoft Teams Adaptive Card via a Workflows-based incoming webhook. Read-only - no action buttons, unlike the interactive JIT approval cards. Teams' legacy Office 365 Connector (MessageCard) webhooks were retired by Microsoft in May 2026, so a Workflows webhook is required.

Each webhook enables only the event types it cares about (dlp_alert, firewall_block, jit_access_requested, compliance_fail, and more) rather than receiving the full firehose. When include_admin_links is enabled, events carry a deep link straight to the relevant record in the admin UI.

JIT approval notifications

Separate from the alerting webhooks above: when a JIT access request needs an approver, SWG can notify them through Slack, Microsoft Teams, or PagerDuty. Unlike the read-only Teams alerting card, the Slack and Teams JIT notifications carry interactive approve/deny buttons - an approver can act directly from the notification, not just from Admin → Access Requests. Each channel is independently optional; configure under Admin → Settings → Integrations → JIT Approval or directly in server.yaml:

integrations:
  slack_approval:
    enabled: true
    bot_token: "xoxb-..."
    signing_secret: "..."      # verifies interactive button callbacks came from Slack
    channel: "#access-requests"
  teams_approval:
    enabled: true
    webhook_url: "https://..."
    shared_secret: "..."       # verifies interactive button callbacks came from Teams
  pagerduty:
    enabled: true
    routing_key: "..."         # Events API v2 integration key

A down or misconfigured chat integration never blocks issuing or denying a grant - the admin UI path always works regardless.

IdP network-zone sync

When a proxy node registers or its egress IP changes, SWG can push the current set of proxy egress IPs directly into your identity provider's network-zone allowlist, so "only allow sign-in through the corporate proxy" IdP policies stay correct automatically instead of requiring a manual allowlist update on every proxy change.

integrations:
  idp_sync:
    okta:
      domain: "company.okta.com"
      api_token: "..."          # SSWS token
      zone_id: "..."            # Okta Network Zone to update
    entra:
      tenant_id: "..."
      client_id: "..."
      client_secret: "..."
      named_location_id: "..."  # Entra Named Location to update

Okta and Entra are configured independently - set either, both, or neither.

Public embed map

The device/threat map on the admin dashboard can be embedded in an external dashboard (Grafana panel, wallboard browser) without an admin login, via a separate, token-authenticated public surface. Data is always aggregated to country granularity - no device names, usernames, or exact coordinates ever leave the server.

Issue a token from Admin → Keys → the "Embed Tokens" card → Create New. The raw token is shown once, at creation - only its SHA-256 hash is stored server-side, so a lost token can't be recovered, only revoked and replaced. Embed the resulting URL directly:

https://<controller-host>/embed/map?token=<raw-token>

The page is self-contained (no admin chrome) and polls /api/public/map every 30 seconds on its own; an invalid, expired, or revoked token shows a small error banner instead of failing the page. To build a custom panel (e.g. Grafana's JSON API data source) instead of iframing the whole page, call the JSON endpoint directly:

GET /api/public/map?token=<raw-token>

{
  "devices": [{"country": "SE", "lat": 60.1, "lon": 18.6, "online": 42, "total": 50}],
  "blocked": [{"country": "RU", "lat": 61.5, "lon": 105.3, "blocked_count": 7}]
}

blocked covers the same rolling 24-hour window as the dashboard's red block overlay.

Security Both routes are rate limited per IP (30 requests/minute) and carve out a deliberately relaxed CSP/CORS (no frame-ancestors restriction, Access-Control-Allow-Origin: *) scoped to just these two paths - the rest of the admin UI keeps its strict CSP and cannot be framed. Revoke a token from Admin → Keys at any time to disable one embed without affecting others or any admin session.

Event log sinks

Security events (firewall blocks, DLP hits, threat detections) can be exported to external systems via three independent sinks configured under logging.event_log:

SinkProtocolUse case
FileJSON lines, rotatedLocal archiving, log shippers
BeatsElastic Beats TCPLogstash / Elasticsearch
SyslogLocal /dev/logSystem journal, rsyslog, Splunk UF

TLS & CA

ALLOD SWG generates a self-signed CA on first start. This CA signs short-lived leaf certificates (default 7 days) for each inspected domain. The CA certificate must be trusted on all endpoints.

Where the CA itself is stored depends on event_store.driver: with the default sqlite driver it's a local file at ca.path (single-node deployments). With postgres (Controller HA), the CA is instead stored in Postgres and shared across every controller replica, so a newly-promoted replica always serves the exact same CA every enrolled device already trusts - it is never regenerated on a fresh replica's first boot. A second, separate internal CA (not customer-configured) issues short-lived certs for controller/proxy/connector internal traffic and is stored the same way under Controller HA.

AlgorithmNotes
ecdsa_p256Default. Fast signing, small certs, broadly supported.
rsa_2048Wider compatibility with older TLS stacks.
rsa_4096Higher security margin at the cost of signing speed.

CA rotation

Before expiry, automatically. The controller checks the CA once a day and times rotation as a share of the CA's lifetime, so it works for any ca_validity_days:

Remaining lifetimeWhat happens
Last 1/6A daily expiry warning in the log.
Last 1/8The next CA is generated and pushed to every connected agent as an additional trusted root. Agents that are offline get it when they reconnect.
Last 1/12The controller switches to the new CA and tells the agents, which already trust it.

On demand. Regenerating the CA from the admin UI or POST /api/ca/regenerate takes effect everywhere at once: the controller serves the new CA immediately, connected agents are told to install it, and standalone proxy nodes pick it up on their next config poll (config_poll_interval). A proxy that loads its CA from a ca_path file keeps using that file until it is replaced and the proxy restarted. A proxy keeps serving its existing leaf certificates for 30 seconds so agents have time to install the new CA, then issues new ones.

Controller HA. With postgres, both the CA and the pending next CA are stored in Postgres, and only the leader rotates. A replica that becomes leader serves the current CA and continues a rotation that was already under way, with the same pending CA the agents were told to trust.

Devices without the agent Setting regenerate: true, or regenerating from the admin UI, invalidates all existing leaf certificates. Agents install the new CA themselves; devices that trust the CA through MDM or group policy need it pushed there before TLS inspection works for them again.

Certificate-pinning bypass: an application that pins its own certificate rejects ALLOD SWG's leaf certificate outright. ALLOD SWG detects that rejection and automatically stops attempting inspection for that host for 24 hours, so the app keeps working instead of breaking - a narrow, per-host exception rather than a blanket policy change. DLP's live lineage recheck still runs against those connections as a compensating, alert-only signal.

API reference

Every admin action is available via REST. See the interactive reference at /api-docs → SWG.

Authenticate with an API key in the Authorization header:

Authorization: Bearer sk_myrs_...
RoleAccess
RoleSuperAdminFull read/write. The only admin-facing role today - there is no separate lower-privilege admin tier.
RoleProxyInternal proxy-to-controller endpoints (/internal/*) - for proxy nodes only.
RoleEnrollmentEnrollment, heartbeat, config fetch, and the agent/connector WebSocket and tunnel endpoints - for endpoint agents and connectors.

Allod VRM does not use a separate API-key role - it authenticates with the shared license key (see Integrations → Allod VRM).


Product documentation

ALLOD | VRM

Automated vendor risk assessment and shadow IT governance - from discovery to compliant Art. 30 register, without leaving your infrastructure.

Overview

Allod VRM (Vendor Risk Management) closes two gaps most organisations have: they don't know which SaaS tools their people are using, and the ones they do know about haven't been properly assessed.

When SWG observes a new application on the fleet, VRM automatically queues it for triage. Automated probing, a GLEIF entity lookup, and a local LLM do the groundwork - so your team reviews conclusions, not raw documents. All processing stays inside your infrastructure.

Each system carries its own record of the humans and paperwork behind it: technical, billing, legal, and security contacts (encrypted at rest with a pre-shared key), alongside attached documents such as the DPIA, an exit plan, or a risk analysis. It's the same register used for reviews and the Art. 30 export, not a separate spreadsheet kept in sync by hand.

Architecture

ComponentRole
VRM serverAdmin UI, REST API, and background worker coordination. Stores all vendor data in a local SQLite or PostgreSQL database.
Probe checkerRuns on a configurable schedule. Runs ~20 automated checks per vendor - TLS, hosting geography, email/DNS security posture, privacy/legal documents, and more - see Vendor assessment.
Risk engineWakes on risk.interval and auto-opens, refreshes, or closes Risk Register entries from CSF SEAL sovereignty scores, RoPA/DPA gaps, missing SBOMs, and approaching contract/license dates - never overwriting a risk a human has already acted on. See Risk Register.
LLM enricherCalls a local Ollama instance to extract retention periods, subprocessor lists, and breach notification commitments from fetched privacy policy / DPA text. No data leaves your infrastructure.
GLEIF clientMatches vendors to the Global LEI Index via the Allod license server. Tracks ultimate parent and jurisdiction changes.
Sanctions monitorFetches the EU consolidated sanctions list daily and checks every vendor and its ultimate parent. Raises a notification on any match.
SchedulerGenerates review tasks from admin-configured review cycle templates (trigger: interval, contract renewal, license renewal, or manual - see GDPR workflows). Moves systems to pending offboarding when no traffic is seen for zero_traffic_days.
SWG syncPolls the SWG controller for shadow IT observations and imports new apps into the VRM triage queue.
Docstorage / e-signingOptional S3-compatible object store for contract documents and signed receipts, backing the click-to-sign vendor contract flow. Off unless docstorage.enabled is set. See Vendor e-signing.

Quick start

# vrm.yaml
server:
  listen: ":9090"
  admin_password: "changeme"

store:
  dsn: "./data/vrm.db"    # SQLite; use a postgres:// DSN for PostgreSQL

license:
  key: "lic_..."          # Enables probing, GLEIF lookups, and sanctions

data_dir: "./data"

swg:
  url: "https://swg.example.com:8081"
  poll_interval: "15m"
allodvrm -config vrm.yaml

The admin UI is at http://localhost:9090. With a license key and SWG URL configured, VRM will start importing shadow IT observations and queuing vendors for assessment automatically.

License key required Network probing, GLEIF entity verification, and sanctions monitoring require a valid license key. The SWG integration and manual vendor management work without one.

vrm.yaml reference

server:
  listen: ":9090"
  admin_password: "changeme"
  contact_psk: "${CONTACT_PSK}"   # Hex-encoded key for PII encryption (32+ hex chars, i.e. 16+ decoded bytes)

store:
  driver: "sqlite"          # sqlite (default) | postgres
  dsn: "./data/vrm.db"

license:
  key: "lic_..."
  scan_interval: "24h"

probe:
  interval: "24h"
  proxy_url: ""             # Optional outbound proxy for probe requests

enricher:
  ollama_url: "http://localhost:11434"
  model: "qwen2.5:7b"       # Any Ollama-served model
  interval: "2h"

scheduler:
  interval: "1h"            # How often to check for due reviews

risk:
  interval: "6h"            # How often the Risk Register auto-seed engine re-evaluates systems

swg:
  url: "https://swg.example.com:8081"
  poll_interval: "15m"
  min_device_pct: 0        # Only triage apps seen by at least N% of fleet; 0 = off

usage:
  zero_traffic_days: 60    # Move to pending_offboarding after N days without traffic

# Optional: S3-compatible storage for contract documents and signed
# receipts. The vendor e-signing flow (/sign/*) is only registered when
# docstorage.enabled is true - see "Vendor e-signing" below.
docstorage:
  enabled: false
  endpoint: ""              # e.g. "http://localhost:9000" or "s3.amazonaws.com"
  bucket: "allod-vrm-docs"
  access_key: "${DOCSTORAGE_ACCESS_KEY}"
  secret_key: "${DOCSTORAGE_SECRET_KEY}"
  prefix: "docs"
  region: "eu-west-1"
  retention_days: 0        # 0 = keep indefinitely (contracts/receipts are compliance records)
  versioned: false

# Optional: outbound SMTP for vendor e-signing invitation emails.
mail:
  host: ""
  port: 587
  user: ""
  pass: "${SMTP_PASS}"
  from: "vrm@example.com"

signing:
  link_expiry: "168h"       # How long a vendor e-signing link stays valid (default 168h = 7 days)
  public_base_url: "https://vrm.example.com"  # Externally-reachable base URL used to build signing links

data_dir: "./data"

SWG integration

When swg.url is configured, VRM polls the SWG controller every poll_interval for observed SaaS applications across the fleet. New apps not already in VRM's vendor inventory are automatically added to the triage queue. SWG separately pushes data-type classification events to VRM on its own schedule - see Data classification & risk.

Auth between VRM and SWG uses a per-customer peer token issued by the license server. VRM caches this token locally and refreshes it on each license check-in.

Usage-aware review scheduling

VRM tracks the last time each vendor was observed sending traffic. Systems that go quiet for zero_traffic_days (default 60) are automatically moved to pending offboarding status, flagging them for review before the contract or DPA is renewed.

Similar systems

Both triage queues - new shadow IT waiting for a decision, and systems flagged for offboarding - show a sidebar of similar existing systems. For a new discovery, that's usually the fastest way to spot "this is just another team's Trello" instead of registering a duplicate. For an offboarding candidate, it's the same list framed the other way: consolidate onto an active alternative, or confirm there isn't one and offboard outright.

The min_device_pct filter lets you ignore shadow IT that has only appeared on a small fraction of the fleet - useful in large organisations where one-off experiments shouldn't create triage work automatically.

Data classification & risk

SWG's DLP engine tags every matching transfer with a business-taxonomy category - PII, FIN, IP, CUST-DATA, CRM, or any custom tag a fleet's own YARA rules emit - independent of which rule actually wins the block/allow decision. Only events that carry a tag are batched (every 30s or 100 events) and pushed to VRM's POST /api/v1/swg/classification-events ingest endpoint over the same peer-token auth SWG already uses to pull VRM's approved-app list. VRM resolves each event's destination to a System by SaaS-category ID or host match and upserts it into that system's observed-tag set; an event that can't be matched to a known system is logged and skipped rather than failing the whole batch.

Approved data-tag sets & enforcement

Each System has an ApprovedDataTags list and a ClassificationEnforcement mode (audit / warn / block), set on the system detail page. The auto:data_classification_mismatch risk rule opens, refreshes, or auto-closes a Risk Register entry whenever a system's observed tags exceed its approved set, following the same idempotent apply-rule contract as every other risk rule (see Risk Register) - it never overwrites a risk a human has already acted on. In warn mode the system owner is also emailed. audit mode records the mismatch silently.

Setting enforcement to block moves the check from VRM's periodic risk tick to real time on SWG itself: dlp.ApplyApprovedTagEnforcement runs at all four DLP inspection call sites in the proxy immediately after a transfer's tags are computed, and rejects it outright when it carries a tag outside the destination system's approved set. SWG caches VRM's approved-tag policy for 5 minutes and fails open - never blocking - if VRM is unreachable or has no policy for that destination.

Tag-to-classification mapping

Observed tags are unassigned by default - there is no hardcoded mapping from a business-taxonomy tag to a ClassificationLevel, and no automatic reclassification of a system. An admin explicitly maps each tag to a level on the /classification page; a custom or unrecognized tag still surfaces there for review rather than being silently stored and ignored.

RoPA auto-suggestions

A DLP rule's Tag can carry a granular coarse:category_key subtag (for example PII:email), set directly in the SWG rule editor. The coarse prefix behaves exactly like a plain tag for approved-tag enforcement and the mismatch risk rule above, but a recognized category_key additionally becomes a pending suggestion on VRM's "RoPA suggestions" triage tab - reviewed and explicitly accepted or dismissed by the DPO, never auto-written, since a DLP detection can false-positive and RoPA correctness carries legal weight.

A system's RoPA tab itself only appears once that signal exists - an admin-approved or SWG-observed PII tag, or RoPA content already on record - so a system with nothing pointing to personal-data processing doesn't carry an empty tab on its detail page.

Peer YARA rule API

Peer-token-authenticated endpoints (/api/v1/peer/yara-rules, plus /test) let VRM's AI-assisted Informationstaggar page list, create, update, delete, and test-drive custom YARA rules on SWG without needing an admin UI session - mirroring the existing /api/casb/v1/* peer-auth pattern used for the approved-app list. Test-driving a candidate rule always runs against a disposable YARA engine instance, never the live one.

Standard rule pack

The Informationstaggar page ships with a bundled starter pack of six ready-made YARA detectors, installable to SWG with one click via the peer API above, so there's something to observe before anyone hand-writes a rule:

DetectorTagMatches
Email addressPII:emailStandard email address format
Phone numberPII:phoneE.164-style international format
National IDPII:national_idSwedish personnummer, US SSN, UK NINO, French INSEE, Canadian SIN, or passport MRZ
IP addressPII:ip_addressIPv4 or IPv6
Device IDPII:device_idMAC address
Payment / IBANFIN:financialPayment card number or IBAN format

Each rule ties to its tag via YARA's own target_tag metadata and complements, not replaces, SWG's checksum-validated built-in national-ID and financial-data detectors. Once installed, a starter rule is an ordinary custom rule - edit or remove it exactly like a hand-written one. Categories with no reliable fixed format - name, address, health, and the other free-text personal-data categories - are deliberately not covered: a content pattern that's precise enough to ship enabled by default doesn't exist for them.

Firewall & SSE log ingestion

Shadow-IT discovery doesn't require ALLOD SWG - VRM can also feed its triage queue directly from your existing firewalls and cloud security (SSE) services, so the same discover-probe-govern loop works even where SWG isn't deployed. Firewalls send syslog; SSE services push their logs to VRM over HTTPS.

VRM only counts allowed connections per destination - a blocked or denied attempt doesn't mean the destination is in use. Hits are tallied in memory and flushed every 15 minutes into the same discovery/triage/offboarding pipeline SWG feeds: a destination that matches an existing system's hosts, or is a subdomain of one, updates its last-seen time and hit count; a new one becomes a pending triage entry, tagged from firewall or from SSE. Nothing about individual log lines is stored. When a log line only carries an IP address, VRM reverse-resolves it to a hostname and falls back to the IP.

Supported sources

SourceTransportWhat counts as a hit
Fortinet FortiGateSyslogNative key=value logs with action accept/allow/permit; hostname or URL preferred over destination IP.
Palo Alto Networks PAN-OSSyslogDefault CSV format: TRAFFIC logs with action allow (destination IP), and URL filtering logs (THREAT, subtype url) with action alert, allow, continue or override (URL hostname).
Cisco ASA / Firepower Threat DefenseSyslogBuilt TCP/UDP connection messages (302013, 302015) for ASA and FTD, and FTD connection events (430002, 430003) with AccessControlRuleAction Allow or Trust - URL hostname preferred over DstIP.
Check PointSyslogLog Exporter in its syslog, key=value or CEF format, with action Accept; the host of resource (URL Filtering / Application Control) preferred over dst.
Zscaler Internet AccessHTTPS push or syslogCloud NSS feeds over HTTPS, or NSS feeds over syslog in JSON or CEF output format, with action Allowed.
Cloudflare Zero Trust GatewayHTTPS pushLogpush HTTP destination for the Gateway HTTP, DNS and Network datasets: HTTP/Network records with Action allow or isolate, DNS records whose ResolverDecision is one of the allowed* values.
NetskopeHTTPS pushPage, application and network event records as JSON, with no action or action allow, bypass, alert or useralert; the destination comes from domain, url or dsthost.
Generic CEFSyslogAny product emitting CEF: act allow/accept/permit/pass; dhost, then the host of request, then dst.
Generic LEEFSyslogLEEF 1.0 and 2.0: action allow/accept/permit/pass; host of url, then dst.

All sources are configured under Settings → Log Sources, which also shows per-source activity (messages, hits, data transferred, last received, last error), the last flush to Triage, and a Flush now button.

Data transfer from logs

Where a log line carries byte counts, VRM adds them to the matched system's usage, so the environmental footprint works without SWG too. Fields read per format:

FormatUp (client to server)Down (server to client)
Fortinet FortiGatesentbytercvdbyte
Palo Alto Networks PAN-OSBytes SentBytes Received
Cisco ASATeardown messages (302014, 302016) report one total without direction; it is shown as such.
Check Pointclient_outbound_bytes or sent_bytesserver_outbound_bytes or received_bytes
Zscaler Internet Accessreqsize (web) or outbytes (firewall)respsize (web) or inbytes (firewall)
Netskopeclient_bytes or cs-bytesserver_bytes or sc-bytes
Generic CEFinout
Generic LEEFsrcBytesdstBytes

Cloudflare Zero Trust Gateway logs carry no byte counts, and Firepower connection events (430002, 430003) contribute hits only. PAN-OS byte counts are read from TRAFFIC end records, which carry the session totals. FortiGate session-end records (close, timeout, client-rst, server-rst) and ASA teardowns contribute their byte totals without counting as an additional hit, and never add a new system to Triage on their own.

Syslog (firewalls)

VRM listens for syslog on UDP and TCP port 5514 (not the classic 514 - binding a privileged port would conflict with the service's NoNewPrivileges hardening; port-forward 514→5514 or point the appliance at 5514 directly). RFC 3164 and RFC 5424 framing are accepted, and over TCP both newline-delimited and octet-counted (RFC 6587) messages.

Each sender's IP is mapped to its log format in the Log Sources tab. A sender that shows up without a mapping is listed under Senders without a mapping with a message count - its messages are ignored until you add it, which is one click from that list.

HTTPS push (SSE services)

Create a push source per service in the Log Sources tab. VRM generates a secret token and shows the push URL once, as https://<vrm>/ingest/logs/<token>; alternatively post to https://<vrm>/ingest/logs with the header Authorization: Bearer <token>. Only a hash of the token is stored; if it's lost, generate a new one (the old URL stops working immediately). The service must be able to reach VRM over HTTPS.

  • Zscaler: create a Cloud NSS feed with output type JSON (JSON array notation off) and use the push URL as its API URL.
  • Cloudflare: create a Logpush job with an HTTP destination for the Gateway HTTP, DNS or Network dataset. To keep the token out of the URL, use the base URL with ?header_Authorization=Bearer%20<token>. Logpush's destination validation upload is accepted automatically.
  • Netskope: post event records as JSON to the push URL, for example from Netskope Cloud Exchange. Netskope's CEF syslog output can instead be added as a Generic CEF syslog sender.

The endpoint accepts newline-delimited JSON, concatenated JSON objects or a JSON array, optionally gzip-compressed (by Content-Encoding: gzip or detected from the body). Responses:

StatusMeaning
200Accepted; the body reports records and hits.
400The body isn't valid JSON (recorded as the source's last error).
401Unknown or missing token.
413Body over 32 MiB as sent, or over 256 MiB after decompression.
429Over 600 requests per minute from one IP.

API

Everything in the Log Sources tab is also available to admin API keys: GET/PUT /api/v1/admin/logsources-config (syslog mappings, status, supported vendors), POST /api/v1/admin/logsources/push (create a push source - the only response containing its token), POST /api/v1/admin/logsources/push/{id}/rotate, DELETE /api/v1/admin/logsources/push/{id}, and POST /api/v1/admin/logsources-flush.

DNS vendor discovery

Your own DNS records name many of the vendors you depend on: every SaaS tenant that verified your domain, every service allowed to send mail as you, your mail and DNS providers, and the services behind your subdomains. VRM reads these records for your organisation's domains and adds each vendor it finds to the triage queue, including vendors that never show up in SWG or firewall traffic.

Organisation domains

Domains are managed under Triage → Organisation domains. A domain you add there is confirmed straight away. VRM also suggests domains from these sources:

SourceWhat is suggested
SCIM usersThe mail domains of active users. Consumer mail providers and *.onmicrosoft.com names are skipped.
SWG internal zoneThe registrable domain of hosts SWG reports under your internal DNS zone. Non-public zones such as corp.local are skipped.
Certificate TransparencyDomains that share a certificate with a confirmed domain and contain its name, such as example-group.com next to example.com. Requires a license key.
Microsoft 365 tenantOnce a confirmed domain turns out to use Microsoft 365, the other domains verified in the same tenant, from Microsoft's Autodiscover service.

Suggested domains are never scanned. Confirm one to include it, or Ignore it to keep it from coming back. Removing a domain leaves the systems already found from it in place.

What is scanned

Confirmed domains are scanned when VRM starts and every 24 hours, or right away with Scan now. A domain with no name servers is marked DNS lookup failed and skipped.

RecordWhat it reveals
TXT domain verificationSaaS tenants that verified the domain, e.g. MS=, google-site-verification=, atlassian-domain-verification=, docusign=. Tokens from vendors VRM doesn't recognise are listed under the vendor's name from the token.
SPFServices allowed to send mail as you (include: and redirect=). Includes pointing into your own domains are followed.
MXMail provider and mail security gateway.
DKIMMail senders, from common selectors such as selector1, google, k1 and s1.
DMARCWhere aggregate and forensic reports are sent (rua, ruf).
NSDNS provider.
CAACertificate authorities allowed to issue for the domain.
SRVHosted communication services, e.g. Microsoft Teams.
CNAMEServices behind subdomains found in Certificate Transparency logs, e.g. a support portal on Zendesk. Requires a license key.
A / AAAAHosting provider of the domain and its www host. Requires a license key.

Records that point at one of your confirmed domains, or at a domain with the same name under another top-level domain, are treated as your own infrastructure.

In the triage queue

Each vendor becomes one pending triage entry, however many records point at it. The entry is tagged from DNS with the strongest record type, e.g. from DNS (SPF), and lists every record it was found in. Its host is the vendor's catalogue hosts when it matches a known SaaS app, otherwise the vendor's website, otherwise the domain the record points at. A verification token from an unrecognised vendor has no host - set one before you approve the entry.

A vendor that already exists as a system in any status, matched by SaaS app, host or name, is not added again, so a rejected vendor stays rejected. Each scan that adds entries creates one notification listing them.

Records left after decommissioning

When a system is set to Decommissioning or Inactive, each scan also lists the records that still point at its vendor. Setting a system to Decommissioning starts a scan straight away. The records appear in a Leftover DNS and CSP records card on the system's page, and each record type opens its own risk in the Risk Register. A risk closes by itself once a scan no longer finds its records.

Record left behindWhat it allowsLikelihood / impact
CNAMEAnyone who claims the same name at the provider can serve content on your subdomain (subdomain takeover).Medium / High
NSWhoever controls the delegated zone at the provider controls every name in it.Medium / High
TXT domain verificationAnother tenant at the service can claim your domain with the token.Medium / High
MXIncoming mail is delivered to a provider you no longer have an agreement with.Medium / High
SPFThe provider's other customers can send mail that passes SPF as you. Each include also uses one of SPF's 10 DNS lookups.Medium / Medium
DKIMMail signed with the old key still passes DKIM for your domain.Medium / Medium
Content-Security-PolicyYour pages still allow content from the host. See below.Low / Medium, or High / High
CAAThe certificate authority may still issue certificates for the domain.Low / Low
DMARCReports about your mail keep going to the provider.Low / Low
SRVClients may still be directed to the provider.Low / Low
A / AAAAThe address may be reassigned to another customer of the provider.Low / Low

With a license key, the scan also reads the Content-Security-Policy and Content-Security-Policy-Report-Only headers of the domain, its www host and up to 50 subdomains found in Certificate Transparency logs. Hosts listed in these policies are only matched against decommissioned systems and never create triage entries. A host whose domain is not registered is marked Domain unregistered. If that host may serve script (script-src, default-src, worker-src, child-src or object-src), anyone can register the domain and run code in your pages, and the risk is raised to High / High.

A record is not listed when a system still in use points at the same vendor, for example when Teams is decommissioned but Outlook on the same Microsoft 365 tenant is active. If a domain's DNS cannot be read during a scan, its records from the previous scan are kept. The first scan that finds records for a system creates a notification.

VRM never changes DNS records or headers. You can still set a system to Inactive while records remain. The page shows a warning, and the risks stay open until the records are removed.

API

The tab is also available to admin API keys: GET /api/v1/admin/org-domains (domains and the last scan), POST /api/v1/admin/org-domains (add and confirm a domain), PUT /api/v1/admin/org-domains/{domain} (set confirmed, ignored or suggested), DELETE /api/v1/admin/org-domains/{domain}, and POST /api/v1/admin/org-domains/scan (starts a scan in the background). GET /api/v1/systems/{id}/dns-residuals lists the records still pointing at a decommissioned system.

Vendors

A vendor is a company; a system is one of its products or services that you use. Several systems can belong to one vendor - Teams, SharePoint and Exchange Online all belong to Microsoft - and everything that describes the company is kept once, on the vendor, instead of on every system.

How systems are linked

A new or synced system is linked to an existing vendor by name, ignoring company-form suffixes (Microsoft Corporation and Microsoft Corp. are the same vendor), and otherwise by the registrable domain of its website. When no vendor matches, one is created. Changing the vendor name on a system moves it to the vendor of that name.

Inherited facts

Headquarters country, ultimate owner country, privacy policy URL and DPA URL are set on the vendor. A system shows the vendor's value while its own field is empty; filling the field overrides it for that system only. Sovereignty scoring uses the vendor's jurisdiction unless the system overrides it.

What lives on the vendor

ItemScope
Company-level probes: SPF, MX, DMARC, MTA-STS, BIMI, RDAP, security.txt, trust portal, CSAF, privacy policy, DPA, ToS, company registry, jurisdiction, supportRun once per vendor against its website domain; every system of the vendor shows the same result. Product-level probes (hosting, TLS, origin IPs, SBOM and so on) still run per system.
Contacts and contractsEither vendor-wide or tied to one system. Vendor-wide contracts can be sent for e-signing from the vendor page.
Review cyclesA cycle type has a scope. A vendor-scoped cycle creates one review per vendor instead of one per system.

Vendors page

Admins get a Vendors list and a page per vendor with its facts, systems, contacts, contracts, reviews and probe results, and the lowest sovereignty level among its systems (with the sovereignty assessment turned on). A vendor that still has systems or contracts can't be deleted; merge it into another vendor instead. A merge moves everything to the surviving vendor and keeps the merged name as an alias, so a later sync that still uses the old name links to the right vendor. When a vendor's last system is deleted and it has no contracts, the vendor is removed too. The vendor API (/api/v1/vendors) is admin-only, because a change there affects every system of the vendor.

Vendor assessment

The probe checker runs automatically for all vendors that have a catalog entry, on probe.interval. Each run executes over two dozen independent, purely external checks - no credentials, no access to the vendor's systems - to build a factual picture that would otherwise take a human hours of manual digging:

ProbeWhat it checks
Hosting geographyResolves the domain to IPs, geolocates each, and detects CDNs - relevant for GDPR third-country transfer assessment, since a CDN in front of the app implies that provider processes the data too.
TLSCertificate chain and issuer. An issuer name like "Cloudflare Inc ECC CA-3" means TLS - and therefore all traffic - is actually terminated at the CDN, not the vendor's own infrastructure.
Origin IP discoveryTries to find the real origin server(s) behind a CDN/WAF via certificate-transparency logs, passive DNS, and common-subdomain probing, then geolocates anything that isn't itself a known CDN - the probe that makes third-country assessment accurate when a vendor fronts everything with Cloudflare.
SPFIdentifies third-party mail services authorised to send on the domain's behalf - those providers process outbound email data too.
MXResolves and geolocates mail servers - which country and which provider (Google Workspace, Microsoft 365, ...) handles inbound mail.
DMARCParses the domain's DMARC policy; p=reject indicates a mature anti-spoofing/anti-phishing posture.
MTA-STS / TLS-RPTConfirms the vendor enforces and monitors TLS on inbound SMTP - a concrete GDPR Art. 32 technical measure for email-borne personal data.
BIMIWhen a Verified Mark Certificate is published, extracts the CA-verified legal entity name and jurisdiction from it - useful for corroborating Art. 28 DPA counterparty identity.
CAAWhether the domain restricts which CAs may issue certificates for it; a missing record means any CA worldwide could be social-engineered into issuing one.
NSGeolocates the domain's nameservers and reports whether all resolve to EU addresses (a signal, not definitive - anycast providers can resolve to a non-EU PoP even with EU capacity).
RDAPStructured registration data: DNSSEC status, domain lock level, DNS provider, registrar, expiry - Art. 32 infrastructure-hardening signals.
security.txtPresence of /.well-known/security.txt (RFC 9116) - indicates a responsible-disclosure process, relevant to Art. 32 incident response.
CSAF / VDPPresence of a CSAF provider-metadata document at its standard discovery location, alongside security.txt - together they count as evidence of a NIS2 Art. 21 coordinated vulnerability-disclosure process. A high/critical-criticality vendor with neither auto-opens a Risk Register entry. When a vendor does publish CSAF, its advisories feed vulnerability monitoring: entries for your systems' products open risks, notify the owner when fixed, and suppress SBOM findings the vendor marks as not affecting it.
HTTP security headersChecks the domain root for HSTS, CSP, and similar response headers - missing ones are a security-hygiene gap.
Trust portalDetects a published compliance trust center on common trust/security subdomains and paths, and extracts what it finds - certifications, DPA availability, sub-processor list.
OpenAPILooks for a public API specification (well-known paths, developer-portal crawl, or inline Redoc/Swagger extraction) - feeds SOV-6 (Technology) of the CSF SEAL score.
Open sourceScans the homepage and a few adjacent pages for explicit open-source license declarations.
SBOMWhether the vendor publishes a Software Bill of Materials - feeds SOV-5 (Supply Chain) of the CSF SEAL score, and its absence for a high/critical-criticality vendor auto-opens a Risk Register entry (see Risk Register).
Company (GLEIF)Looks the vendor up in the GLEIF LEI database for country of incorporation and ownership chain - feeds SOV-1/SOV-2 of the CSF SEAL score. See GLEIF & Ownership monitoring.
Jurisdiction (fallback)When GLEIF has no LEI record for the vendor (common for privately held companies), a heuristic regex-matches VAT/company-registration-number formats on the vendor's own site as a weaker fallback signal for SOV-1 - never overriding a real GLEIF resolution when one exists.
Login / SSODetects third-party SSO support from the outside (OIDC discovery, SAML endpoints, IdP redirects) and whether the login page's own text mentions MFA - a text mention, not a verified control, since actual enforcement can't be confirmed without authenticating.
SCIM provisioningDetects whether the vendor supports SCIM v2 for automated user provisioning/deprovisioning - relevant to how quickly access to that vendor can actually be revoked when someone leaves, versus depending on a manual offboarding step.
Support / company pageFetches a handful of contact/about/support pages for vendor contact details.
Privacy policyFetches and stores the vendor's privacy policy page for LLM extraction and change tracking.
DPAFetches and stores the Data Processing Agreement if publicly available (PDF links are recorded but not parsed).
ToSFetches and stores the Terms of Service / EULA page for LLM extraction and change tracking.

Probe results are stored in the vendor record and surfaced in the admin UI review workflow. The LLM enricher processes privacy policy and DPA text to extract structured data; several probes above (Company, SBOM, OpenAPI, and RoPA/DPA fields) also feed the CSF SEAL sovereignty score and the automated Risk Register.

OSCAL scanning

Licensed instances additionally probe a vendor's domain for a publicly served OSCAL System Security Plan or component definition, checked at the well-known locations vendors commonly publish them (/.well-known/oscal.json and similar). When found, it's parsed and attached to the vendor record as machine-readable evidence of the vendor's own security control implementation - no manual document collection required.

Risk Register

Every active or decommissioning system is automatically evaluated on risk.interval (default 6h) against a fixed set of rules, each sourced from data the probe checker and RoPA already collected - no separate risk-assessment workflow to run by hand. Inactive systems are checked only for leftover DNS and CSP records. An auto-seeded risk is only ever opened, refreshed, or closed by the engine while untouched: the moment a human sets its status, treatment, owner, or a time-boxed acceptance, the engine stops overwriting those fields and only auto-closes it once its condition genuinely clears.

RuleOpens when
Data classification mismatchTraffic observed via SWG's classification-events feed carries a data-type tag outside the system's approved data-tag set.
Sovereignty bottleneckThe system is high/critical criticality and its overall CSF SEAL score is at the lowest evaluated level. Only while the sovereignty assessment is turned on.
Missing DPARoPA marks the system as processing personal data, but no DPA is recorded as in place (Art. 28 GDPR).
Missing SBOMThe system is high/critical criticality and the SBOM probe found nothing publicly published.
Missing vulnerability-disclosure processThe system is high/critical criticality and probing found neither a security.txt nor a CSAF advisory feed - see Vendor assessment.
Contract expiringThe contract end date is within 60 days (including already past).
License expiringThe license expiry date is within 60 days (including already past).
Unassessed critical vendorThe system is high/critical criticality but no probe, RoPA sovereignty field, or vendor data exists yet to evaluate any CSF SEAL objective. Only while the sovereignty assessment is turned on.
Contractual lock-inThe vendor's terms require more than 180 days' notice to terminate, or more than 90 days on an auto-renewing subscription.
No data export on terminationThe vendor's terms state the customer cannot export its data after termination.
Low liability capThe vendor's terms exclude its liability or cap it below 12 months of fees.
Non-EEA governing lawThe vendor's terms (or, if silent, its DPA) are governed by the law of a country outside the EU/EEA and EU adequacy countries.
Short price-change noticeThe vendor's terms allow price changes with less than 30 days' notice.
Incomplete DPAA DPA is recorded as in place, but analysis of the vendor's DPA finds Art. 28 GDPR elements missing.
Leftover DNS or CSP recordThe system is decommissioning or inactive and your DNS records or Content-Security-Policy headers still point at its vendor. One risk per record type - see Records left after decommissioning.

The six contractual rules read the terms that LLM enrichment extracts from the vendor's published Terms of Service and DPA, and from the contracts uploaded on the vendor's or system's Contracts tab (PDF or DOCX). An uploaded contract overrides the published terms field by field: where the contract states a term, its value applies; where it is silent, the published value still does. With several contracts, the most recently updated one wins. Every uploaded contract counts except voided or expired ones, including drafts, since a contract uploaded already signed stays a draft. A contract is re-analysed whenever its file is replaced. Each risk's description names the document its term came from. A term no document mentions never opens a risk, and a risk closes automatically once the documents no longer meet its condition.

Each opened risk gets a likelihood/impact pair and a score (likelihood × impact, on the same ordinal scale used for manually-entered risks), a Mitigate treatment by default, and the system's own owner pre-assigned.

A vendor's published SBOM and CSAF advisories feed the Risk Register through a separate, ongoing mechanism rather than one of the fixed rules above - a distinct entry per vulnerability found, not just per vendor - see vulnerability monitoring.

CSF SEAL sovereignty scoring

Each system is scored against the 8 sub-objectives of the CSF SEAL v1.2.1 sovereignty framework - Strategic, Legal, Data & AI, Operational, Supply Chain, Technology, Security, and Environmental (SOV-1 through SOV-8) - using whatever combination of probe results, RoPA fields, and manually-entered sovereignty data is available for that system. An objective with no evidence at all is left unassessed (not scored as failing). The overall level shown on a system is the minimum across every objective that could be evaluated - a single weak dimension caps the whole score, rather than being averaged away by strong ones elsewhere - alongside a separate 0-100 weighted score for trend tracking, where an unassessed objective contributes 0 to that average rather than being excluded from it.

The sovereignty assessment is opt-in: an admin turns it on under Settings > Features. While it is off, the score, the Sovereignty tab and the vendor list's sovereignty column are hidden and the two sovereignty rules raise nothing. Turning it off closes their open risks; turning it on again reopens those whose condition still holds. A risk someone closed stays closed, and entered sovereignty data is kept.

This scoring runs server-side identically to (and stays in sync with) the same computation the system-detail page has always shown client-side, so the number driving Risk Register auto-seeding always matches what's on screen.

Concentration and fourth parties

VRM maps which providers your systems depend on - the vendors' own suppliers, or fourth parties - and how many of your critical systems share each one.

DependencyWhere it comes from
HostingThe network owners of the addresses the system resolves to and of its origin servers behind a CDN (vendor assessment).
CDNCDN detection on the hosting addresses, the TLS certificate and the response headers.
DNSThe providers of the vendor domain's name servers.
MailThe providers behind the vendor domain's MX records.
Sub-processorThe RoPA record's sub-processors and those named in the vendor's DPA and privacy policy.

Names are mapped to the company behind them, so AMAZON-02, AWS Route 53 and Amazon.com, Inc. all count as Amazon Web Services. Systems that are inactive, rejected or pending are not counted; a system is critical when its criticality is high or critical.

A provider that at least 3 critical systems, and at least half of all critical systems, depend on gets one organisation-wide Risk listing those systems and how each uses it - impact high from three quarters. It is refreshed as the numbers change and closes by itself when they fall below the threshold.

The dashboard's Concentration card lists the providers more than one system depends on, with the share of critical systems; each system page shows what that system depends on.

Environmental footprint

VRM estimates the climate impact behind your vendors in three ways and shows them on the Sustainability page (admin), with a CSV export. All figures are estimates, labelled with their method and sources.

Vendor climate maturity

Two probes look at each vendor: whether the system's app domain runs on verified green hosting (the Green Web Foundation's check), and where the vendor's sustainability or climate page is. LLM enrichment reads that page for scope 1, 2 and 3 emissions, the reporting year, the renewable share of its electricity, a net-zero year, its Science Based Targets (SBTi) status, whether it reports under the CSRD, and its revenue. Any of these can be entered on the vendor page; an entered value overrides what was read from the page.

Score pointWhen
1A sustainability report or emission figures are published.
2The vendor has a climate target: a net-zero year or an SBTi commitment.
3The SBTi has validated its near-term or net-zero target.
4It reports scope 1, 2 and 3 and runs on mostly renewable electricity or verified green hosting.

A high or critical system whose vendor has been checked and has no climate target gets a low compliance risk in the Risk Register; it closes once a target is found or entered.

Usage

For systems seen by SWG or reported in firewall and SSE logs, the data transferred over the last 30 days is turned into emissions with the Sustainable Web Design Model v4: data-centre energy at the carbon intensity of the hosting country's grid (none for its operational share on verified green hosting), and networks, end-user devices and embodied energy at the world average. Grid intensities come from Ember (CC BY 4.0). The system page shows the result, with data up and down and where the figures come from. When SWG has reported the system within the last two days its measurement is used; otherwise the log sources' 30-day total. The two are never added together, since a firewall normally sees the same traffic SWG does.

Spend (scope 3, category 1)

The analysis of uploaded contracts also reads the contract value, currency, billing period and term; an admin can enter or correct the value per contract on the vendor page. Values are annualised and converted to euro at the ECB's daily reference rates. Emissions per euro use the vendor's own intensity - its reported emissions divided by its revenue - when both are known, otherwise 0.0921 kgCO2e per euro: the UK Government's 2022 average for computer programming, consultancy and related services (SIC 62, 0.108 kgCO2e per pound) at the ECB's 2022 exchange rate. The CSV export is a starting point for CSRD ESRS E1-6 scope 3 reporting.

DORA register of information

Financial entities under the Digital Operational Resilience Act keep a register of their contractual arrangements for ICT services (Article 28(3) DORA) in the templates of Commission Implementing Regulation (EU) 2024/2956, and report it to their competent authority each year. VRM keeps that register and exports it as the reporting package the EBA specifies.

The register is opt-in: an admin turns it on under Settings > Features, and the DORA page appears in the menu. Turning it off hides the page and its API; what was entered is kept.

What is filled in for you

Every field is a column of one of the 15 templates (B_01.01 to B_99.01). Where VRM already knows the answer, the register reports it and shows where it came from; whatever you enter takes precedence.

SourceFills in
GLEIFThe provider's LEI, legal name and country, and its ultimate parent - which is also listed as a provider, as the template requires (GLEIF & Ownership).
Uploaded contractsStart date, end date from the term, notice period, the country of the governing law and the annual cost, converted to your reporting currency at the ECB's reference rates (contract values).
SystemsSaaS as the type of service for catalogued SaaS apps, the countries of provision, storage and processing from where the system is hosted, data sensitivity from its RoPA (special categories high, other personal data medium), the level of reliance from its criticality, and an exit plan when one is attached.
Rules of the ITS and the EBA"Not applicable" where a key does not apply, 9999-12-31 for an entity not deleted, a provider without a parent as its own ultimate parent, the reporting entity as signer and user of each arrangement.

The overview lists systems in use that are not yet in the register, with their contracts, and registers one with a click: an arrangement with the vendor and contract, and the system's ICT service under it. The supply chain tab suggests the sub-processors named in the vendors' DPAs and privacy policies.

Checks before export

The register is checked against the validation rules of the EBA's DORA taxonomy, the keys and references of its data model, LEI check digits (ISO 17442) and the EBA's answers on how to fill in the templates. Each finding links to the field to correct. Findings the ESAs would reject the whole file for - an empty key, a reference to something not in the register, an LEI that is not the reporting entity's - block the export; data quality findings do not.

Export

Export package downloads the register for a reference date (by default 31 December of the previous year) as the EBA plain-csv reporting package for taxonomy 4.0, module DORA010100: a ZIP named LEI.IND_SE_DORA010100_DORA_2025-12-31_...zip (.CON for a consolidated register) with one CSV per template. Submit it through your competent authority's reporting channel. The Templates tab shows the rows exactly as exported, with the filled-in cells marked.

Vulnerability monitoring

For every vendor whose published SBOM the SBOM probe finds (CycloneDX or SPDX), VRM extracts each component's package URL (purl) and re-checks it against the OSV.dev vulnerability database on vulnscan.interval (default 24h) - continuous monitoring of your vendors' own dependencies, not a one-time snapshot at onboarding.

Every finding opens its own Risk Register entry - one per vulnerable component, not one per vendor - scored by severity (low/medium/high/critical, normalized from OSV's own severity data or CVSS score when OSV doesn't provide one directly) and pre-assigned to the system's owner.

CSAF/VEX cross-checking

When a vendor also publishes a CSAF advisory feed, every OSV finding is cross-checked against the vendor's own VEX statements before a Risk is opened. A finding is silently suppressed only when the vendor's own advisory classifies that exact purl/CVE pair as known_not_affected - matched precisely, never inferred from a name or version range - so a vendor that's already told you a CVE doesn't apply to their build doesn't also generate duplicate triage work.

Vendor-reported vulnerabilities (CSAF)

A vendor's CSAF feed covers every product it ships, so VRM keeps only the entries that belong to the system: purls found in the vendor's SBOM, and advisory products whose name contains the system name as whole words (Teams matches Microsoft Teams 1.5, never Steamship). When the vendor names the product differently, enter the names under CSAF product names on the system page - they replace the system-name match and take effect immediately.

Entries the vendor marks known_affected, first_affected, last_affected or under_investigation open a Risk Register entry, the same as an OSV finding. The system page's CSAF card counts the reported CVEs per status, highlights unfixed high/critical ones, lists the matched products, and shows how many CVEs in the feed concern other products.

Fix released by the vendor

When an entry for the system turns fixed or first_fixed, the system owner gets one notification per scan listing the CVEs, links to the advisories, and any open Risk to close once the fixed version is deployed. Risks are not closed automatically. The first crawl of a feed only records the baseline and sends nothing.

Known exploitation and EPSS

Every finding, from OSV or the vendor's CSAF advisories, is checked against the CVEs known to be exploited in the wild - CISA's KEV catalogue and ENISA's EU Vulnerability Database - and given its EPSS score, the probability that it is exploited in the next 30 days. Both lists and the scores reach VRM through the Allod license server once a day; VRM itself contacts neither source.

  • A known-exploited CVE opens its Risk with likelihood critical; impact stays the CVSS severity. The Risk records which lists name it, since when, and whether it is used in ransomware campaigns.
  • Its notification is sent whatever the minimum severity, marked as known exploited.
  • A Risk opened before its CVE became known-exploited is raised the same way on the next scan and notified once. Its status, treatment and owner are left as set; closed and accepted Risks are not changed.
  • The Risk Register shows a Known exploited badge and an EPSS badge from 10%, and can be filtered to known-exploited Risks. The CSAF card counts unfixed known-exploited CVEs and lists them first.

Notifications

Configure a minimum severity under Settings → Notifications (default: high) - every finding is still recorded in the Risk Register regardless, the setting only gates outbound delivery. Above that threshold, the system owner is notified by email, in the admin UI for fix notices, and, if configured, Slack and/or Microsoft Teams webhooks. The same threshold applies to new findings and to fixes released by the vendor; known-exploited findings are always sent.

Policy drift detection

VRM re-fetches privacy policies, DPAs, and Terms of Service on the same schedule as the probe checker and compares each new version against the stored baseline using a content hash.

What triggers a drift notification

  • Privacy policy content changes (new subprocessors, revised retention periods, altered transfer mechanisms)
  • DPA content changes (updated legal basis, changed breach notification commitments)
  • ToS content changes (new data use clauses, jurisdiction changes)

When a change is detected, a notification is raised in the admin UI linking directly to the affected vendor. The previous and current versions are both stored so your DPO can review what changed before deciding whether a re-assessment is needed.

Drift vs. probe failure

A fetch error (HTTP 4xx/5xx, DNS failure) is recorded as a probe failure, not a drift event. Drift is only raised when content is successfully fetched and differs from the stored baseline.

LLM re-enrichment When a privacy policy or DPA drift event is raised, the LLM enricher is queued to re-process the document automatically so structured fields stay current.

GLEIF & Ownership monitoring

Every vendor is matched against the Global LEI (Legal Entity Identifier) Index via the Allod license server. A successful match yields:

  • Verified legal entity name and registration jurisdiction
  • Full ownership chain to the ultimate parent entity
  • Country of the ultimate parent - relevant for data transfer assessments

Ownership change alerts

VRM polls for ownership changes every 5 minutes. When an acquisition moves a vendor's ultimate parent to a new jurisdiction, a notification is raised immediately - before your next scheduled review cycle. This gives you time to reassess data transfer impact before contracts renew.

Sanctions monitoring

VRM fetches the EU consolidated sanctions list from the Allod license server daily and checks every vendor name and its ultimate parent against the list.

A match triggers an immediate notification in the admin UI. The same vendor is not re-notified on subsequent checks unless it appears under a new sanctions entry.

Coverage Monitoring is based on name matching against the EU consolidated list. It complements, but does not replace, formal sanctions screening by a qualified compliance team.

GDPR workflows & Art. 30 register

VRM's review system is built around the GDPR Article 30 Records of Processing Activities register - not bolted on after the fact.

Review cycles

A review cycle is an admin-configured template (Admin → Review cycles), not a fixed built-in list - you define its name, its own set of structured fields (text, date, boolean, select, URL; each marked as filled in by the owner or by the vendor), and how it's triggered:

TriggerFires when
intervalEvery N days (interval_days) - a recurring cadence, e.g. an annual review.
contract_renewalThe system's contract end date approaches.
license_renewalThe system's license expiry date approaches.
manualNever fires on its own - started by hand from the system's page.

lead_days controls how far ahead of the trigger date the review task is created. Only one cycle type is seeded by default - DPIA (Data Protection Impact Assessment, manual trigger, 30-day lead, with fields for risk assessment, documented privacy risks, DPO consultation, mitigation measures, and completion date) - everything else, including any "annual" cadence, is something you build with the interval trigger.

The scheduler checks for due reviews every hour (scheduler.interval) and creates review tasks automatically from whichever cycle types are configured, using each cycle's own field template.

Art. 30 register export

The complete Art. 30 register is exportable from Admin → Register as a structured JSON or CSV file, ready for submission to your DPO or supervisory authority.

Vendor lifecycle management

VRM tracks vendors across their full lifecycle - not just during the initial review. Each stage can trigger structured tasks:

  • Onboarding: New vendor discovered via SWG or added manually - review task created, DPA and privacy policy fetched, owner assigned.
  • Active monitoring: Continuous probe runs, ownership polling, sanctions checks, and policy drift detection run in the background.
  • Renewal: Contract renewal date triggers a re-assessment task with pre-filled fields from the previous review.
  • Re-assessment: Any drift event (policy change, ownership change, sanctions match) can trigger an out-of-cycle review task.
  • Offboarding: Marking a vendor as inactive closes open review tasks and archives the Art. 30 record with a decommission timestamp.

Owner management

Each system has an assigned owner. When an owner's account is disabled or removed from the identity provider (via SCIM), VRM raises a notification for every system they own so accountability gaps are caught immediately.

Vendor e-signing

Off unless docstorage.enabled is set - once it is, a system's page gains a contract-document workflow: upload a document (stored encrypted at rest in your own S3-compatible bucket, never Allod's infrastructure), then generate a signing link for it. The link is emailed to a vendor contact via mail (default validity signing.link_expiry, 168h/7 days) and opens a public, unauthenticated page at /sign/<token> where the recipient can review the document and click to sign - no account needed on their end.

This is an attestation model, not a personal cryptographic signature. A click-to-sign event produces a receipt recording the document's SHA-256 (computed server-side from the bytes actually received, never trusted from the client), the signer's name/email, timestamp, IP address, and user agent - then signs that receipt with AllodVRM's own Ed25519 key (a self-signed identity generated and stored locally on first use). The receipt says "AllodVRM attests that signer X signed document Y (hash Z) at time T from IP A" - it does not claim the vendor's own cryptographic identity, the way a qualified electronic signature (eIDAS-style) would. Signed receipts are downloadable from the system's page and are themselves stored in the same document store.

LLM enrichment

The enricher calls an LLM to extract structured data from the privacy policy, DPA, and Terms of Service text fetched by the probe checker, and from the contracts and DPAs uploaded on a vendor's or system's Contracts tab (PDF or DOCX). Extracted data includes:

  • Privacy policy - personal data categories, data subjects, legal bases, transfer countries, and sub-processors, used to pre-fill the RoPA
  • DPA - which Art. 28(3) GDPR elements the agreement contains (processor instructions, subject-matter, duration, nature and purpose, data types, data subjects, sub-processor terms, security measures, audit rights, deletion or return of data), plus named sub-processors and governing law
  • Terms of Service and contracts - termination notice, auto-renewal, data export and deletion on termination, liability cap, governing law, dispute resolution, and price-change notice

These feed the contractual rules of the Risk Register.

No text is ever sent to Allod Solutions or any external AI provider, regardless of which of the two backends below is active - both run entirely on infrastructure you control. Configure either under Settings → AI in the admin UI.

External Ollama

Point VRM at an Ollama instance you already run (enricher.ollama_url). The default model is qwen2.5:7b; any Ollama-compatible model can be used.

Local (bundled, no external server)

Licensed instances can instead run a small local model with nothing else to set up: VRM downloads a llama.cpp runtime and a GGUF model via the license server on first activation, then runs inference in a small supervised child process (allodvrm-llm-helper) rather than in VRM's own process - isolation that matters because the native inference library runs outside Go's memory safety, so a crash there (e.g. running out of memory on a large document) only takes down that helper, which is automatically restarted, not the whole application. The model picker on the AI tab lists whatever's actually available from your license server rather than a name typed from memory, and a live status indicator (Starting / Ready / Error / Stopped) shows whether it actually loaded rather than just what's configured.

The enricher is triggered automatically after each probe run and also runs on a configurable interval (enricher.interval, default 2 h) to process any newly fetched text.

Identity (SCIM / LDAP)

VRM syncs users and groups from your identity directory to enable owner assignment, accountability gap detection, and group-based access control for the admin UI.

Push (IdP → VRM)

Configure under Admin → Identity → SCIM push. Supported: Okta, Microsoft Entra ID, Authentik, and any RFC-7644-compliant SCIM 2.0 IdP.

Pull (VRM polls IdP)

Configure under Admin → Identity → Pull from IdP. VRM supports the same four providers as SWG: scim, authentik, entra, and ldap. Credentials, LDAP field defaults, and Active Directory attribute overrides are identical - see the SWG identity section for the full reference.

Owner lifecycle

Each system in VRM has an assigned owner. When a sync run detects that an owner's account has been deprovisioned or deactivated in the directory, VRM raises a notification for every system they own - so accountability gaps are caught immediately, without waiting for a manual review cycle.

API reference

The VRM REST API exposes all admin actions - vendor management, review workflows, SCIM config, and probe triggers. See the interactive reference at /api-docs → VRM.

Authenticate with the admin password via HTTP Basic, or configure API keys in Admin → API Keys.

Also see: ALLOD SWG API reference →