Skip to content

Commit 734e723

Browse files
SecAI-Hubclaude
andcommitted
Epic 6: Trust and audit UX — degraded banner, why-safe page, sidebar trust
UI visibility improvements: - Degraded-mode banner: red alert bar at top of ALL pages when appliance state is not "trusted". Polls /api/observability/appliance-state every 30s. - Sidebar trust indicator: third status dot showing network/privacy state (green "Offline Private" / yellow "Tor Only" / "Filtered" / red "Degraded") - Profile badge already added in Epic 4 New "Why is this safe?" page: - /why-safe route with plain-language explanation of security layers - Sections: data stays local, what's running, what happens if something goes wrong, no telemetry, how to verify - Dynamically shows active profile and network state - Link from security.html: "Not sure what all this means?" New documentation: - docs/why-is-this-safe.md — non-UI version of the same content - docs/telemetry-policy.md — explicit no-telemetry statement with verification commands (Epic 8 partial) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7fbc024 commit 734e723

6 files changed

Lines changed: 254 additions & 0 deletions

File tree

docs/telemetry-policy.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Telemetry Policy
2+
3+
**SecAI OS does not collect telemetry.** No data leaves the device unless explicitly enabled by the user.
4+
5+
## What This Means
6+
7+
| Category | Behavior |
8+
|----------|----------|
9+
| Usage analytics | Not collected |
10+
| Crash reports | Not sent externally |
11+
| Phone-home / heartbeat | None |
12+
| Update checks | Pulled by the device on a schedule, not pushed from a server |
13+
| DNS | Blocked by default (nftables). DNS leak detection runs locally. |
14+
| Model data | Never transmitted. Inference runs entirely on local hardware. |
15+
| Prompts and responses | Never logged to external services. Local audit logging optional. |
16+
17+
## Network Access Requires Explicit Consent
18+
19+
Network access is controlled by the active [profile](../files/system/etc/secure-ai/config/appliance.yaml):
20+
21+
- **offline_private** (default): All egress blocked. No exceptions.
22+
- **research**: Tor-routed web search only, through the search mediator with PII stripping and differential privacy (decoy queries).
23+
- **full_lab**: Filtered outbound through the airlock proxy. All connections logged to the local audit chain.
24+
25+
Switching profiles requires explicit user confirmation. The UI shows the privacy implications before applying.
26+
27+
## Audit Trail
28+
29+
All network-related events are recorded in the tamper-evident audit chain:
30+
31+
- Profile changes (including who changed it and when)
32+
- Airlock connections (destination, sanitization applied, data size)
33+
- Search queries (stripped of PII, with decoy query counts)
34+
- Service state changes (which services started/stopped)
35+
36+
The audit chain is hash-linked — any tampering is detectable via `verify-release.sh` or the Security Dashboard.
37+
38+
## Verification
39+
40+
To confirm no unexpected outbound connections:
41+
42+
```bash
43+
# Check nftables rules (should show default-deny egress)
44+
sudo nft list ruleset | grep -A5 "chain output"
45+
46+
# Check for active connections
47+
ss -tunap | grep -v '127.0.0.1'
48+
49+
# DNS leak check (runs automatically on a timer)
50+
journalctl -u secure-ai-dns-leak-check --no-pager -n 5
51+
```
52+
53+
## Policy Scope
54+
55+
This policy covers the SecAI OS image and all bundled services. It does not cover:
56+
57+
- Third-party models (their behavior during inference is constrained by sandboxing, but not guaranteed)
58+
- User-installed software (if the immutable OS is modified)
59+
- Network traffic from hypervisors when running in a VM

