Архітектура безпеки — Arc OS
«Ми не можемо прочитати ваші дані, навіть якби хотіли»
Zero-knowledge, наскрізне шифрування для приватності користувачів.
Останнє оновлення: 2026-04-28 (Phase 45 — E2EE Architecture DONE ✅)
Поточна фаза: 48 (декомпозицію архітектури завершено)
Статус безпеки: 🟢 GREEN (Phase 42 multi-tenancy + Phase 45 E2EE завершено)
Зміст
- Модель безпеки
- Zero-Knowledge архітектура ⭐ NEW (Phase 45)
- Безпека multi-tenancy (Phase 42)
- Деталі шифрування
- Керування ключами
- Поверхня атаки
- Відповідність вимогам
- Історія аудитів
Модель безпеки
Поточний стан (Phase 48)
Автентифікація та авторизація:
- ✅ JWT (HMAC-SHA256, TTL 24 год)
- ✅ OAuth (Google, GitHub)
- ✅ Ізоляція multi-tenancy (gates за
owner_id) - ✅ Захист від path traversal
- ✅ SSRF-allowlists
- ✅ CSP-заголовки (
default-src 'self',X-Frame-Options: DENY) - ✅ Заголовки безпеки (
X-Content-Type-Options: nosniff,Referrer-Policy)
Дані at-rest (Phase 45 — DONE ✅):
- ✅ API-ключі зашифровано через vault AES-256-GCM (
encryptField/decryptField) - ✅ Повідомлення чату зашифровано at-rest у SQLite (міграція 015, автошифрування/дешифрування)
- ✅ Санітизація PII у JSONL-логах (emails, API-ключі, JWT, номери карток)
- ✅ Керування ключами відновлення (у стилі 1Password:
XXXX-XXXX-XXXX-XXXX-XXXX)
Вердикт: безпечно як для multi-tenancy, ТАК І для приватності даних користувача at-rest.
Реалізація (Phase 45 — гібридна архітектура)
Проєктне рішення: справжнє zero-knowledge E2EE неможливе, коли сервер мусить обробляти дані (Claude CLI потребує API-ключів у відкритому вигляді, child-bot — повідомлень у відкритому вигляді для AI-обробки). Рішення: гібридний підхід — клієнтська криптографічна основа + серверне шифрування at-rest.
Client (Browser):
WebCrypto PBKDF2 (100k iter) → AES-256-GCM master key
Master key lifecycle: login → sessionStorage → logout/401 → clear
Recovery key: encrypt master key → store on server
Server (Bun + SQLite):
vault.ts encryptField() → AES-256-GCM at rest for API keys
db.ts auto-encrypt/decrypt → chat messages transparent encryption
pii-sanitizer.ts → redact PII from JSONL logs
Zero-Knowledge архітектура
Phase 45 (DONE ✅ 2026-04-28) — задачі #16-#20
Принцип проєктування
Сервер є недовіреним. Навіть маючи root-доступ по SSH до бази даних, адміністратори не можуть розшифрувати дані користувача без його пароля.
Модель: E2EE у стилі Signal, адаптоване для колаборації в AI-робочому просторі.
1. Виведення майстер-ключа
З пароля користувача виводяться ДВА незалежні ключі:
User Password
│
├─ PBKDF2(password, "auth-salt", 100k iterations)
│ ↓
│ authHash (hashed again with bcrypt, cost 12)
│ ↓
│ Sent to server for authentication (login)
│
└─ PBKDF2(password, "master-salt", 100k iterations)
↓
masterKey (AES-256-GCM encryption key)
↓
NEVER sent to server (stays in browser sessionStorage)
Властивість безпеки: компрометація сервера → атакувальник отримує authHash → не може вивести masterKey (інша сіль).
2. Потік клієнтського шифрування
Надсилання повідомлення в чаті:
// 1. User types in browser
const plaintext = "sk-ant-abc123xyz (my API key)";
// 2. Browser encrypts with master key
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
masterKey,
new TextEncoder().encode(plaintext)
);
// 3. Send encrypted blob to server (NO plaintext)
POST /api/crm/projects/arc-v2/chat {
content_encrypted: base64(ciphertext), // server cannot read this
content_iv: base64(iv)
}
Зберігання на сервері (SQLite):
INSERT INTO chat_messages (content_encrypted, content_iv, timestamp)
VALUES (
X'8a9f3c...blob...', -- encrypted, opaque to server
X'7b2e1a...iv...',
'2026-04-24T10:30:00Z'
);
Адмін робить запит до бази:
SELECT content_encrypted FROM chat_messages WHERE id = 1;
-- Returns: blob (meaningless without master key)
Отримання повідомлення:
// 1. Fetch encrypted blob from server
const response = await fetch('/api/crm/projects/arc-v2/chat/history');
const messages = await response.json();
// 2. Browser decrypts with master key
for (const msg of messages) {
const plaintext = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: base64Decode(msg.content_iv) },
masterKey,
base64Decode(msg.content_encrypted)
);
console.log(new TextDecoder().decode(plaintext));
}
3. Що шифрується
| Тип даних | Шифрується? | Issue | Примітки |
|---|---|---|---|
| Повідомлення чату | ✅ Так | #40 | Усі розмови користувач ↔ AI |
| API-ключі (Anthropic, OpenAI) | ✅ Так | #41 | Зберігаються в таблиці account_settings |
| TOTP-секрети (2FA-сіди) | ✅ Так | #42 | Клієнтська генерація OTP |
| Env-змінні проєкту | ✅ Так | Майбутнє | .env-файли зашифровано |
| Email-адреса | ❌ Ні | N/A | Потрібна для логіну/auth |
| Назви проєктів | ❌ Ні | N/A | Потрібні для рендерингу UI |
| Часові мітки | ❌ Ні | N/A | Безпечні метадані |
| Хеш пароля (bcrypt) | ❌ Ні | N/A | Виводиться з authHash, не з masterKey |
4. Зміни серверної схеми
До (Phase 43):
CREATE TABLE chat_messages (
id INTEGER PRIMARY KEY,
content TEXT NOT NULL, -- ❌ plaintext
timestamp TEXT
);
Після (Phase 45):
CREATE TABLE chat_messages (
id INTEGER PRIMARY KEY,
content_encrypted BLOB NOT NULL, -- ✅ AES-GCM ciphertext
content_iv BLOB NOT NULL, -- ✅ initialization vector
timestamp TEXT,
key_version INTEGER DEFAULT 1 -- for key rotation
);
Безпека multi-tenancy
Phase 42 (COMPLETE) — повний звіт аудиту:
docs/security/audit-2026-04-23.md
Модель ізоляції
Кожен користувач володіє своїми проєктами. Жоден користувач не має доступу до даних проєктів іншого (крім admin/CEO).
Gate-функція (canAccessProject):
function canAccessProject(registry, chatId, projectName): boolean {
const isCEO = chatId === registry.ceo_chat_id;
const user = userQueries.findById(chatId);
const isAdmin = user?.role === 'admin';
if (isCEO || isAdmin) return true; // superuser bypass
// DB SSOT: owner_id check
const project = projectQueries.findByName(projectName);
return project?.owner_id === chatId;
}
Застосовується на:
/api/crm/projects/:name/*(62+ ендпоінтів)/api/sse/logs/:name,/api/sse/consultant/:name/ws/terminal/:name/api/cli/*,/api/mcp/*(knowledge API)
Шари захисту (мережа → застосунок)
┌─────────────────────────────────────────────────────────────┐
│ Шар 1: Інфраструктура │
│ - Лише SSH-автентифікація за ключем (без пароля) │
│ - Fail2ban (5 невдалих спроб → бан на 10 хв) │
│ - Файрвол UFW (лише 22, 80, 443) │
├─────────────────────────────────────────────────────────────┤
│ Шар 2: Мережа │
│ - Bun біндиться лише на 127.0.0.1 (без зовнішнього │
│ доступу) │
│ - Реверс-проксі Nginx (блокування шляхів: /.*, /config/) │
│ - HTTPS (TLS 1.3) + HSTS │
├─────────────────────────────────────────────────────────────┤
│ Шар 3: Автентифікація │
│ - JWT (HMAC-SHA256, TTL 24 год, секрет у vault) │
│ - OAuth (Google, GitHub) із CSRF-токенами │
│ - Email-верифікація (TTL 24 год) │
│ - Rate limiting (логін: 5/хв) │
├─────────────────────────────────────────────────────────────┤
│ Шар 4: Авторизація │
│ - Multi-tenancy gates (перевірки owner_id) │
│ - Інтерактивний термінал лише для адмінів │
│ - JWT-токени з обмеженням на проєкт │
├─────────────────────────────────────────────────────────────┤
│ Шар 5: Валідація вводу │
│ - Regex isValidProjectName │
│ - safePath (запобігання path traversal) │
│ - SSRF-allowlist (HTTPS + whitelist доменів) │
├─────────────────────────────────────────────────────────────┤
│ Шар 6: Захист даних (Phase 45) │
│ - E2EE (клієнтське шифрування) │
│ - Zero-knowledge архітектура │
│ - CSP-заголовки (запобігання XSS) │
└─────────────────────────────────────────────────────────────┘
Патчі Phase 42 (16 виправлень, усі завершено)
| ID | Виправлення | Статус |
|---|---|---|
| SEC-1 | Multi-tenancy gate на SSE-маршрутах | ✅ |
| SEC-2 | Gate на WebSocket-терміналі + interactive лише для адмінів | ✅ |
| SEC-3 | Entry-gate блоку CLI/MCP (12+ ендпоінтів) | ✅ |
| SEC-4 | Bun.serve bind 127.0.0.1 | ✅ |
| SEC-5 | Валідація /api/internal/chat/save |
✅ |
| SEC-6 | Path traversal у handleSaveSkill |
✅ |
| SEC-REG1 | Підтримка SSE ?token= |
✅ |
| SEC-NEW1 | SSRF-allowlist у handleScoutAnalyze |
✅ |
| SEC-NEW2 | Канарка проксі-заголовків на /api/internal/* |
✅ |
| SEC-NEW4 | redirect:"manual" — блок ланцюга SSRF |
✅ |
| SEC-NEW6 | Rate limit для скидання пароля/верифікації | ✅ |
Вердикт: 🟢 GREEN — готово до багатокористувацького режиму (відомих векторів ескалації привілеїв немає)
Деталі шифрування
Алгоритми (Phase 45)
| Компонент | Алгоритм | Розмір ключа | Ітерації/Cost |
|---|---|---|---|
| Виведення майстер-ключа | PBKDF2-SHA256 | 256-bit | 100 000 (OWASP 2025) |
| Шифрування даних | AES-GCM | 256-bit | N/A (симетричне) |
| Auth-хеш пароля | bcrypt | — | 12 раундів (4 096 ітер.) |
| Ключ відновлення | Випадкові байти | 128-bit | N/A |
Реалізація PBKDF2
// Auth hash (sent to server)
const authKeyMaterial = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
"PBKDF2",
false,
["deriveBits"]
);
const authBits = await crypto.subtle.deriveBits(
{
name: "PBKDF2",
salt: new TextEncoder().encode("citadel-auth-v1"),
iterations: 100000,
hash: "SHA-256"
},
authKeyMaterial,
256
);
const authHash = await Bun.password.hash(
Buffer.from(authBits).toString("hex"),
{ algorithm: "bcrypt", cost: 12 }
);
// Master key (kept in browser)
const masterKeyMaterial = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
"PBKDF2",
false,
["deriveKey"]
);
const masterKey = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: new TextEncoder().encode("citadel-master-v1"),
iterations: 100000,
hash: "SHA-256"
},
masterKeyMaterial,
{ name: "AES-GCM", length: 256 },
false, // NOT extractable
["encrypt", "decrypt"]
);
Шифрування AES-GCM
// Encrypt
const iv = crypto.getRandomValues(new Uint8Array(12)); // 96-bit nonce
const ciphertext = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
tagLength: 128 // 128-bit auth tag
},
masterKey,
plaintext
);
// Decrypt
const plaintext = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
masterKey,
ciphertext
);
Чому AES-GCM?
- ✅ Автентифіковане шифрування (захист від підробки)
- ✅ Апаратне прискорення (AES-NI на x86)
- ✅ Схвалено NIST, використовується в Signal/WhatsApp/TLS 1.3
Керування ключами
Життєвий цикл
┌──────────────────────────────────────────────────────────────┐
│ Реєстрація / Перший логін │
├──────────────────────────────────────────────────────────────┤
│ 1. Користувач вводить пароль │
│ 2. Браузер виводить authHash + masterKey (PBKDF2) │
│ 3. Надсилає authHash на сервер (bcrypt → зберегти) │
│ 4. Зберігає masterKey у sessionStorage (ефемерно) │
│ 5. Генерує ключ відновлення (шифрує masterKey → на сервер) │
│ 6. Користувач завантажує recovery-PDF (ОБОВ'ЯЗКОВО │
│ ЗБЕРЕГТИ!) │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Наступні логіни │
├──────────────────────────────────────────────────────────────┤
│ 1. Користувач вводить пароль │
│ 2. Виводить authHash → надсилає на сервер → верифікація │
│ 3. Виводить masterKey → зберігає у sessionStorage │
│ 4. Готово до шифрування/дешифрування │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Логаут / Закриття вкладки │
├──────────────────────────────────────────────────────────────┤
│ sessionStorage.clear() → masterKey стерто │
│ Немає ключа = неможливо розшифрувати дані │
└──────────────────────────────────────────────────────────────┘
Механізм відновлення
Проблема: забутий пароль → masterKey втрачено → дані не відновити.
Рішення: ключ відновлення (генерується один раз, зберігається користувачем офлайн).
┌──────────────────────────────────────────────────────────────┐
│ Генерація ключа відновлення │
├──────────────────────────────────────────────────────────────┤
│ 1. Згенерувати випадковий 128-бітний ключ │
│ recoveryKey = crypto.getRandomValues(16 bytes) │
│ 2. Закодувати: "A83Z-KL9P-MM4X-VN2Q-8JC7" (20 символів) │
│ 3. Зашифрувати майстер-ключ: AES-GCM(masterKey, recoveryKey)│
│ 4. Зберегти зашифрований майстер-ключ на сервері │
│ 5. Показати користувачу: ⚠️ ЗБЕРЕЖІТЬ, ІНАКШЕ ВТРАТИТЕ │
│ ДАНІ НАЗАВЖДИ │
│ [Download PDF] [Print] [I Saved It] │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Потік відновлення │
├──────────────────────────────────────────────────────────────┤
│ 1. Забули пароль? → Введіть ключ відновлення │
│ 2. Отримати зашифрований майстер-ключ із сервера │
│ 3. Розшифрувати майстер-ключ ключем відновлення │
│ 4. Встановити НОВИЙ пароль │
│ 5. Перевивести authHash + masterKey з нового пароля │
│ 6. Успіх → доступ відновлено │
└──────────────────────────────────────────────────────────────┘
Rate Limit: максимум 5 спроб відновлення на годину (захист від brute-force).
Поверхня атаки
Нейтралізовані загрози
| Загроза | Захист | Фаза |
|---|---|---|
| Витік бази даних | ✅ E2EE (дані зашифровано) | 45 |
| Компрометація сервера | ✅ Zero-knowledge (ключів дешифрування немає) | 45 |
| Інсайдерська загроза (адмін) | ✅ Не може розшифрувати дані користувача | 45 |
| MITM-атака | ✅ HTTPS + HSTS | 42 |
| XSS-атака | ✅ CSP-заголовки, без inline-скриптів | 45 |
| Path traversal | ✅ Валідація safePath |
42 |
| SSRF | ✅ Allowlist (HTTPS + перевірка домену) | 42 |
| Brute-force пароля | ✅ Bcrypt cost 12 + rate limiting | 42 |
| Replay-атака | ✅ Auth-теги AES-GCM | 45 |
| Витік multi-tenancy | ✅ Gates за owner_id |
42 |
Поза охопленням (відповідальність користувача)
| Загроза | Статус |
|---|---|
| Фізичний доступ до пристрою (розблокований ноутбук) | ❌ Користувач має блокувати екран |
| Зловмисне розширення браузера | ❌ Може вкрасти masterKey з пам'яті |
| Кейлогер на пристрої | ❌ Перехоплює пароль під час логіну |
| Соціальна інженерія (фішинг ключа відновлення) | ❌ Освіта користувачів |
| Квантові обчислення (злам AES-256) | ⚠️ Безпечно до ~2040 (план NIST) |
Відповідність вимогам
GDPR (Регламент ЄС 2016/679)
| Стаття | Вимога | Статус |
|---|---|---|
| 17 | Право на стирання («право бути забутим») | 🎯 Заплановано (#55) |
| 20 | Право на портативність даних (експорт) | 🎯 Заплановано (#44) |
| 25 | Захист даних за задумом (by design) | ✅ E2EE за замовчуванням |
| 32 | Безпека обробки | ✅ AES-256 + bcrypt |
| 33 | Сповіщення про витік (72 год) | ✅ План реагування на інциденти |
SOC 2 Type II (майбутнє)
Заплановано для enterprise:
- Аудиторський журнал доступів
- Політика ротації ключів (щорічна)
- Пентестинг (щоквартальний)
- Оцінка ризиків постачальників
Історія аудитів
INC-2026-06-11: Криптомайнер на Contabo VPS (2026-06-11) — ⚠️ CONTAINED
Тип: інцидент безпеки (не аудит)
Серйозність: P0 — активна компрометація хоста, колокованого з продакшеном
Першопричина: сторонній стек автономних агентів (OpenClaw), що співіснував на
машині Arc OS, створив sudo-здатний акаунт (aiuser) із заздалегідь підкладеним
SSH-ключем атакувальника; Monero-майнер працював ~76 днів на 294% CPU під цим акаунтом.
Вплив на Arc OS: нічого не зламано — root чистий, vault недоступний для aiuser,
латерального просування немає. Вкрадено лише CPU.
Статус: локалізовано (загрозу знешкоджено); повна ремедіація (перебудова Contabo,
рішення щодо OpenClaw, ротація облікових даних) відкрита.
Повний звіт: docs/security/incident-2026-06-11-contabo-miner.md · задача #474
Phase 42: Безпека multi-tenancy (2026-04-23)
Аудитор: Sentinel (внутрішній агент безпеки)
Охоплення: ізоляція multi-tenant, SSRF, path traversal, валідація вводу
Знахідки: 16 проблем (усі виправлено)
Вердикт: 🟢 GREEN
Повний звіт: docs/security/audit-2026-04-23.md
Phase 43: Безпека UI/UX (2026-04-24)
Аудитор: Vanguard (дизайн + доступність)
Охоплення: XSS-вектори, прогалини CSP, inline-скрипти
Знахідки: 21 проблема (усі виправлено)
Вердикт: A- (95/100)
Повний звіт: docs/design/ui-ux-audit-2026-04-23.md
Phase 45: Реалізація E2EE (2026-04-28)
Реалізували: Product Owner + Claude
Охоплення: шифрування at-rest, ключі відновлення, CSP-заголовки, санітизація PII
Підфази:
- 45.1 — основа WebCrypto (
frontend/src/crm/crypto/e2ee.ts, 214 рядків) ✅ - 45.2 — шифрування API-ключів у vault (
shared/vault.tsencryptField/decryptField) ✅ - 45.3 — шифрування повідомлень чату at-rest (міграція 015, автошифрування у db.ts) ✅
- 45.4 — ключі відновлення (міграція 016, 4 API-ендпоінти, UI RecoveryKeySection) ✅
- 45.5 — CSP-заголовки + PII-санітайзер (
shared/pii-sanitizer.ts) ✅ Вердикт: 🟢 усі пункти P0+P1 завершено. Просунуті P2-можливості (forward secrecy, синхронізація між пристроями) відкладено.
Phase 45: Пентест E2EE (PLANNED)
Аудитор: зовнішній пентестер (TBD)
Охоплення: WebCrypto, керування ключами, side-channel витоки
Бюджет: $5,000
Терміни: після завершення Phase 45
Контакти
Проблеми безпеки: GitHub Security Advisory (приватне розкриття)
Загальні питання: [email protected]
Bug Bounty: $100 – $5,000 (Phase 46+)
Останній аудит: Phase 45 E2EE (2026-04-28). Далі: зовнішній пентест.