Skip to content

Portal

The portal is the human-facing half of Kartis: a server-rendered web surface mounted at /portal/... inside the kartis-scan binary, next to the bearer-token /v1/... API that Eon Center, Eon Aethis and Eon Insights use. It is off by default — an existing deployment that only serves machine callers keeps exactly the surface it had. Turn it on in kartis-scan.yaml (see Configuration).

Tenancy

Both surfaces resolve to the same tenant boundary before any query runs:

kartis-scan
├── /v1/*      bearer token   machine callers
└── /portal/*  cookie session humans
                    │
                    └── both resolve to the same tenant

A portal session is scoped to the same tenant as that customer’s API tokens.

Pages

PageRouteShows
Login/portal/loginpassword plus a mailed one-time code
Overview/portal/overviewthe estate as it stands after the newest completed run
Hosts/portal/hostsone row per host from its newest completed run: IP, open ports, product/version, last seen
Host detail/portal/hosts/{host}that host’s services, certificates and findings, with per-finding advice
Attack surface/portal/surfacewhat exists and answers, from the recon pass — HTTP status and title, CNAME, A/AAAA, subdomain-takeover flags
Certificates/portal/certificatesthe leaf certificate per TLS service, with expiry windows, wildcards and self-signed
Crypto/portal/cryptocrypto-relevant findings by severity, filterable down to PQC-relevant only
Runs/portal/runsrun history with status, trigger and counts
Run detail/portal/runs/{uuid}one run’s outcome
Compare runs/portal/runs/compare?run=&run=two runs side by side
Reports/portal/reportsper run: date, domain, download
Account/portal/accountusers, password change, domain verification status

Hosts and Attack surface answer different questions. Hosts shows what has been scanned; Attack surface shows what exists and answers, read from the recon pass’s liveness data. The same estate can therefore look thin on one page and hold thousands of hosts on the other.

Comparing two runs keys each kind of change on what makes it meaningful:

ChangeKeyed on
hosts appeared / gonehost name
ports opened / closedhost:port
findings new / resolvedfinding id + title, counted per severity
certificates changedhost:port

Login and sessions

ConcernBehaviour
Cookie__Host- prefixed, HttpOnly, Secure, SameSite=Lax; random, only a hash stored
Lifetimeidle timeout plus an absolute maximum
Passwordargon2id
Second factorone-time code by email, short-lived, single use — mandatory for every role
CSRFtoken verified on every non-GET request
Brute forcebackoff per account and per IP, followed by lockout
Rolesowner (scope, on-demand runs, user management), viewer (read, report download)
Auditlogin, scope change, on-demand trigger, report download, verification change

The one-time code carries a code and nothing else — no findings, no host names, no customer name in the subject, so a misdelivered message discloses only that the address has a Kartis account. If SMTP submission fails the login returns a generic error and the failure is written to the audit table; the code is never shown in the response as a fallback, under any configuration.

A login depends on mail arriving, so treat deliverability as part of the deployment: SPF authorising the sending path, DKIM signing on the sender address with DMARC aligned, and a dedicated transactional sender.

Domain verification

Per root domain, the customer publishes:

_kartis-verify.<apex>.  TXT  "kartis-verify=<token>"

The scheduler refuses to run against an unverified domain. The record is re-checked periodically: one that disappears means the customer no longer controls the domain.

Domains found by org expansion can’t be furnished with a TXT record by definition — they’re the ones the customer had forgotten. Two classes resolve that:

ClassEntryTreatment
VerifiedTXT record, or explicit in the signed scopeactive scanning: port scan, TLS handshake, WAF detection
Observedfound through the org graphpassive only: CT logs, DNS, WHOIS, GLEIF — no packet to their infrastructure

An observed domain renders with an add to scope action, which triggers a fresh verification.

Scheduled runs

kartis-scheduler is a separate binary that only needs the database — it doesn’t scan and serves no HTTP. It picks up sessions whose next slot has passed and queues a run for each.

Cadence is monthly or biweekly plus an anchor (day of month, or weekday); there is no cron expression to get wrong. The portal shows the next slot, for example next scan on 1 October.

Two rules worth knowing before you operate it:

  1. A failed scheduled run is not retried automatically. The portal shows the failure; the customer restarts it.
  2. Runs missed during downtime are caught up once, not once per missed slot.

Deployment

The portal is HTTPS-only by construction: its session cookie carries the __Host- prefix and the Secure flag, which a browser rejects over plain HTTP. Run it behind your own TLS front and set trusted_proxies to that proxy, so forwarded client addresses and the forwarded scheme are believed from it and nowhere else. Expose only /portal/ on the public vhost and keep /v1/* off it.

The insecure option drops the cookie prefix and the Secure flag. It exists for a test over plain HTTP and for nothing else.