docs/why-is-this-safe.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Why is SecAI OS Safe?
2+
3+
This page explains SecAI OS security in plain language. For the full technical model, see [threat-model.md](threat-model.md) and the [Security Dashboard](http://127.0.0.1:8480/security) in the UI.
4+
5+
## Your Data Stays on This Device
6+
7+
SecAI OS runs all AI inference locally — on your hardware, using your GPU. No prompts, responses, or model data are sent to any cloud service. The network firewall blocks all outbound connections by default.
8+
9+
## What's Running
10+
11+
SecAI OS has three operating profiles:
12+
13+
| Profile | Network | What It Means |
14+
|---------|---------|---------------|
15+
| **Maximum Privacy** (default) | Blocked | Nothing leaves your device. Not even DNS. |
16+
| **Web-Assisted Research** | Tor only | Search queries are anonymized through Tor with PII stripping. |
17+
| **Full Lab** | Filtered | Outbound traffic goes through the airlock proxy with logging. |
18+
19+
You choose your profile at first boot. You can change it later from Settings. The active profile is always visible in the UI header.
20+
21+
## What Happens If Something Goes Wrong
22+
23+
| Scenario | Automatic Response |
24+
|----------|-------------------|
25+
| Tampered model detected | Quarantined and removed from the trusted store |
26+
| Integrity check fails | System degrades to safe mode, alerts you |
27+
| Suspicious agent activity | Agent frozen, airlock disabled, vault re-locked |
28+
| Bad OS update | Greenboot auto-rolls back to last known-good state |
29+
| Worst case | 3-level emergency panic: lock, wipe keys, or full wipe |
30+
31+
## No Telemetry
32+
33+
SecAI OS does **not** collect any telemetry:
34+
35+
- No usage analytics
36+
- No crash reports sent externally
37+
- No phone-home or heartbeat
38+
- No automatic connections to external servers
39+
40+
The only network activity is what you explicitly enable by switching to the "research" or "full lab" profile. See [telemetry-policy.md](telemetry-policy.md) for the full statement.
41+
42+
## How to Verify
43+
44+
You don't have to take our word for it:
45+
46+
- **Security Dashboard** (`http://127.0.0.1:8480/security`) shows real-time verification: Secure Boot status, TPM2 sealing, audit chain integrity, model provenance, SLO compliance.
47+
- **Audit logs** are hash-chained — any tampering breaks the chain visibly.
48+
- **OS image** is cosign-signed with SLSA3 provenance attestation. Verify with:
49+
```bash
50+
cosign verify --key cosign.pub ghcr.io/secai-hub/secai_os:latest
51+
```
52+
- **Every model** passes 7 automated stages (source policy, format gate, integrity, provenance, static scan, behavioral test, diffusion scan) before it can be used.
53+
- **Forensic export** bundles all verification evidence into a signed archive you can review offline.
54+
55+
## Further Reading
56+
57+
- [Threat Model](threat-model.md) — formal threat classes, invariants, residual risks
58+
- [Security Status](security-status.md) — implementation status of all 54 milestones
59+
- [Telemetry Policy](telemetry-policy.md) — no-telemetry guarantee
60+
- [Audit Quick Path](audit-quick-path.md) — step-by-step verification for auditors

services/ui/ui/app.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,11 @@ def security_page():
592592
return render_template("security.html", active_page="security")
593593

594594

595+
@app.route("/why-safe")
596+
def why_safe_page():
597+
return render_template("why-safe.html", active_page="security")
598+
599+
595600
@app.route("/updates")
596601
def updates_page():
597602
return render_template("updates.html", active_page="updates")

services/ui/ui/templates/base.html

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,10 @@
586586
<span class="status-dot gray" id="sidebar-system-dot"></span>
587587
<span id="sidebar-system-text">System</span>
588588
</div>
589+
<div class="status-indicator" id="sidebar-trust">
590+
<span class="status-dot green" id="sidebar-trust-dot"></span>
591+
<span id="sidebar-trust-text">Offline</span>
592+
</div>
589593
</div>
590594
</aside>
591595

@@ -603,6 +607,9 @@ <h1>{% block page_title %}{% endblock %}</h1>
603607
<span class="topbar-badge" id="system-badge">system</span>
604608
</div>
605609
</header>
610+
<div id="degraded-banner" style="display:none;background:var(--danger-subtle);border:1px solid var(--danger);border-radius:var(--radius);padding:0.5rem 1rem;margin:0 1rem 0.5rem;font-size:0.85rem;color:var(--danger)">
611+
<strong>System integrity check failed.</strong> <a href="/security" style="color:var(--danger);text-decoration:underline">View details</a>
612+
</div>
606613
<div class="content {% block content_class %}{% endblock %}">
607614
{% block content %}{% endblock %}
608615
</div>
@@ -775,39 +782,71 @@ <h3 id="modal-title"></h3>
775782
var resp = await fetch('/api/profile');
776783
var data = await resp.json();
777784
var badge = document.getElementById('profile-badge');
785+
var trustDot = document.getElementById('sidebar-trust-dot');
786+
var trustText = document.getElementById('sidebar-trust-text');
778787
var p = data.active || 'unknown';
779788
var locked = data.locked || false;
780789
if (p === 'offline_private') {
781790
badge.textContent = locked ? 'offline (locked)' : 'offline';
782791
badge.style.borderColor = 'var(--success)';
783792
badge.style.color = 'var(--success)';
784793
badge.title = 'Profile: Maximum isolation — no network';
794+
trustDot.className = 'status-dot green';
795+
trustText.textContent = 'Offline Private';
785796
} else if (p === 'research') {
786797
badge.textContent = locked ? 'research (locked)' : 'tor only';
787798
badge.style.borderColor = 'var(--warning)';
788799
badge.style.color = 'var(--warning)';
789800
badge.title = 'Profile: Privacy-preserving research — Tor-routed';
801+
trustDot.className = 'status-dot yellow';
802+
trustText.textContent = 'Tor Only';
790803
} else if (p === 'full_lab') {
791804
badge.textContent = locked ? 'full lab (locked)' : 'filtered';
792805
badge.style.borderColor = 'var(--accent)';
793806
badge.style.color = 'var(--accent)';
794807
badge.title = 'Profile: Full capability lab — filtered network';
808+
trustDot.className = 'status-dot yellow';
809+
trustText.textContent = 'Filtered';
795810
} else {
796811
badge.textContent = 'unknown';
797812
badge.style.borderColor = 'var(--text-muted)';
798813
badge.style.color = 'var(--text-muted)';
814+
trustDot.className = 'status-dot gray';
815+
trustText.textContent = 'Unknown';
799816
}
800817
} catch(e) {
801818
/* Profile API not available — non-critical */
802819
}
803820
}
804821

