Closes #110. Also the prerequisite for #118 — arbitrary console commands need SSH on every device, and stored passwords do not scale to sixty of them. Each device gets its own ed25519 keypair, generated on the manager, pushed over the existing password session, imported, and then *proved* by opening a fresh connection and authenticating with it. Nothing trusts a key until that last step succeeds. Backups and config export use a verified key where one exists. Verified end to end on hardware: deploy, rotate twice leaving exactly one authorised key each time, verify, and a 4.6 KB config backup pulled over key-only SSH with no password fallback. **The design note on the issue was wrong about the central safety property**, and testing on a switch is what proved it. I claimed the stored password remained as a fallback. It does not: RouterOS binds a key to a user and then refuses password authentication for that user entirely. Removing the key restored password login immediately, which settles the causation. That mistake compounded with a second one. Deployment wrote the new private key to the database before proving it, so a rotation that failed halfway left the database holding a key the device had never authorised while the device demanded one we no longer had — SSH lost on the test switch, recoverable only through the binary API. Three changes follow: * Rotation authenticates with the *existing* key, since the password is no longer an option once a key is installed. * Nothing is written until a fresh connection has authenticated with the new key. A deployment that fails verification removes the key it just installed rather than leaving the device demanding a credential nobody holds. * The binary API, unaffected by any of this, is the removal path — and is therefore the real recovery route, not the password. Two further faults came out of live testing rather than reasoning. Every key for a device shared one comment, so removing "the old key" during a rotation removed the new one too; comments are now unique per key. And RouterOS stores that comment in `info`, not `key-owner` — matching the wrong field silently matched nothing and left every rotation's old key behind, which is why the switch accumulated keys that later blocked it. Revoke now clears *every* key this manager installed for a device, not just the one on record. Orphans from a half-failed rotation otherwise keep password SSH disabled with no way back through the product, which is exactly the state the test switch got stuck in. When deployment fails with an authentication error and orphans are present, the message says so instead of leaving someone guessing. The UI states the trade before it is made: installing a key disables password SSH for that user, the API and WinBox are unaffected, and revoking restores it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
63 lines
1.7 KiB
JSON
63 lines
1.7 KiB
JSON
{
|
|
"name": "mikrotik-manager-backend",
|
|
"version": "0.23.24-beta",
|
|
"description": "Mikrotik Network Management Platform - Backend API",
|
|
"main": "dist/index.js",
|
|
"scripts": {
|
|
"build": "tsc",
|
|
"start": "node dist/index.js",
|
|
"dev": "nodemon --watch src --ext ts --exec ts-node src/index.ts",
|
|
"lint": "eslint src",
|
|
"migrate": "ts-node src/db/migrate.ts",
|
|
"test": "jest",
|
|
"test:coverage": "jest --coverage"
|
|
},
|
|
"dependencies": {
|
|
"@influxdata/influxdb-client": "^1.33.2",
|
|
"@influxdata/influxdb-client-apis": "^1.35.0",
|
|
"bcryptjs": "^2.4.3",
|
|
"bullmq": "^5.1.1",
|
|
"compression": "^1.7.4",
|
|
"cors": "^2.8.5",
|
|
"dotenv": "^17.4.2",
|
|
"express": "^4.18.2",
|
|
"express-async-errors": "^3.1.1",
|
|
"helmet": "^8.1.0",
|
|
"ioredis": "^5.10.1",
|
|
"jsonwebtoken": "^9.0.2",
|
|
"morgan": "^1.10.0",
|
|
"nodemailer": "^9.0.3",
|
|
"openid-client": "^5.7.1",
|
|
"otpauth": "^9.5.1",
|
|
"pg": "^8.21.0",
|
|
"qrcode": "^1.5.4",
|
|
"socket.io": "^4.6.1",
|
|
"ssh2": "^1.14.0"
|
|
},
|
|
"devDependencies": {
|
|
"@eslint/js": "^10.0.1",
|
|
"@types/bcryptjs": "^2.4.6",
|
|
"@types/compression": "^1.7.5",
|
|
"@types/cors": "^2.8.17",
|
|
"@types/express": "^4.17.21",
|
|
"@types/jest": "^30.0.0",
|
|
"@types/jsonwebtoken": "^9.0.5",
|
|
"@types/morgan": "^1.9.9",
|
|
"@types/node": "^26.1.1",
|
|
"@types/nodemailer": "^6.4.14",
|
|
"@types/pg": "^8.10.9",
|
|
"@types/qrcode": "^1.5.6",
|
|
"@types/ssh2": "^1.11.17",
|
|
"@types/supertest": "^7.2.0",
|
|
"eslint": "^10.7.0",
|
|
"eslint-plugin-security": "^4.0.1",
|
|
"jest": "^30.4.2",
|
|
"nodemon": "^3.0.2",
|
|
"supertest": "^7.2.2",
|
|
"ts-jest": "^29.4.9",
|
|
"ts-node": "^10.9.2",
|
|
"typescript": "^6.0.3",
|
|
"typescript-eslint": "^8.64.0"
|
|
}
|
|
}
|