Sécurité
Runeya a subi 11 audits de sécurité couvrant 90+ points. Ce document détaille les principes et patterns appliqués.
Principes généraux
- Localhost-only en mode local-simple — l'agent bind sur
127.0.0.1 - Secrets chiffrés à repos (AES-256-GCM) et jamais envoyés en clair dans les logs
- Validation stricte : tous les inputs passent par des schémas Zod avant traitement
- Fail-closed : erreur de déchiffrement → exception, jamais de retour du ciphertext
- Timing-safe : comparaison de passphrases via
crypto.timingSafeEqual()
Authentification server → agent
typescript
// Agent génère une passphrase aléatoire au démarrage
const passphrase = crypto.randomBytes(32).toString('hex')
// Server stocke la passphrase chiffrée
agents.json: encrypt(passphrase)
// Chaque requête HTTP server → agent
headers: { 'Authorization': `Bearer ${passphrase}` }
// Agent valide avec timing-safe compare
const valid = crypto.timingSafeEqual(
Buffer.from(stored),
Buffer.from(received)
)WebSocket
La passphrase WebSocket est passée dans le header Authorization de la requête upgrade — jamais dans les query params de l'URL.
Validation des inputs
Toutes les routes tRPC utilisent des schémas Zod avec contraintes :
typescript
z.string().min(1).max(128) // IDs, noms
z.string().url().max(2048) // URLs (url() + max() obligatoires)
z.string().email().max(320) // Emails
z.array(items).max(100) // Arrays (max() obligatoire)
z.record(key, val).refine( // Records (count limité)
r => Object.keys(r).length <= 1000
)Isolation des processus
typescript
// NativeRunner — jamais shell: true par défaut
spawn(command, args, {
detached: true, // groupe de processus isolé
shell: false, // pas d'injection via shell
})
// Environnement — jamais ...process.env
// Utiliser buildSafeEnv() avec allowlist explicite
const env = buildSafeEnv(resolvedVariables)Chiffrement des secrets
| Élément | Méthode |
|---|---|
Variables secret: true | AES-256-GCM, clé depuis .encryption.key |
| Passphrases agents | AES-256-GCM, même clé |
| Master password | Argon2id + pepper keychain OS |
| Clé AES | 256 bits, fichier data/.encryption.key (perms 0o600) |
typescript
// decryptValue() doit toujours throw sur échec — jamais retourner le ciphertext
function decryptValue(ciphertext: string, key: Buffer): string {
const [ivHex, tagHex, dataHex] = ciphertext.split(':')
if (!ivHex || !tagHex || !dataHex) throw new Error('Invalid ciphertext format')
// validate hex before Buffer.from()
if (!/^[0-9a-f]+$/i.test(ivHex)) throw new Error('Invalid IV hex')
// ...
}Routes protégées
- Toutes les routes tRPC utilisent
protectedProcedure publicProcedureréservé aux routes de santé (health.check)- Le middleware valide le JWT avant d'appeler tout resolver
Sécurité WS — upgrade
typescript
// Validation de la passphrase AVANT handleUpgrade()
wss.on('upgrade', (req, socket, head) => {
const passphrase = extractBearer(req.headers.authorization)
if (!validatePassphrase(passphrase)) {
socket.destroy() // Rejeter AVANT handleUpgrade
return
}
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit('connection', ws, req)
})
})SSRF prevention
typescript
// testConnection utilise TOUJOURS l'URL stockée, jamais l'input utilisateur
async function testConnection(agentId: string) {
const agent = await agentStore.get(agentId) // URL depuis le store
return fetch(agent.url + '/health', { signal: AbortSignal.timeout(5000) })
}AbortSignal.timeout
Tous les appels fetch server → agent incluent un timeout :
typescript
fetch(url, {
signal: AbortSignal.timeout(10_000), // 10s max
headers: { Authorization: `Bearer ${passphrase}` },
})Checklist audit
| Catégorie | Règle |
|---|---|
| Hex validation | Valider /^[0-9a-f]+$/i avant Buffer.from(hex) |
| Secrets | Jamais retourner de plaintext secret |
| URLs | Jamais de secrets dans les query params |
| Fetch | Toujours AbortSignal.timeout() |
| Shell | shell: false dans NativeRunner |
| Binding | Agent sur 127.0.0.1 |
| WS auth | Passphrase dans header, avant handleUpgrade() |
| Schema | Strings avec .max(), arrays avec .max() |
| Records | .refine() pour limiter le nombre de clés |
| Déchiffrement | Throw sur erreur, jamais retour ciphertext |
| Fichiers | chmod 0o600 sur .encryption.key |
| tRPC | protectedProcedure sauf health |
| SSRF | testConnection utilise URL stockée |
| Env | buildSafeEnv() — jamais ...process.env |