Prometheus metrics
The controller exposes Prometheus metrics at /metrics for monitoring sessions, proxy
health, auth and integrations.
Securing the endpoint
Section titled “Securing the endpoint”/metrics is off until you set TESSERA_METRICS_TOKEN — without it the endpoint returns
404. Generate a random value and pass it as a bearer token (see
Configuration):
curl -H "Authorization: Bearer $TESSERA_METRICS_TOKEN" https://tessera.example.com/metricsScrape config:
scrape_configs: - job_name: tessera scheme: https authorization: credentials: <TESSERA_METRICS_TOKEN> static_configs: - targets: ["tessera.example.com"]Exported metrics
Section titled “Exported metrics”| Metric | Type | Labels | Meaning |
|---|---|---|---|
tessera_build_info | gauge | version | Build version (value is always 1). |
tessera_http_requests_total | counter | method, code | API requests by method and status class. |
tessera_http_request_duration_seconds | histogram | — | API request latency. |
tessera_http_requests_in_flight | gauge | — | Requests currently being served. |
tessera_sessions_started_total | counter | kind | Sessions created, by target kind. |
tessera_sessions_active | gauge | kind | Currently active proxied sessions. |
tessera_sessions_closed_total | counter | kind | Sessions ended, by target kind. |
tessera_session_duration_seconds | histogram | — | Session length. |
tessera_k8s_requests_total | counter | verb | Proxied apiserver requests by verb. |
tessera_commands_blocked_total | counter | — | Commands/queries blocked in read-only mode. |
tessera_proxy_errors_total | counter | kind | Proxy session errors by target kind. |
tessera_access_requests_total | counter | event | Just-in-time access requests by event. |
tessera_auth_logins_total | counter | method, outcome | Login attempts by method and outcome (outcome is one of success, failure, blocked, disabled). |
tessera_notifications_total | counter | channel, outcome | Notification deliveries. |
tessera_security_alerts_total | counter | action | Security alerts raised, by the audit event that raised them. |
tessera_audit_export_total | counter | outcome | Audit-export deliveries to your SIEM. |
The audit chain
Section titled “The audit chain”These seven describe the tamper-evident log itself: whether it is being sealed, whether it still adds up, and whether anything is being lost on the way in. See Audit log for what the chain does and does not cover.
| Metric | Type | Meaning |
|---|---|---|
tessera_audit_last_seal_timestamp_seconds | gauge | When a block was last sealed successfully. Set on success only, so its age is the signal. |
tessera_audit_blocks_sealed_total | counter | Blocks sealed since start. |
tessera_audit_seal_errors_total | counter | Sealing attempts that failed. |
tessera_audit_unsealed_rows | gauge | Rows written but not yet in a block. A handful at any moment is normal. |
tessera_audit_chain_problems | gauge | Problems found by the last full verification. Anything above zero is an incident. |
tessera_audit_last_verify_timestamp_seconds | gauge | When the chain was last verified in full. Survives a restart. |
tessera_audit_write_errors_total | counter | Audit entries that could not be written at all. |
tessera_audit_chain_unsigned | gauge | 1 when no signing key is configured, so the chain is hash-linked only. |
And one that is not about the audit log but belongs on the same dashboard:
| Metric | Type | Meaning |
|---|---|---|
tessera_license_expires_timestamp_seconds | gauge | When the current licence expires. |
Plus the standard Go runtime and process collectors (go_*, process_*).
Useful queries
Section titled “Useful queries”# Active sessions by kindtessera_sessions_active
# API error ratesum(rate(tessera_http_requests_total{code=~"5.."}[5m])) / sum(rate(tessera_http_requests_total[5m]))
# Blocked commands (read-only enforcement) over timerate(tessera_commands_blocked_total[5m])
# Failed logins (anything that isn't a success — failure, blocked or disabled)rate(tessera_auth_logins_total{outcome!="success"}[5m])Alerting
Section titled “Alerting”Seven rules. They are written out in full because an audit log nobody is alerted about is evidence that something happened, not a control: ISO/IEC 27001:2022 A.8.16 asks for monitoring, and monitoring means somebody is told.
These watch the machinery. The events themselves, somebody being handed administrative authority
or a run of failed sign-ins, are announced by the controller into a chat instead, because they
need a person rather than a dashboard: see Notifications.
tessera_security_alerts_total is the same set counted, which is worth graphing rather than
paging on.
groups: - name: tessera-audit rules: # The log stopped being written. Nothing else on this page matters if this fires. - alert: TesseraAuditWriteErrors expr: increase(tessera_audit_write_errors_total[15m]) > 0 labels: { severity: critical } annotations: summary: "Tessera could not write {{ $value }} audit entries"
# The chain does not add up. Either the log was altered or the thing that proves it # wasn't is broken, and both need a person. - alert: TesseraAuditChainBroken expr: tessera_audit_chain_problems > 0 for: 5m labels: { severity: critical } annotations: summary: "Tessera audit chain reports {{ $value }} problem(s)"
# Nobody has checked. A chain_problems of zero that is two days old means "not looked at", # which reads exactly like "fine" on a dashboard. - alert: TesseraAuditNotVerified expr: time() - tessera_audit_last_verify_timestamp_seconds > 172800 labels: { severity: warning } annotations: summary: "Tessera audit chain has not been verified for over 48h"
# The sealer has stopped. Rows keep being written and stop being protected, which is the # state an attacker with database access can arrange on purpose. - alert: TesseraAuditSealerStalled expr: time() - tessera_audit_last_seal_timestamp_seconds > 600 for: 5m labels: { severity: critical } annotations: summary: "No audit block sealed for over 10 minutes"
# The chain carries no signatures, so anyone who can write to the database can rewrite # the log and have the check pass. Not an incident, a configuration gap: it fires once, # stays until a key is set, and is deliberately kept out of chain_problems so that gauge # keeps meaning "the log does not add up". - alert: TesseraAuditChainUnsigned expr: tessera_audit_chain_unsigned == 1 for: 1h labels: { severity: warning } annotations: summary: "Tessera audit chain is unsigned — set TESSERA_AUDIT_SIGN_KEY"
# The SIEM is not receiving everything. Delivery is best effort by design, but the # chain.sealed announcements are the only evidence that the newest blocks were not # deleted, and a full buffer drops them like anything else. - alert: TesseraAuditExportDropping expr: increase(tessera_audit_export_total{outcome="dropped"}[15m]) > 0 labels: { severity: critical } annotations: summary: "Tessera dropped {{ $value }} audit events on the way to the SIEM"
# The licence, three weeks ahead, which is enough time to renew without a rush. - alert: TesseraLicenceExpiring expr: (tessera_license_expires_timestamp_seconds - time()) / 86400 < 21 labels: { severity: warning } annotations: summary: "Tessera licence expires in {{ $value | printf \"%.0f\" }} days"Every exported event carries a seq and a boot id. seq counts up within one controller
process, boot changes when it restarts, and the number is taken before the send, so an event
that was dropped still consumes one. A gap in seq within a single boot is the receiver’s own
proof that something did not arrive, independent of the alert above.
tessera_audit_chain_problems reports every problem found, not the number listed: the response
caps the list it returns so that a systemic break does not produce an unreadable one, and the
count is not capped with it.
tessera_audit_unsealed_rows is deliberately not an alert on its own. It rises and falls by
design as blocks close, and TesseraAuditSealerStalled already catches the state where it stops
falling. Graph it, do not page on it.
Verifying the chain
Section titled “Verifying the chain”The controller walks the whole chain once a day, at 03:17 in its local time, and records the
result in three places: the two metrics above, a chain.verified or chain.problem row in the
audit log itself, and the line beside the Verify integrity button in the console.
Change the hour with TESSERA_AUDIT_VERIFY_AT (HH:MM), or set it to off and run the pass
from your own scheduler against GET /api/audit/verify, which returns the same result as the
button and updates the same metrics. Do one or the other, not both: a full walk reads every block
and every row it covers, which is the heaviest read the controller makes. On an installation
holding a hundred million rows it takes around eleven minutes, which is why it runs at night
rather than on a timer during the day.
We would like to count visits with Google Analytics, which needs a cookie. Nothing is loaded and nothing is stored unless you accept. What this sets.