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: "/etc/allodswg/ca"
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.
  • 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 Setting regenerate: true invalidates all existing leaf certificates immediately. Push the new CA to endpoints before re-enabling TLS inspection.

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.

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.

Firewall log ingestion

Shadow-IT discovery doesn't require ALLOD SWG - VRM can also feed its triage queue directly from your existing on-prem firewall's syslog output, so the same discover-probe-govern loop works even where SWG isn't deployed.

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). Received allowed-connection hits are tallied per destination and periodically flushed into the same discovery/triage/offboarding pipeline SWG feeds - a destination seen enough times becomes a triage candidate the same way an SWG-observed app does.

Fortinet FortiGate is supported today; additional NGFW vendors (Palo Alto, Cisco ASA/FTD, Check Point) are planned. Each syslog sender's IP is mapped to its vendor via PUT /api/v1/admin/logsources-config (no dedicated Settings UI page yet) - a sender that shows up without a configured mapping is tracked as "unmapped" and surfaced in that same endpoint's GET response so you can see it's talking before pointing it at the right parser.

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 VEX advisories also feed SBOM vulnerability scanning's false-positive suppression.
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. 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.
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.

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 feeds 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 SBOM vulnerability scanning.

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.

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.

SBOM vulnerability scanning

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.

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 and, if configured, Slack and/or Microsoft Teams webhooks.

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 privacy policy and DPA text fetched by the probe checker. Extracted fields include:

  • Data retention periods
  • Subprocessor list and their jurisdictions
  • Breach notification commitment (timeframe)
  • Data transfer mechanisms (SCCs, adequacy decision, etc.)

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 →