Skip to content

Dashboard

The dashboard is the web face of vpcatlas. It scans your AWS account (or several), keeps the result in memory, and serves it as live diagrams, an inventory, a history of the scans and a comparison between any two of them. It is read-only toward AWS: the only thing that writes anything is the optional snapshot storage you point it at.

The dashboard ships with all three distributions. The installable binary, the Python package and the Docker image all contain it, and vpcatlas serve starts it.

What it shows

  • Live diagrams. The main page draws every diagram page in your browser with the diagrams.net viewer, from the very file the download buttons give you: the Global page, one page per Region, one page per VPC. A sidebar lists the pages, grouped by level.
  • Inventory. Resources by type, the full inventory, the external accounts and the cross-account links that were found, and the collection gaps (calls that were denied or failed, so you know what the picture does not cover).
  • History. Every scan that was kept, newest first, each with counters against the one before it (resources added, removed and changed, links added and removed, links that cross accounts). Any kept snapshot can be opened as a diagram.
  • Diff between two runs. Pick two snapshots on the History page and the dashboard lists the resources added, removed and changed (with the fields that changed, before and after) and the links added and removed, cross-account ones first. It can also draw the pages of the newer snapshot with those changes marked, and offer that .drawio for download.
  • Downloads. Each diagram page as a draw.io file (.drawio) or a Lucidchart file (.lucid), all pages in one .drawio, a .zip with everything, and the inventory as JSON, Markdown or HTML.

Start it

vpcatlas serve --profile audit --regions eu-west-1,eu-central-1

Then open http://127.0.0.1:8080. The first scan runs at start-up; until it finishes the data routes answer 503 with Retry-After: 15 and the page says so.

Options of vpcatlas serve:

Option Default Meaning
--host 127.0.0.1 Address to listen on. The container image uses 0.0.0.0.
--port 8080 Port to listen on (1 to 65535).
--refresh-seconds 300 Seconds between scans; 0 scans only once. Also read from VPCATLAS_REFRESH_SECONDS.
--profile, -p credential chain AWS profile. Repeat it to scan several accounts into one graph.
--role-arn none Role to assume. Repeat it to scan several accounts.
--external-id none ExternalId the role requires. Also read from VPCATLAS_EXTERNAL_ID.
--session-duration 3600 Seconds an assumed-role session lasts (900 to 43200). Also VPCATLAS_SESSION_DURATION.
--role-session-name vpcatlas RoleSessionName used when assuming a role. Also VPCATLAS_ROLE_SESSION_NAME.
--regions, -r every enabled Region Regions to scan, comma separated.
--exclude-regions none Regions to leave out, comma separated.
--concurrency, -c 8 Regions scanned in parallel (1 to 32).
--cache none Serve this graph file (written by vpcatlas discover) instead of calling AWS.
--account-names none JSON file that maps account ids to labels.

Everything else is set with VPCATLAS_* environment variables, read once at start-up (listed below). An option on the command line wins over the matching variable. Secrets are never options: access keys come from the standard AWS credential chain (see Credentials), and the dashboard password from VPCATLAS_AUTH_PASSWORD.

vpcatlas serve has no MFA options, because a dashboard that scans on a schedule cannot ask for a code. Use a role without an MFA condition, or export short-lived credentials.

Credentials

The dashboard reads AWS the way the rest of the CLI does. Nothing is configured when the standard chain already resolves credentials:

  • environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN);
  • shared credentials and config files, including named profiles and SSO profiles (--profile, VPCATLAS_PROFILE or AWS_PROFILE);
  • an EC2 instance role, an ECS task role, a Lambda execution role;
  • an EKS pod role (IAM roles for service accounts, through web identity).

To scan another account, set --role-arn (or VPCATLAS_ROLE_ARN, comma separated for several), plus --external-id / VPCATLAS_EXTERNAL_ID when the trust policy requires one. The identity that assumes the roles is the first profile when you give one, otherwise whatever the chain resolves. One audited account per role; the graphs are merged. The trust policy and the permissions the role needs are in iam.md.

Refresh model

Variable Default Meaning
VPCATLAS_REFRESH_SECONDS 0 Seconds between scans. 0 means the first scan and manual ones only. (vpcatlas serve defaults its option to 300; the container image sets this variable to 0.)
VPCATLAS_MIN_REFRESH_SECONDS 300 Floor of the automatic interval and of manual refreshes.
VPCATLAS_REFRESH_MODE background; lazy in AWS Lambda background runs a loop in the process. lazy scans on request when the data is stale.
VPCATLAS_SCAN_ON_REQUEST true In lazy mode, whether a request may start a scan.