822+
async function pollApplianceState() {
823+
try {
824+
var resp = await fetch('/api/observability/appliance-state');
825+
var data = await resp.json();
826+
var banner = document.getElementById('degraded-banner');
827+
var trustDot = document.getElementById('sidebar-trust-dot');
828+
var trustText = document.getElementById('sidebar-trust-text');
829+
if (data.state && data.state !== 'trusted') {
830+
banner.style.display = 'block';
831+
trustDot.className = 'status-dot red';
832+
trustText.textContent = 'Degraded';
833+
} else {
834+
banner.style.display = 'none';
835+
}
836+
} catch(e) {
837+
/* Observability API not available — hide banner */
838+
document.getElementById('degraded-banner').style.display = 'none';
839+
}
840+
}
841+
805842
pollVaultStatus();
806843
pollSystemStatus();
807844
pollProfileStatus();
845+
pollApplianceState();
808846
setInterval(pollVaultStatus, 10000);
809847
setInterval(pollSystemStatus, 15000);
810848
setInterval(pollProfileStatus, 30000);
849+
setInterval(pollApplianceState, 30000);
811850

812851
/* Close sidebar on mobile when clicking main area */
813852
document.querySelector('.main').addEventListener('click', function() {

services/ui/ui/templates/security.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
{% block content_class %}content-wide{% endblock %}
55

66
{% block content %}
7+
<p style="margin-bottom:0.75rem"><a href="/why-safe" style="color:var(--accent);font-size:0.9rem">Not sure what all this means? Read "Why is this safe?"</a></p>
8+
79
<!-- Appliance Health Banner -->
810
<div class="card" id="appliance-banner" style="margin-bottom:1rem;border-left:4px solid var(--text-muted)">
911
<div class="card-header">
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
{% extends "base.html" %}
2+
{% block title %}Why is this safe? — SecAI OS{% endblock %}
3+
{% block page_title %}Why is this safe?{% endblock %}
4+
5+
{% block content %}
6+
<div style="max-width:720px">
7+
8+
<div style="background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1.25rem;margin-bottom:1rem">
9+
<h3 style="margin-bottom:0.5rem;color:var(--success)">Your data stays on this device</h3>
10+
<p style="color:var(--text-secondary);line-height:1.6">
11+
SecAI OS runs all AI inference locally — on your hardware, using your GPU.
12+
No prompts, responses, or model data are sent to any cloud service.
13+
The network firewall blocks all outbound connections by default.
14+
</p>
15+
</div>
16+
17+
<div style="background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1.25rem;margin-bottom:1rem">
18+
<h3 style="margin-bottom:0.5rem">What's running right now</h3>
19+
<p style="color:var(--text-secondary);line-height:1.6;margin-bottom:0.75rem">
20+
Your appliance is running in the <strong id="ws-profile">offline_private</strong> profile.
21+
</p>
22+
<ul style="color:var(--text-secondary);line-height:1.8;padding-left:1.25rem">
23+
<li id="ws-network">Network: <strong>blocked</strong> — no data leaves this device</li>
24+
<li>Vault: encrypted with AES-256 (LUKS2 + Argon2id)</li>
25+
<li>Models: verified through a 7-stage quarantine pipeline before use</li>
26+
<li>Services: sandboxed with seccomp-BPF, Landlock, and systemd hardening</li>
27+
<li>Audit: tamper-evident hash-chained logs record all security events</li>
28+
</ul>
29+
</div>
30+
31+
<div style="background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1.25rem;margin-bottom:1rem">
32+
<h3 style="margin-bottom:0.5rem">What happens if something goes wrong</h3>
33+
<ul style="color:var(--text-secondary);line-height:1.8;padding-left:1.25rem">
34+
<li><strong>Tampered model detected:</strong> automatically quarantined and removed</li>
35+
<li><strong>Integrity check fails:</strong> system degrades to safe mode, alerts you</li>
36+
<li><strong>Suspicious activity:</strong> agent frozen, airlock disabled, vault re-locked automatically</li>
37+
<li><strong>Bad update:</strong> Greenboot rolls back to the last known-good state</li>
38+
<li><strong>Worst case:</strong> 3-level emergency panic — lock, wipe keys, or full wipe</li>
39+
</ul>
40+
</div>
41+
42+
<div style="background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1.25rem;margin-bottom:1rem">
43+
<h3 style="margin-bottom:0.5rem">No telemetry</h3>
44+
<p style="color:var(--text-secondary);line-height:1.6">
45+
SecAI OS does not collect any telemetry. No usage analytics, no crash reports,
46+
no phone-home, no heartbeat. The only network activity is what you explicitly
47+
enable by switching to the "research" or "full lab" profile — and even then,
48+
all traffic is Tor-routed with PII stripping.
49+
</p>
50+
</div>
51+
52+
<div style="background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1.25rem;margin-bottom:1rem">
53+
<h3 style="margin-bottom:0.5rem">How to verify</h3>
54+
<p style="color:var(--text-secondary);line-height:1.6;margin-bottom:0.5rem">
55+
You don't have to take our word for it:
56+
</p>
57+
<ul style="color:var(--text-secondary);line-height:1.8;padding-left:1.25rem">
58+
<li>The <a href="/security" style="color:var(--accent)">Security dashboard</a> shows real-time verification status</li>
59+
<li>Audit logs are hash-chained — any tampering breaks the chain visibly</li>
60+
<li>The OS image is cosign-signed with SLSA3 provenance attestation</li>
61+
<li>Every model passes 7 stages of automated verification before use</li>
62+
<li><a href="/security" style="color:var(--accent)">Export a forensic bundle</a> for independent verification</li>
63+
</ul>
64+
</div>
65+
66+
<p style="color:var(--text-muted);font-size:0.85rem;text-align:center;margin-top:1rem">
67+
For the full technical security model, see the <a href="/security" style="color:var(--accent)">Security dashboard</a>.
68+
</p>
69+
</div>
70+
71+
<script nonce="{{ csp_nonce }}">
72+
(async function() {
73+
try {
74+
var resp = await fetch('/api/profile');
75+
var data = await resp.json();
76+
var p = data.active || 'offline_private';
77+
document.getElementById('ws-profile').textContent = p.replace(/_/g, ' ');
78+
var netEl = document.getElementById('ws-network');
79+
if (p === 'offline_private') {
80+
netEl.innerHTML = 'Network: <strong>blocked</strong> — no data leaves this device';
81+
} else if (p === 'research') {
82+
netEl.innerHTML = 'Network: <strong>Tor only</strong> — queries are anonymized through Tor';
83+
} else if (p === 'full_lab') {
84+
netEl.innerHTML = 'Network: <strong>filtered</strong> — outbound traffic goes through the airlock';
85+
}
86+
} catch(e) { /* non-critical */ }
87+
})();
88+
</script>
89+
{% endblock %}

0 commit comments

Comments
 (0)