Strata

Documentationfor Strata 0.9.

Strata maps your information systems in 3D: each site is a stack of plates (zones), coloured by VLAN, holding its assets, the flows between them, what they expose, their vulnerabilities and the people to call. It runs on your own server.

Install with Docker

You need Docker with the Compose plugin, about 1 GB of RAM and 1 GB of disk. To build the image yourself, the host also needs access to the npm registry during the build.

git clone https://github.com/SolidGeeks/strata.git
cd strata
cp .env.example .env
nano .env                         # set EDIT_PASS
docker compose up -d --build      # builds the image here

On Windows: copy .env.example .env then notepad .env.

Or skip the build: docker compose pull && docker compose up -d fetches the ready-made image ghcr.io/solidgeeks/strata of the release. Every image is signed; check it before deploying with gh attestation verify oci://ghcr.io/solidgeeks/strata:0.9.0 --owner SolidGeeks, which must print Verification succeeded!

Open http://localhost:8080 and sign in with EDIT_USER / EDIT_PASS. The map starts empty: Load demo (5 IS) fills it with five sample sites, Delete everything clears it.

docker compose logs -f strata     # logs
docker compose restart strata     # restart
docker compose down               # stop (data stays in the volume)

By default the port only listens on the machine itself (127.0.0.1:8080). See HTTPS and network to open it to your users.

Configuration

Settings live in .env. Apply a change with docker compose up -d.

VariableDefaultWhat it does
EDIT_USER / EDIT_PASSadmin / emptyFirst admin account, created or reset at every start. With no EDIT_PASS and no account in data/users.json, there is no authentication at all.
VIEW_USER / VIEW_PASSviewer / emptyOptional shared read-only account.
EDIT_PASS_FILE / VIEW_PASS_FILEnoneRead the password from this file instead (Docker secrets, systemd credentials, sops-nix, agenix). The file wins; a trailing newline is ignored; an unreadable file stops the server.
ALLOW_SELF_APPROVALfalsetrue lets an approver approve their own proposals.
HISTORY_KEEP5000How many document changes the history keeps.
READ_ONLYfalseFreezes the map for everyone (an audit, a migration).
BACKUP_KEEP / BACKUP_EVERY_HOURS14 / 24Automatic backups: how many to keep, how often.
MAX_IMPORT_MB20Largest JSON or firewall configuration file accepted.
LOGIN_MAX_FAILS10Different wrong passwords for one account before its sign-ins are refused (429) for LOGIN_LOCK_MINUTES. 0 turns it off.
LOGIN_MAX_FAILS_PER_ADDRESS20The same, for all accounts tried from one client address.
LOGIN_LOCK_MINUTES15How long a lock lasts; failures are counted over the same time.
TRUST_PROXYnoneReverse proxies whose X-Forwarded-For gives the client address: addresses, IPv4 ranges, loopback or private. See HTTPS and network.
CONNECTORStruefalse turns the connectors off entirely, modules included.
MODULE_REGISTRYSolid Geeks storeIndex of the module store (read only when an admin opens it). off: no store.
MODULE_UPLOADfalsetrue lets admins install a module package from a file.
MODULES_DIRnoneA folder of modules you provide yourself, one sub-folder each.
CONNECTOR_TIMEOUT_MINUTES30Longest a connector run may take before it is stopped.
SOURCE_URLthis repositoryThe Source code link in the app. Point it at your fork if you modify Strata (AGPL, section 13).
PORT / HOST8080 / 0.0.0.0Where the server listens.
DATA_DIR/data in DockerData and backups folder.
SITE_ADDRESSnoneDNS name used by the bundled HTTPS proxy.

HTTPS and network

Passwords travel as HTTP Basic credentials. Never expose Strata over plain HTTP beyond the machine it runs on.

Bundled proxy (Caddy)

  1. In .env, set SITE_ADDRESS=strata.example.lan, the name your users will type.
  2. Point that name at the host (internal DNS or hosts file).
  3. Start with the HTTPS profile:
docker compose --profile https up -d

