diff --git a/README.md b/README.md index 248d633..932eed8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

- Version + Version License Docker TypeScript diff --git a/backend/package.json b/backend/package.json index 15efc2c..be0c8f4 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "mikrotik-manager-backend", - "version": "0.23.24-beta", + "version": "0.23.25-beta", "description": "Mikrotik Network Management Platform - Backend API", "main": "dist/index.js", "scripts": { diff --git a/backend/src/routes/devices.ts b/backend/src/routes/devices.ts index 1f00f40..d8ca450 100644 --- a/backend/src/routes/devices.ts +++ b/backend/src/routes/devices.ts @@ -1663,7 +1663,19 @@ router.delete('/:id/ssh-key', requireWrite, async (req: Request, res: Response) * ends by proving the key, and running sixty of those at once would be a * thundering herd against the fleet for no wall-clock gain worth having. */ -router.post('/ssh-keys/deploy-all', requireWrite, async (_req: Request, res: Response) => { +router.post('/ssh-keys/deploy-all', requireWrite, async (req: Request, res: Response) => { + // This is the most consequential call in the feature: it disables password SSH + // across an entire fleet in one request. The per-device path asks for an + // acknowledgement in the UI, and an endpoint that can do it sixty times over + // should not be easier to trigger than the one that does it once. + if ((req.body as { confirm?: boolean })?.confirm !== true) { + return res.status(400).json({ + error: 'Refusing to deploy fleet-wide without confirmation. This disables password ' + + 'SSH for the configured user on every device it touches; the API and WinBox ' + + 'are unaffected. Repeat with {"confirm": true} once that is understood.', + }); + } + const targets = await query( `SELECT ${SSH_TARGET_COLS} FROM devices d WHERE d.status != 'disabled' AND d.ssh_username IS NOT NULL diff --git a/frontend/package.json b/frontend/package.json index 0754e77..387bb92 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mikrotik-manager-frontend", - "version": "0.23.24-beta", + "version": "0.23.25-beta", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/components/device-detail/SshKeyCard.tsx b/frontend/src/components/device-detail/SshKeyCard.tsx index 7b0b253..a7ba98f 100644 --- a/frontend/src/components/device-detail/SshKeyCard.tsx +++ b/frontend/src/components/device-detail/SshKeyCard.tsx @@ -11,10 +11,16 @@ import { sshKeyApi } from '../../services/api'; * so installing a key is a real trade, not a pure addition, and the operator has * to be told before they make it rather than after (#110). */ -export default function SshKeyCard({ deviceId }: { deviceId: number }) { +export default function SshKeyCard( + { deviceId, sshUsername }: { deviceId: number; sshUsername?: string }, +) { const queryClient = useQueryClient(); const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null); const [confirmRevoke, setConfirmRevoke] = useState(false); + // Installing a key changes how this device can be reached, and cannot be + // undone by retrying — it is undone by revoking. A single click was the wrong + // weight for that, especially when revoking already asked for confirmation. + const [acknowledged, setAcknowledged] = useState(false); const { data, isLoading } = useQuery({ queryKey: ['ssh-key', deviceId], @@ -43,6 +49,7 @@ export default function SshKeyCard({ deviceId }: { deviceId: number }) { if (isLoading) return

Loading key status…
; const verified = data?.status === 'verified'; + const user = data?.ssh_username || sshUsername || 'this user'; return (
@@ -90,24 +97,53 @@ export default function SshKeyCard({ deviceId }: { deviceId: number }) {
)} - ) : ( -
- -
- Installing a key disables password SSH for this user.{' '} - RouterOS requires key authentication once a key is bound to an account. The binary API and - WinBox are unaffected, and revoking the key restores password SSH. + ) : null} + + {/* Shown before installing *and* after. Someone arriving at a keyed device + needs to know why their password no longer works over SSH just as much + as the person about to cause it. */} +
+ +
+
+ {data + ? `Password SSH is disabled for "${user}" on this device.` + : `Installing a key disables password SSH for "${user}".`}
+
    +
  • RouterOS requires key authentication once a key is bound to an account.
  • +
  • The binary API and WinBox are unaffected, and remain a way in.
  • +
  • Anyone who signs in to this device over SSH with a password — a colleague, + a script, another tool — will stop being able to.
  • +
  • + If this server loses its database or encryption key, + the private key goes with it.{' '} + SSH would then need recovering through the API or WinBox, because the password + will already have been refused. +
  • +
  • Revoking the key restores password SSH.
  • +
- )} +
{!data && ( - + <> + + + )} {data && ( <> diff --git a/frontend/src/components/device-detail/SystemConfigTab.tsx b/frontend/src/components/device-detail/SystemConfigTab.tsx index 282b46d..f13371d 100644 --- a/frontend/src/components/device-detail/SystemConfigTab.tsx +++ b/frontend/src/components/device-detail/SystemConfigTab.tsx @@ -579,7 +579,7 @@ export default function SystemConfigTab({ deviceId, device }: Props) {
- +
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 5e485a5..2ac773c 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -381,9 +381,10 @@ export const sshKeyApi = { api.post<{ message: string }>(`/devices/${deviceId}/ssh-key`, { rotate }), verify: (deviceId: number) => api.post<{ message: string }>(`/devices/${deviceId}/ssh-key/verify`), revoke: (deviceId: number) => api.delete<{ message: string }>(`/devices/${deviceId}/ssh-key`), + /** Requires explicit confirmation: this disables password SSH fleet-wide. */ deployAll: () => api.post<{ considered: number; succeeded: number; results: { device_id: number; name: string; ok: boolean; error?: string }[] }>( - '/devices/ssh-keys/deploy-all', undefined, { timeout: 600_000 }), + '/devices/ssh-keys/deploy-all', { confirm: true }, { timeout: 600_000 }), }; export const devicesApi = { diff --git a/frontend/src/version.ts b/frontend/src/version.ts index 7937c07..28ccace 100644 --- a/frontend/src/version.ts +++ b/frontend/src/version.ts @@ -1 +1 @@ -export const APP_VERSION = 'v0.23.24 Beta'; +export const APP_VERSION = 'v0.23.25 Beta';