Skip to content

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émentMéthode
Variables secret: trueAES-256-GCM, clé depuis .encryption.key
Passphrases agentsAES-256-GCM, même clé
Master passwordArgon2id + pepper keychain OS
Clé AES256 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
  • publicProcedure ré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égorieRègle
Hex validationValider /^[0-9a-f]+$/i avant Buffer.from(hex)
SecretsJamais retourner de plaintext secret
URLsJamais de secrets dans les query params
FetchToujours AbortSignal.timeout()
Shellshell: false dans NativeRunner
BindingAgent sur 127.0.0.1
WS authPassphrase dans header, avant handleUpgrade()
SchemaStrings avec .max(), arrays avec .max()
Records.refine() pour limiter le nombre de clés
DéchiffrementThrow sur erreur, jamais retour ciphertext
Fichierschmod 0o600 sur .encryption.key
tRPCprotectedProcedure sauf health
SSRFtestConnection utilise URL stockée
EnvbuildSafeEnv() — jamais ...process.env

Publié sous licence MIT.