How it behaves:

  • In background mode the dashboard loads the stored snapshot if there is one, scans at start-up when there is none (or it is older than the interval), and then scans every interval. The interval is never shorter than VPCATLAS_MIN_REFRESH_SECONDS: a value of 60 acts as 300.
  • A scan runs at a time. The last good snapshot keeps being served while a scan runs, and also when a scan fails; the failure is shown in the status and in the log.
  • Refresh now (POST /refresh) starts a scan. It answers 202 with started or running, and 429 with a Retry-After header when the last scan began less than VPCATLAS_MIN_REFRESH_SECONDS ago (after a failed scan the wait is at most 30 seconds). In lazy mode the scan runs inline and the answer is done or, when the scan fails, 502.
  • In lazy mode there is no loop. A request that finds the snapshot older than VPCATLAS_REFRESH_SECONDS first looks for a newer one in the snapshot storage (at most every 15 seconds), and scans only if there is none and VPCATLAS_SCAN_ON_REQUEST is true. This is the mode for AWS Lambda; see deploy/lambda.md.

Scope of a scan:

Variable Default Meaning
VPCATLAS_PROFILE credential chain Profile(s), comma separated. With VPCATLAS_ROLE_ARN the first one assumes the roles.
VPCATLAS_ROLE_ARN none Role(s) to assume, comma separated.
VPCATLAS_EXTERNAL_ID none External id the roles require.
VPCATLAS_SESSION_DURATION 3600 Assumed-role session length in seconds, 900 to 43200.
VPCATLAS_ROLE_SESSION_NAME vpcatlas RoleSessionName, 2 to 64 characters.
VPCATLAS_REGIONS AWS_REGION / AWS_DEFAULT_REGION, else every enabled Region Comma separated, or all.
VPCATLAS_EXCLUDE_REGIONS none Regions to leave out.
VPCATLAS_CONCURRENCY 8 Worker threads of one scan, 1 to 32.
VPCATLAS_GRAPH_FILE none Serve a graph written by vpcatlas discover instead of calling AWS.
VPCATLAS_ACCOUNT_NAMES_FILE none JSON object that maps account ids to labels.

History and snapshot storage

Every scan produces a snapshot: the graph, the inventory and all the diagram files, frozen. The history keeps the last ones and computes what changed between neighbours.

Variable Default Meaning
VPCATLAS_SNAPSHOT_URI none Where snapshots are kept: s3://bucket/prefix or file:///absolute/path.
VPCATLAS_HISTORY_KEEP 30 Snapshots kept; after each scan the oldest ones are deleted. At least 1.
VPCATLAS_HISTORY_MIN_INTERVAL_SECONDS 0 A scan identical to the previous one and newer than this is not added to the history. A scan that differs is always kept. 0 keeps every scan.

Without VPCATLAS_SNAPSHOT_URI the history lives in memory (compressed) and is lost when the process stops. With it, a restart, a new replica or a Lambda cold start serves the stored snapshot without scanning, and the history survives.

Two forms are accepted:

  • file:///absolute/path writes under a directory. In a container, mount a writable volume there; the image's root file system is read-only in the shipped ECS and Kubernetes manifests.
  • s3://bucket/prefix writes under a bucket prefix. The identity running the dashboard needs s3:GetObject, s3:PutObject and s3:DeleteObject on arn:aws:s3:::bucket/prefix/*, and s3:ListBucket on the bucket (with the condition s3:prefix = prefix/*). Without s3:ListBucket, S3 answers AccessDenied for an object that is not there yet and the store reports a read error.

Both write the same layout under the root:

Key Content
vpcatlas-snapshot.json.gz The latest snapshot, replaced by every save. A cold start serves it.
history/<UTC time>-<snapshot id>.json.gz One object per kept snapshot. The time is ISO 8601 with hyphens instead of colons (2026-10-17T10-00-00Z), so names sort by time.
history/index.json The timeline entries, newest first. If it is missing or unreadable it is rebuilt from the objects, which loses the change counters of the entries.

One writer is assumed. Two replicas that both scan would write the same keys, so with several replicas let a single scheduled job do the scanning (set VPCATLAS_SCAN_ON_REQUEST=false, as in the Lambda setup).

Authentication

Set VPCATLAS_AUTH_PASSWORD to turn on HTTP Basic protection. The user name is VPCATLAS_AUTH_USER (default atlas). Without a password the dashboard is open.

  • Protected: every page, API route, download and POST /refresh.
  • Not protected: /healthz, so load balancers and probes keep working; the static assets under /static/; and the API documentation at /api/docs and /api/openapi.json, which describe the routes but carry no scan data.
  • The comparison is constant-time. Basic sends the password with every request, so serve the dashboard over HTTPS (a load balancer, an ingress or CloudFront in front of it).
  • Every response carries X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer and X-Frame-Options: SAMEORIGIN; everything except /static/ is sent with Cache-Control: no-store, so topology data stays out of shared caches. The inventory page is served with a strict Content-Security-Policy.

The dashboard shows your network topology. Basic protection is the minimum; prefer a private network or an authenticating proxy in front of it.

Behind a proxy: base path