Add TRUST_PROXY=private to .env, so that failed sign-ins are counted per user rather than for Caddy as a whole.

The Caddyfile uses tls internal: Caddy signs the certificate with its own local authority, which suits an internal network or a VPN. Install that authority on your workstations to remove the browser warning:

docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt ./caddy-root.crt

For a public name reachable on ports 80 and 443, delete the tls internal line and Caddy gets a Let’s Encrypt certificate itself.

Your own reverse proxy

Proxy to http://127.0.0.1:8080. Live updates use Server-Sent Events on /api/events, so turn buffering off on that path. With nginx:

location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /api/events {
    proxy_pass http://127.0.0.1:8080;
    proxy_buffering off;
    proxy_read_timeout 1h;
}

Set TRUST_PROXY=loopback so that Strata reads the client address from X-Forwarded-For. Only trust a proxy that sets this header itself.

Strata also works under a sub-path (https://intranet/strata/): it only uses relative URLs.

Without Docker (systemd)

Node.js 20 or later. The server has no npm dependency at run time; npm run setup only copies three.js and the fonts into public/vendor.

sudo mkdir -p /opt/strata && sudo cp -r strata/* /opt/strata/
cd /opt/strata
sudo npm run setup
sudo useradd --system --home /opt/strata --shell /usr/sbin/nologin strata
sudo mkdir -p /var/lib/strata && sudo chown strata:strata /var/lib/strata
sudo cp .env.example /etc/strata.env && sudo chmod 600 /etc/strata.env
sudo cp deploy/strata.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now strata

The service listens on 127.0.0.1:8080: put an HTTPS reverse proxy in front. For a quick try on a workstation: npm run setup && EDIT_PASS=test npm start.

Building from source elsewhere: npm ci (the lockfile pins three.js 0.128 and the fonts), npm run vendor (copies them into public/vendor), npm test, then ship package.json, server/, public/ and scripts/users.mjs and run node server/server.js. Nothing is downloaded at run time, and nothing is written outside DATA_DIR; data files are readable by the server’s own user only.

Nix and NixOS

The repository is a flake. To try Strata (data in ./data, listening on 127.0.0.1:8080):

EDIT_PASS=test nix run github:SolidGeeks/strata

On NixOS, the module runs it as a hardened systemd service: its own strata user, data in /var/lib/strata, read-only system, no capabilities, restricted system calls.

{
  inputs.strata.url = "github:SolidGeeks/strata";

  outputs = { nixpkgs, strata, ... }: {
    nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
      modules = [
        strata.nixosModules.default
        {
          services.strata = {
            enable = true;
            settings = {               # the variables of Configuration
              EDIT_USER = "admin";
              TRUST_PROXY = "loopback";
            };
            secretFiles.EDIT_PASS = "/run/secrets/strata-admin";  # out of the Nix store
            openFirewall = false;      # keep it behind an HTTPS reverse proxy
          };
        }
      ];
    };
  };
}

Files in secretFiles are read by systemd as root and handed to the service as credentials, so they can stay root-only. environmentFile takes KEY=value lines the same way. Accounts from the command line: sudo -u strata env DATA_DIR=/var/lib/strata strata-users set alice contributor. modules = [ ./my-connector ] provides connector modules; settings.MODULE_REGISTRY = "off" keeps to them. The flake also has overlays.default, and its checks build the package (with the tests) and run a NixOS VM test of the module.

Accounts and roles

RoleCan
viewerread the map, sheets, plans and history
contributoralso prepare proposals (drafts) and submit them; import a firewall configuration file as a proposal
approveralso edit the map directly, approve or reject proposals, restore versions, run connectors
adminalso manage accounts and connector settings

Manage accounts in the app (Collaboration › Account) or on the command line; the server picks changes up within seconds. Passwords are hashed with scrypt in data/users.json.

docker compose exec strata node scripts/users.mjs list
docker compose exec strata node scripts/users.mjs set alice contributor   # asks for a password
docker compose exec strata node scripts/users.mjs role alice approver
docker compose exec strata node scripts/users.mjs disable alice

Repeated failed sign-ins are refused for a while (429), per account and per client address (see Configuration). Only different wrong passwords count, so a tab left open with an old password locks nobody out, and browsers already signed in keep working.

Single sign-on

OpenID Connect (Entra ID, Keycloak, Authentik, Google Workspace, Okta): register Strata at the provider with the redirect URI https://strata.example.org/auth/oidc/callback, then set PUBLIC_URL, SSO_ISSUER, SSO_CLIENT_ID, SSO_CLIENT_SECRET (or _FILE) and the groups that give each role (SSO_ROLE_ADMIN, SSO_ROLE_APPROVER, SSO_ROLE_CONTRIBUTOR, SSO_ROLE_VIEWER; SSO_DEFAULT_ROLE for everyone else). Opening Strata shows a sign-in page; the ID token’s signature, issuer, audience, expiry and nonce are checked, with PKCE.

A sign-in proxy (Authelia, oauth2-proxy, Pomerium): SSO_HEADER_USER and SSO_HEADER_GROUPS name the headers it sets; they are believed only from the addresses in SSO_HEADER_TRUST (exact addresses, no range).

A name the provider gives that is also a local account is refused, and an e-mail address is used only once verified: prefer a name people cannot change (SSO_USER_CLAIM=oid on Entra ID). Sessions last SESSION_HOURS (8); Account › End all single sign-on sessions signs everyone out.

Local accounts stay, for tests, small installations and as a way in when the provider is down: LOCAL_LOGIN=admins keeps them for administrators, off turns them off. API tokens for scripts: Account › API tokens (admin), shown once, sent as Authorization: Bearer stk_….

Building the map

Sites, zones, VLANs

An information system (IS) is a site. Zones, contacts sets its zones (one level each, for example Industrial, Office, Core IT, Exposed), its VLANs with their ranges, and the IS contacts shown on assets that have none of their own. Arrange plates lets you drag a plate anywhere on its level; Line up puts a level back in a row.

Assets

Each asset has a type (server, firewall, router, switch, workstation, PLC…), a VLAN, an IP, a system, applications with their listening ports, vulnerabilities, a criticality, incident contacts and notes. A new asset starts on the first free IP of its VLAN range; an IP outside the range is pointed out. A vulnerability has a CVSS score or a severity, and optionally an EPSS (0 to 1, or a percentage) and a Known exploited (CISA KEV) mark. Search by name or IP to fly to it.

Flows

A flow goes from a source to a destination on a port: assets, the Internet, or a named Internet service. Strata suggests the devices it crosses (edge firewall for Internet traffic, core firewall or router between VLANs); untick Suggested to set the path yourself. Exposure follows from the flows: a port is Internet, inter-VLAN or intra-VLAN depending on where its traffic comes from.

Every flow crossing a firewall, router, switch or gateway has its own lane on that device, in 3D and in 2D. Flows › Colour by colours them by risk, by exposure, or Per flow: flows that cross the same device then never share a colour (up to 10). On a device sheet, Flows crossing this equipment lists them in lane order; hovering one picks it out on the model.

Moving around the 3D view

Drag with the left button to orbit, with the middle (or right) button to move the view left, right, up and down, and scroll to zoom. On a touch screen, one finger orbits, two fingers zoom and move. The view stops at the edge of the model; Reset view centres it again.

2D diagram

The 2D diagram view is the same model, flat: drag assets and plates, draw a flow from one asset to another (F), add an asset on a plate (A), move (V). Positions are shared with the 3D model.

Theme

Display › Theme: Auto follows the system, Day and Night force it. N switches.

Views and business services

The switch at the top of an IS gives three points of view:

A business service is an activity of the organisation: online sales, payroll, production line 1. Add one in the Business view (+ Business service) with a criticality, an owner and only the assets it runs on; an asset form also ticks the services it runs. Strata works out the rest from the flows: the assets they call, and so on, the equipment these flows cross and the equipment in front of the service, and the Internet services they use. Log and monitoring flows (syslog, SNMP traps, NetFlow) are left out. Show on the model picks a service out in 3D or 2D.

The asset sheet lists the business services impacted by an asset, and why: it runs on it, it relies on it through a chain of flows (web-01 → app-erp → db-erp), or its flows cross that equipment.

Risk priorities

Analysis › Risk priorities gives one score per asset, from 0 to 100, and the vulnerabilities to fix in order. The score multiplies three factors, each between 0 and 1:

FactorHow it is computed
VulnerabilitiesFor each one, its severity (CVSS / 10) times its likelihood of exploitation: 1 when it is on the CISA list of known exploited vulnerabilities, otherwise 0.15 + 0.85 × √EPSS, otherwise 0.3. Combined as the chance that at least one gets exploited.
ContextThe average of the exposure (Internet 1, inter-VLAN 0.7, intra-VLAN 0.45, none 0.3) and the reachability from the Internet along the flows (1 for one hop, 0.1 less for each extra hop, at least 0.5; 0.3 when not reachable).
CriticalityThe higher of the asset’s own and that of the business services it would hit: low 0.5, medium 0.75, high 1.

Critical from 70, high from 40, medium from 20. The fixes are ordered by the score each vulnerability would give its asset on its own. Remediation list (CSV) exports the whole list. On the asset sheet, Risk score › Why? shows each factor.

Strata does not download the EPSS and KEV feeds: it works offline. They come from the Cyberwatch connector, an import, or the asset form.

Attack paths and blast radius

Analysis › Attack paths lists the critical assets (criticality High, of their own or through a business service) that an attacker coming from the Internet could reach along the allowed flows, with every path to each one, most likely first. Where to cut names the assets and the equipment that every path to a critical asset goes through: harden those, or tighten their rules. The rest of the model fades; hovering a path picks it out, in 3D and 2D.

Blast radius starts from any asset instead (pick it in the panel, or Blast radius on its sheet): everything an attacker holding it could reach, the most likely way to each asset, and the business services hit.

StepLikelihood of taking the next asset over
VulnerableIts vulnerabilities, combined as for the risk score (CVSS times likelihood of exploitation).
Remote administration port50%: SSH, Telnet, RPC, SMB, RDP, VNC, WinRM, for credentials picked up on the way.
Any other service10%, for what the last scan did not see.

A step counts for the highest of these, and a path for the product of its steps. Flows run one way: answering a flow does not let the destination reach back. Both lists export as CSV.

Proposals and history

A contributor clicks Propose a change, then edits as usual: everything goes into a draft (blue Draft bar) and the live map does not move. Submit for review sends it to the approvers. A proposal can also start from a JSON file: a whole-map export, or one IS exported with Export this IS (JSON).

The approver opens it from Proposals: the map shows the changes in place (green added, amber modified, dashed red for removed flows in 2D) with the full list. They comment, then Approve and publish or Reject. Nobody approves their own proposal unless ALLOW_SELF_APPROVAL=true. If the same items changed on the map meanwhile, the approver is warned and decides.

Every change is recorded (who, when, what) in data/history.jsonl. History › View shows the map as it was, read-only; Restore brings the whole map back to that version, and the restore is itself a version you can undo.

Internet services

Instead of a single “Internet” cloud, describe the destinations you allow: ESET update servers, Ubuntu mirrors, Windows Update, NTP, your SaaS. Each service has hosts (FQDN or IP) and ports, and is either shared by every IS or specific to one. Flows can start or end at a service; services appear next to the cloud in 3D, at the top of the 2D diagram and in plans, and the firewall rules export uses their hosts as destinations.

Connectors

In Collaboration › Connectors. An admin fills in each connector’s settings; Test the connection checks access; Sync now (approver) runs it in the background; Sync automatically every … hours schedules it. Connectors never write the map: each run prepares one proposal per IS, refreshed in place while it is still open. When the map already matches, no proposal is created.

Credentials are stored in data/connectors.json (mode 600), never sent back to browsers. For internal certificates, paste your CA in More settings, or tick Accept any TLS certificate on a trusted network. Assets remember their record in each tool (ext), so later syncs still match after a rename.

Cyberwatch

Reads the server list and each server’s CVEs (API v3). Matches assets by host name, then IP. Lowest severity and maximum per asset are configurable; CVEs ignored in Cyberwatch are left out by default. Cyberwatch owns the CVE-… entries of the assets it scans; findings typed by hand are kept. EPSS and the CISA KEV mark are read when Cyberwatch gives them. Create a read-only API key in Profile › API keys; Basic authentication is the default, the older HMAC signature is available for older instances.

GLPI

Reads computers, network devices, printers and phones through apirest.php (GLPI 9.5 and 10): IP addresses from network ports, operating system, installed software, technician and group in charge, location. The location picks the IS, with rules such as Lyon = lyon or /^DC-?West/ = dcw, otherwise the IS whose name appears in it. Existing assets get their system, applications and contacts (entries typed by hand stay); missing assets are created on the VLAN whose range holds their IP, or on a “To sort” plate. Enable the REST API, create an API client (App-Token) and a read-only service account with a user token.

phpIPAM

Reads sections, subnets, VLANs and addresses (/api/{app}/, phpIPAM 1.4+). A subnet already used as a VLAN range goes to that IS; otherwise rules on the section, location or description decide. Creates or completes VLANs (new ones arrive in an Imported zone), fills asset IPs, moves an asset to its subnet’s VLAN, and lists differing IPs without changing them. Create an API application with Read rights and SSL with App code token security.

Palo Alto

Two sources: a configuration file exported from the firewall or Panorama (Import a configuration file, open to contributors), or the XML API with a read-only key. Active allow rules become flows of the chosen IS:

What cannot be drawn (internal any→any, dynamic groups, negations, addresses with no asset) is listed in the proposal. Flows from a rule that disappeared are removed; flows drawn by hand are never touched. A proposal from a file counts as the importer’s own for the four-eyes rule.

Modules and the module store

Each connector is a module: a folder with a manifest (its settings, whether it imports files, what it may reach) and its code. The four above ship with Strata; three more come from the store:

The first two are file imports open to any contributor; NetBox is run by approvers.

  1. Install: Connectors › Module store (admin). Each card says what the module reaches (reads the map, connects to your tool over the network), its version, licence and source. Install downloads the package and refuses it if it differs from the checksum the store publishes.
  2. Set it up: Settings on its card under Installed, then Test the connection. Every module also has Enabled with Sync automatically every … hours (both needed for scheduled syncs), the TLS options and Limit to these IS under More settings.
  3. Run it: Sync now, on a schedule, or its import button with a file. The card then shows the summary, a button to each proposal it opened (refreshed or unchanged when it updated an open one), and Details on what it skipped; Recent runs lists the last runs of all modules.
  4. Update or remove: Update to x.y.z appears in the store when there is a new version (settings are kept); Remove on the card deletes the module and its settings, not the proposals it made.

Modules of your own go in a folder set in MODULES_DIR: with Docker, put them in ./modules, uncomment the ./modules:/modules:ro volume in docker-compose.yml and set MODULES_DIR=/modules; on NixOS, services.strata.modules. Restart Strata after adding one; a module that cannot load is listed at the bottom of Connectors with the reason. MODULE_UPLOAD=true lets admins install a package file, MODULE_REGISTRY points to another store (or off).

Every test or run starts the module in its own process: it reads its own folder only, not the data files, the accounts or the other connectors’ settings; it cannot write files or start programs; it sees none of the server’s variables; it is stopped after CONNECTOR_TIMEOUT_MINUTES. It can use the network, so install only modules you trust.

Writing a module: MODULES.md walks through a first module step by step (a JSON import, tried in a few minutes on a local Strata), then the manifest, the code, the SDK, testing, and publishing it in the store.

Try them without your tools

Load the demo, then start the simulator (fake Cyberwatch, GLPI, phpIPAM and PAN-OS on port 8099, with data matching the Lyon site) and use the addresses and credentials listed at the top of the script:

docker compose exec -d strata node scripts/connector-sim.mjs

Plans and exports

Backups

To undo a mistake, use History › Restore. Backups are for losing the server. Strata copies db.json into /data/backups/ at start, every BACKUP_EVERY_HOURS, before each import and before each restore, and keeps the latest BACKUP_KEEP.

./scripts/backup.sh                                          # backups/strata-YYYYMMDD-HHMMSS.tar.gz: map, history, proposals, accounts, connectors
./scripts/restore.sh backups/strata-20261024-120000.tar.gz   # everything back
./scripts/restore.sh export.json                             # the map only

The archive contains the connector credentials: store it like a secret. Include it, or the strata-data volume, in your usual backup.

Upgrading

./scripts/backup.sh
git pull                      # or replace the files, keeping .env
docker compose up -d --build  # or: docker compose pull && docker compose up -d

Release candidates (1.0.0-rc.1…) are published under their own version only, never as latest: to try one, back up, set image: ghcr.io/solidgeeks/strata:1.0.0-rc.1 in docker-compose.yml, then docker compose pull && docker compose up -d.

API

The full reference, and the compatibility policy, are in the API reference. Every route uses the same credentials as the interface, with the rights of the account’s role. Writes need the headers Content-Type: application/json and X-Requested-With: strata. Group several writes into one history entry with X-Batch and X-Batch-Label. For a tool that should propose rather than write, give it a contributor account and use POST /api/proposals.

RouteDoes
GET /api/stateThe whole map; ?at=REV as it was after a version
GET /api/exportThe same, as a file
PUT/DELETE /api/si/{id}An IS: zones, VLANs, flows, contacts, services
PUT/DELETE /api/assets/{id}An asset
PUT /api/wan, PUT /api/servicesInter-IS links; shared Internet services
POST /api/importReplace the whole map
GET /api/history, POST /api/rollbackHistory; restore a version
/api/proposals…Create, edit, submit, comment, approve, reject
GET /api/connectors, PUT /api/connectors/{id}Connector settings (secrets masked; writing is admin only)
POST /api/connectors/{id}/test, /runTest, or run ({"wait": true} waits for the result; {"file": {"name", "text"}, "overrides": {…}} imports a file)
GET /api/modules, POST /api/modules/{id}/install, DELETE /api/modules/{id}The module store (admin)
GET /api/users, PUT/DELETE /api/users/{name}Accounts (admin)
GET /api/eventsServer-Sent Events: change, proposals, connectors
GET /healthzHealth check, no authentication
curl -u admin:PASSWORD -H 'Content-Type: application/json' -H 'X-Requested-With: strata' \
  -X PUT https://strata.example.lan/api/assets/srv-erp-01 \
  -d '{"site":"lyon","name":"srv-erp-01","type":"server","vlan":"v-lyo-20","ip":"10.20.0.20",
       "os":"RHEL 8","role":"ERP","crit":3,
       "vulns":[{"id":"CVE-2021-44228","title":"Log4Shell","cvss":10,"sev":"crit"}]}'

The quickest way to learn the exact structure is an Export JSON of the demo.

Security and licence

This map describes your weak points precisely: treat it as sensitive data.

Strata is free software under the GNU AGPL v3. You may use, modify and share it. If you let people use a modified version over a network, offer them its source: set SOURCE_URL to your fork.

Troubleshooting

SymptomWhat to check
Blank page, no 3Dpublic/vendor/ is missing: rebuild with docker compose up -d --build (with npm registry access) or run npm run setup.
Nothing in 3D, the rest worksThe browser lacks WebGL 2 or JavaScript modules: use Chrome or Edge 80+, Firefox 114+ or Safari 15+, with hardware acceleration on.
“This browser only” badgeThe page cannot reach the API: it was opened as a local file, or the proxy blocks /api/.
Others see changes only after reloadingThe proxy buffers /api/events: see your own reverse proxy.
“You have read-only access”You are signed in as a viewer, or READ_ONLY=true.
Switching accountsBrowsers keep Basic credentials: use a private window, or close the browser.
Permission error on /dataKeep the named volume. A host folder must belong to UID 1000: chown -R 1000:1000 ./data.
A connector cannot connectTest the connection names the cause: DNS, refused port, untrusted certificate, refused credentials.

Known limits