To serve it below a path, set VPCATLAS_BASE_PATH (for example /atlas). A leading slash is added and a trailing one removed, so atlas, /atlas and /atlas/ are the same. Every link and URL the dashboard builds carries the prefix. The application is created with the prefix as its root path, which is what puts the prefix on the links and on the API description. Set the variable to the prefix the browser sees. It is not verified whether the proxy in front of the dashboard should strip the prefix or forward it as is: check one page and one download through the proxy. In Lambda behind an API Gateway stage, the same variable is passed to the handler as the API Gateway base path.

Health endpoint

GET /healthz answers 200 with {"status": "ok", "version": "<version>"} as soon as the process is up, even before the first scan finishes. It needs no authentication. The container health check runs python -m vpcatlas.dashboard.healthcheck, which requests http://127.0.0.1:$PORT/healthz (PORT, default 8080) and exits 0 on a 200. Note that PORT only tells the probe where to look; the port the server listens on is --port.

GET /api/status is the richer state the page polls: scan state (scanning or idle), refresh mode and intervals, seconds until the next scan, the last error, the age and summary of the snapshot, the number of history entries and the list of diagram pages.

The diagrams.net viewer

The pages draw diagrams with the diagrams.net viewer script, loaded by the browser from https://viewer.diagrams.net/js/viewer-static.min.js. That is the only external resource the pages load. If the browsers that open the dashboard cannot reach that host (an isolated network), the pages show "the diagrams.net viewer did not load" after 15 seconds; the tables, the inventory and all the downloads still work.

To self-host the script:

  1. Download viewer-static.min.js from the diagrams.net viewer (https://viewer.diagrams.net/js/viewer-static.min.js) and publish it on a static host you control: an internal web server, an S3 bucket behind CloudFront, or a path on the reverse proxy in front of the dashboard.
  2. Set VPCATLAS_VIEWER_URL to its address. It must be an https:// URL or an absolute path (starting with /); anything else is refused at start-up.

An absolute path is resolved against the dashboard's own origin, so it works when the proxy in front of the dashboard serves that path. The dashboard does not serve the file itself. Which extra resources the viewer fetches at run time has not been checked, so test the self-hosted copy in a browser with the network tab open.

HTTP routes

Paths are relative to VPCATLAS_BASE_PATH. Unless noted, every route needs the Basic credentials when VPCATLAS_AUTH_PASSWORD is set. Data routes answer 503 with Retry-After: 15 until the first scan has succeeded.

Method and path Answers
GET / The main page: diagram viewer, inventory, external accounts, collection gaps.
GET /inventory The inventory as a standalone HTML page.
GET /history The timeline and the comparison page. Query: from, to (snapshot ids). With more than one snapshot it compares the newest with the one before.
GET /diff/{older_id}/{newer_id} The viewer on the pages of newer_id, with what changed since older_id marked.
GET /healthz Liveness: {"status": "ok", "version": ...}. No authentication.
GET /api/status Scan state, freshness, history count and diagram pages.
GET /api/overview Summary detail the page loads when the snapshot changes.
GET /api/graph The network graph of the current snapshot, as JSON.
GET /api/inventory The inventory of the current snapshot, as JSON.
GET /api/diagrams Every diagram page in tab order, with the hash, size and URL of its .drawio.
GET /api/diagrams/{key} One page; the xml field is the body of its export, byte for byte.
GET /api/history Every recorded snapshot, newest first, each with counters against the one before it.
GET /api/history/{snapshot_id} One recorded snapshot and the .drawio files the history can still serve.
GET /api/diff What changed between two snapshots. Query: from, to; to defaults to the newest, from to the one before to. If from is newer than to the diff is reversed.
GET /api/docs, GET /api/openapi.json The interactive API description. No authentication.
GET /export/{name}.drawio A diagram page as draw.io. name is a page key: global, a Region, or a VPC id (vpc-...); all joins every page in tab order. The SHA-256 of the file is the ETag.
GET /export/{name}.lucid A diagram page as Lucidchart.
GET /export/all.zip One archive: every page as .drawio and .lucid, all pages in one .drawio, the inventory in JSON, Markdown and HTML, and graph.json.
GET /export/inventory.json, .md, .html The inventory in each format.
GET /export/history/{snapshot_id}/{name}.drawio The .drawio of a recorded snapshot.
GET /export/diff/{from_id}/{to_id}.json The body of /api/diff as a file.
GET /export/diff/{older_id}/{newer_id}/{key}.drawio One page of the newer snapshot drawn with the changes marked.
POST /refresh Starts a scan: 202 (started or running), 429 (throttled, with retry_after), or in lazy mode 200 (done) and 502 (failed).
GET /static/... The page's CSS and JavaScript. No authentication.

Downloaded files are named vpcatlas-<account id>-<key>.<extension>.

Logging

VPCATLAS_LOG_LEVEL sets the level of the vpcatlas loggers (DEBUG, INFO, WARNING, ERROR or CRITICAL; default INFO). Scans, failures and snapshot handling are logged to standard error.

Deploying: deploy.md lists the guides for Docker, Compose, ECS, Kubernetes and Lambda.