From b027717c46dfcb2e9e0bd9be0bdf99f970ebeef1 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Thu, 24 Sep 2026 19:55:28 -0500 Subject: [PATCH] feat: move step-ca into a plugin (C4) --- package-lock.json | 15 +- package.json | 1 - packages/plugin-sdk/ARCHITECTURE.md | 119 +++--- packages/plugin-sdk/FINISH-LIST.md | 36 +- packages/plugin-sdk/src/backend.ts | 6 + packages/plugin-sdk/src/testing.ts | 2 +- plugins/step-ca/CHANGELOG.md | 16 + plugins/step-ca/README.md | 31 ++ plugins/step-ca/locales/en.json | 48 +++ plugins/step-ca/locales/translated/af_ZA.json | 31 ++ plugins/step-ca/locales/translated/ar_SA.json | 31 ++ plugins/step-ca/locales/translated/bg_BG.json | 31 ++ plugins/step-ca/locales/translated/bn_BD.json | 31 ++ plugins/step-ca/locales/translated/ca_ES.json | 31 ++ plugins/step-ca/locales/translated/cs_CZ.json | 31 ++ plugins/step-ca/locales/translated/da_DK.json | 31 ++ plugins/step-ca/locales/translated/de_DE.json | 31 ++ plugins/step-ca/locales/translated/el_GR.json | 31 ++ plugins/step-ca/locales/translated/es_ES.json | 31 ++ plugins/step-ca/locales/translated/fi_FI.json | 31 ++ plugins/step-ca/locales/translated/fr_FR.json | 31 ++ plugins/step-ca/locales/translated/he_IL.json | 31 ++ plugins/step-ca/locales/translated/hi_IN.json | 31 ++ plugins/step-ca/locales/translated/hu_HU.json | 31 ++ plugins/step-ca/locales/translated/id_ID.json | 31 ++ plugins/step-ca/locales/translated/it_IT.json | 31 ++ plugins/step-ca/locales/translated/ja_JP.json | 31 ++ plugins/step-ca/locales/translated/ko_KR.json | 31 ++ plugins/step-ca/locales/translated/nl_NL.json | 31 ++ plugins/step-ca/locales/translated/no_NO.json | 31 ++ plugins/step-ca/locales/translated/pl_PL.json | 31 ++ plugins/step-ca/locales/translated/pt_BR.json | 31 ++ plugins/step-ca/locales/translated/pt_PT.json | 31 ++ plugins/step-ca/locales/translated/ro_RO.json | 31 ++ plugins/step-ca/locales/translated/ru_RU.json | 31 ++ plugins/step-ca/locales/translated/sr_SP.json | 31 ++ plugins/step-ca/locales/translated/sv_SE.json | 31 ++ plugins/step-ca/locales/translated/th_TH.json | 31 ++ plugins/step-ca/locales/translated/tr_TR.json | 31 ++ plugins/step-ca/locales/translated/uk_UA.json | 31 ++ plugins/step-ca/locales/translated/vi_VN.json | 31 ++ plugins/step-ca/locales/translated/zh_CN.json | 31 ++ plugins/step-ca/locales/translated/zh_TW.json | 31 ++ plugins/step-ca/manifest.json | 77 ++++ .../migrations/mysql/0001_create_certs.sql | 16 + .../migrations/postgres/0001_create_certs.sql | 16 + plugins/step-ca/migrations/snapshot.json | 51 +++ .../migrations/sqlite/0001_create_certs.sql | 16 + plugins/step-ca/package.json | 18 + plugins/step-ca/src/backend/auth-session.ts | 378 ++++++++++++++++++ plugins/step-ca/src/backend/cert-store.ts | 96 +++++ .../step-ca/src/backend/client.ts | 62 +-- plugins/step-ca/src/backend/index.ts | 28 ++ plugins/step-ca/src/backend/provider.ts | 88 ++++ plugins/step-ca/src/backend/routes.ts | 65 +++ plugins/step-ca/src/backend/runtime.ts | 164 ++++++++ plugins/step-ca/src/backend/tables.ts | 34 ++ .../src/frontend/RedirectUriSetting.tsx | 54 +++ .../step-ca/src/frontend/StepCaAuthEditor.tsx | 29 ++ plugins/step-ca/src/frontend/StepCaDialog.tsx | 102 +++++ .../step-ca/src/frontend/StepCaOverlay.tsx | 187 +++++++++ plugins/step-ca/src/frontend/index.tsx | 29 ++ .../step-ca/tests/backend/client.test.ts | 73 +++- plugins/step-ca/tests/backend/fake-ca.ts | 160 ++++++++ plugins/step-ca/tests/backend/helpers.ts | 105 +++++ plugins/step-ca/tests/backend/step-ca.test.ts | 295 ++++++++++++++ .../step-ca/tests/frontend/activate.test.tsx | 137 +++++++ plugins/step-ca/tsconfig.json | 4 + plugins/step-ca/vitest.config.ts | 3 + src/backend/auth/legacy-providers.ts | 70 ---- .../database/routes/host-compat-routes.ts | 22 +- .../database/routes/host-step-ca-routes.ts | 42 -- src/backend/database/routes/host.ts | 2 - .../database/routes/user-settings-routes.ts | 129 +----- src/backend/hosts/step-ca-auth.ts | 376 ----------------- src/backend/hosts/step-ca-runtime.ts | 172 -------- src/backend/plugins/ctx-notify.ts | 6 + src/backend/starter.ts | 4 + .../routes/host-compat-routes.test.ts | 15 +- .../tests/hosts/connect/pipeline.test.ts | 51 +-- src/backend/tests/hosts/step-ca-auth.test.ts | 184 --------- src/backend/tests/plugins/ctx-notify.test.ts | 15 + .../utils/step-ca-settings-migration.test.ts | 116 ++++++ .../step-ca-settings-migration.ts | 110 +++++ src/backend/utils/step-ca-egress.ts | 13 - src/ui/api/private-endpoints-api.ts | 19 - src/ui/api/settings-api.ts | 33 -- src/ui/auth/Auth.tsx | 3 - src/ui/auth/legacy-auth-ui.tsx | 66 --- src/ui/locales/en.json | 11 - src/ui/locales/translated/af_ZA.json | 11 - src/ui/locales/translated/ar_SA.json | 11 - src/ui/locales/translated/bg_BG.json | 11 - src/ui/locales/translated/bn_BD.json | 11 - src/ui/locales/translated/ca_ES.json | 11 - src/ui/locales/translated/cs_CZ.json | 11 - src/ui/locales/translated/da_DK.json | 11 - src/ui/locales/translated/de_DE.json | 11 - src/ui/locales/translated/el_GR.json | 11 - src/ui/locales/translated/es_ES.json | 11 - src/ui/locales/translated/fi_FI.json | 11 - src/ui/locales/translated/fr_FR.json | 11 - src/ui/locales/translated/he_IL.json | 11 - src/ui/locales/translated/hi_IN.json | 11 - src/ui/locales/translated/hu_HU.json | 11 - src/ui/locales/translated/id_ID.json | 11 - src/ui/locales/translated/it_IT.json | 11 - src/ui/locales/translated/ja_JP.json | 11 - src/ui/locales/translated/ko_KR.json | 11 - src/ui/locales/translated/nl_NL.json | 11 - src/ui/locales/translated/no_NO.json | 11 - src/ui/locales/translated/pl_PL.json | 11 - src/ui/locales/translated/pt_BR.json | 11 - src/ui/locales/translated/pt_PT.json | 11 - src/ui/locales/translated/ro_RO.json | 11 - src/ui/locales/translated/ru_RU.json | 11 - src/ui/locales/translated/sr_SP.json | 11 - src/ui/locales/translated/sv_SE.json | 11 - src/ui/locales/translated/th_TH.json | 11 - src/ui/locales/translated/tr_TR.json | 11 - src/ui/locales/translated/uk_UA.json | 11 - src/ui/locales/translated/vi_VN.json | 11 - src/ui/locales/translated/zh_CN.json | 11 - src/ui/locales/translated/zh_TW.json | 11 - src/ui/plugin-host/auth-registry.ts | 2 - src/ui/sidebar/AdminSettingsPanel.tsx | 54 --- src/ui/sidebar/AdminSettingsSections.tsx | 79 ---- src/ui/sidebar/HostEditor.tsx | 3 - 128 files changed, 3805 insertions(+), 1781 deletions(-) create mode 100644 plugins/step-ca/CHANGELOG.md create mode 100644 plugins/step-ca/README.md create mode 100644 plugins/step-ca/locales/en.json create mode 100644 plugins/step-ca/locales/translated/af_ZA.json create mode 100644 plugins/step-ca/locales/translated/ar_SA.json create mode 100644 plugins/step-ca/locales/translated/bg_BG.json create mode 100644 plugins/step-ca/locales/translated/bn_BD.json create mode 100644 plugins/step-ca/locales/translated/ca_ES.json create mode 100644 plugins/step-ca/locales/translated/cs_CZ.json create mode 100644 plugins/step-ca/locales/translated/da_DK.json create mode 100644 plugins/step-ca/locales/translated/de_DE.json create mode 100644 plugins/step-ca/locales/translated/el_GR.json create mode 100644 plugins/step-ca/locales/translated/es_ES.json create mode 100644 plugins/step-ca/locales/translated/fi_FI.json create mode 100644 plugins/step-ca/locales/translated/fr_FR.json create mode 100644 plugins/step-ca/locales/translated/he_IL.json create mode 100644 plugins/step-ca/locales/translated/hi_IN.json create mode 100644 plugins/step-ca/locales/translated/hu_HU.json create mode 100644 plugins/step-ca/locales/translated/id_ID.json create mode 100644 plugins/step-ca/locales/translated/it_IT.json create mode 100644 plugins/step-ca/locales/translated/ja_JP.json create mode 100644 plugins/step-ca/locales/translated/ko_KR.json create mode 100644 plugins/step-ca/locales/translated/nl_NL.json create mode 100644 plugins/step-ca/locales/translated/no_NO.json create mode 100644 plugins/step-ca/locales/translated/pl_PL.json create mode 100644 plugins/step-ca/locales/translated/pt_BR.json create mode 100644 plugins/step-ca/locales/translated/pt_PT.json create mode 100644 plugins/step-ca/locales/translated/ro_RO.json create mode 100644 plugins/step-ca/locales/translated/ru_RU.json create mode 100644 plugins/step-ca/locales/translated/sr_SP.json create mode 100644 plugins/step-ca/locales/translated/sv_SE.json create mode 100644 plugins/step-ca/locales/translated/th_TH.json create mode 100644 plugins/step-ca/locales/translated/tr_TR.json create mode 100644 plugins/step-ca/locales/translated/uk_UA.json create mode 100644 plugins/step-ca/locales/translated/vi_VN.json create mode 100644 plugins/step-ca/locales/translated/zh_CN.json create mode 100644 plugins/step-ca/locales/translated/zh_TW.json create mode 100644 plugins/step-ca/manifest.json create mode 100644 plugins/step-ca/migrations/mysql/0001_create_certs.sql create mode 100644 plugins/step-ca/migrations/postgres/0001_create_certs.sql create mode 100644 plugins/step-ca/migrations/snapshot.json create mode 100644 plugins/step-ca/migrations/sqlite/0001_create_certs.sql create mode 100644 plugins/step-ca/package.json create mode 100644 plugins/step-ca/src/backend/auth-session.ts create mode 100644 plugins/step-ca/src/backend/cert-store.ts rename src/backend/utils/step-ca-client.ts => plugins/step-ca/src/backend/client.ts (91%) create mode 100644 plugins/step-ca/src/backend/index.ts create mode 100644 plugins/step-ca/src/backend/provider.ts create mode 100644 plugins/step-ca/src/backend/routes.ts create mode 100644 plugins/step-ca/src/backend/runtime.ts create mode 100644 plugins/step-ca/src/backend/tables.ts create mode 100644 plugins/step-ca/src/frontend/RedirectUriSetting.tsx create mode 100644 plugins/step-ca/src/frontend/StepCaAuthEditor.tsx create mode 100644 plugins/step-ca/src/frontend/StepCaDialog.tsx create mode 100644 plugins/step-ca/src/frontend/StepCaOverlay.tsx create mode 100644 plugins/step-ca/src/frontend/index.tsx rename src/backend/tests/utils/step-ca-client.test.ts => plugins/step-ca/tests/backend/client.test.ts (59%) create mode 100644 plugins/step-ca/tests/backend/fake-ca.ts create mode 100644 plugins/step-ca/tests/backend/helpers.ts create mode 100644 plugins/step-ca/tests/backend/step-ca.test.ts create mode 100644 plugins/step-ca/tests/frontend/activate.test.tsx create mode 100644 plugins/step-ca/tsconfig.json create mode 100644 plugins/step-ca/vitest.config.ts delete mode 100644 src/backend/database/routes/host-step-ca-routes.ts delete mode 100644 src/backend/hosts/step-ca-auth.ts delete mode 100644 src/backend/hosts/step-ca-runtime.ts delete mode 100644 src/backend/tests/hosts/step-ca-auth.test.ts create mode 100644 src/backend/tests/utils/step-ca-settings-migration.test.ts create mode 100644 src/backend/utils/crypto-migration/step-ca-settings-migration.ts delete mode 100644 src/backend/utils/step-ca-egress.ts delete mode 100644 src/ui/auth/legacy-auth-ui.tsx diff --git a/package-lock.json b/package-lock.json index f649c6fa5..db275afb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,6 @@ "nanoid": "^6.0.1", "node-pty": "^1.1.0", "pg": "^8.23.0", - "redis": "^6.2.1", "semver": "^7.8.5", "sharp": "^0.35.4", "socks": "^2.8.10", @@ -7041,6 +7040,10 @@ "resolved": "plugins/sso", "link": true }, + "node_modules/@termix-plugin/step-ca": { + "resolved": "plugins/step-ca", + "link": true + }, "node_modules/@termix-plugin/tailscale": { "resolved": "plugins/tailscale", "link": true @@ -18725,6 +18728,16 @@ "@termix/plugin-sdk": "*" } }, + "plugins/step-ca": { + "name": "@termix-plugin/step-ca", + "version": "1.0.0", + "dependencies": { + "redis": "^6.2.1" + }, + "devDependencies": { + "@termix/plugin-sdk": "*" + } + }, "plugins/tailscale": { "name": "@termix-plugin/tailscale", "version": "1.0.0", diff --git a/package.json b/package.json index 1e77cb5dc..92decfdc8 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,6 @@ "nanoid": "^6.0.1", "node-pty": "^1.1.0", "pg": "^8.23.0", - "redis": "^6.2.1", "semver": "^7.8.5", "sharp": "^0.35.4", "socks": "^2.8.10", diff --git a/packages/plugin-sdk/ARCHITECTURE.md b/packages/plugin-sdk/ARCHITECTURE.md index 47102bd8b..dd369ee11 100644 --- a/packages/plugin-sdk/ARCHITECTURE.md +++ b/packages/plugin-sdk/ARCHITECTURE.md @@ -1008,11 +1008,11 @@ auto-session and trusted devices, plus the base SSH auth types password, key, stored credential, agent and none. Every other login method, second factor and SSH auth method is a plugin through `ctx.auth`. -In 2.9.0, Step-CA and Vault still live in core. -They register through the same interfaces from -`src/backend/auth/legacy-providers.ts` and `src/ui/auth/legacy-auth-ui.tsx` -with `pluginId: "core"`. Phase C moves each one into its plugin by moving its -block out of those two files; D1 deletes them. +In 2.9.0, Vault still lives in core. It registers through the same +interface from `src/backend/auth/legacy-providers.ts` with +`pluginId: "core"`. Its Phase C step moves its block out of that file; D1 +deletes it. **C4** moved Step-CA out and deleted +`src/ui/auth/legacy-auth-ui.tsx`, which had no other editor left. Nothing else in core branches on those type names. **C1** moved TOTP and passkeys out: @@ -1136,11 +1136,28 @@ Nothing else in core branches on those type names. 2.8 message names for Termix-Mobile; the file manager and docker answer `requires_browser_sign_in` and continue on `connect-browser-sign-in`. - `@termix/plugin-sdk/ssh-certs` (`applyCertificateAuth`) replaces core's - `opkssh-cert-auth.ts`: opkssh, core key auth with a CA certificate, and the - Step-CA and Vault providers still in core use it. -- Step-CA no longer shares anything with OPKSSH: its interaction is - `stepca`, its messages `stepca_*`, and it keeps issued certificates in - memory until it becomes a plugin. + `opkssh-cert-auth.ts`: opkssh, step-ca, core key auth with a CA + certificate, and the Vault provider still in core use it. + +**C4** moved Step-CA out: + +- `step-ca` registers the `stepca` SSH auth provider. Termix never runs the + `step` binary: the plugin talks to the CA's HTTPS API and the identity + provider through `ctx.fetch` (`network:outbound`), using its `tls` option + to bootstrap the CA root by fingerprint and pin it afterwards. +- Issued certificates live in its own table, `p_step_ca_certs`, sealed with + `ctx.secrets.seal`, until the certificate's `validBefore`. 2.8 cached them + in `opkssh_tokens`; those are left to expire. +- The CA URL, fingerprint, provisioner and private endpoint list are admin + settings. `step-ca-settings-migration.ts` copies the 2.8 `step_ca_*` + settings rows in and turns on `legacyCallback` when a CA was configured, + so an upgraded install keeps sending `/host/step-ca-callback`, which + core answers with a 307 to `/plugin-api/step-ca/callback`. +- With `REDIS_URL` set, a callback that lands on another instance is handed + to the one that started the sign-in through Redis + (`TERMIX_STEP_CA_REDIS_PREFIX`), as in 2.8. +- The sign-in dialog is a `terminal.overlay` contribution speaking + `stepca_*`. #### One SSH connect pipeline @@ -1682,44 +1699,44 @@ deciding, not the mechanism. Built per plugin in `src/backend/plugins/ctx.ts` and passed to `activate`. -| Member | Capability | Status | -| ------------------------------------------------ | -------------------------------------------------- | ----------------------- | -| `ctx.pluginId`, `ctx.manifest` | none | **A1** | -| `ctx.log.*` | none | **A1** | -| `ctx.events.emit` / `.on` | `events:core` for core topics | **A1** | -| `ctx.kv.get/set/delete/list` | `kv:own` | **A1** | -| `ctx.files.dataDir` | `files:own` | **B13** | -| `ctx.registry.*` | none | **A1** | -| `ctx.services.provide` / `.get` / `.providers` | per-service RBAC | **A1**, named **B12** | -| `ctx.secrets.offer` / `.withdraw` / `.getShared` | per-secret RBAC | **A1** | -| `ctx.secrets.get` / `.set` / `.delete` | `secrets:own` | **B18** | -| `ctx.secrets.seal` / `.unseal` | `secrets:own` | **C1** | -| `ctx.disposables.add` | none | **A1** | -| `ctx.asUser(userId, fn)` | none, always audited | **A1** | -| `ctx.currentActor()` | none | **A1** | -| `ctx.db.define` / `.client` / `.refs` | `db:own` | **A3** | -| `ctx.db.persist` / `.dialect` | `db:own` (persist only) | **A9**, lazy **B16** | -| `ctx.sync.registerEntity` | none | **A3** | -| `ctx.http.router` / `ctx.ws.route` / `.upgrade` | `network:serve` | **A4** | -| `ctx.http.baseUrl(req)` | none | **C2** | -| `ctx.rbac.has` / `.hasFor` / `.require` | own permissions only | **A5** | -| `ctx.capabilities.has` / `.require` | the capability itself | **B11** | -| `ctx.hosts.*` | `hosts:read` / `hosts:write` | **B4**, **B5**, **B18** | -| `ctx.hosts.status.*` | `hosts:read` | **B16** | -| `ctx.ssh.*` | `ssh:connect`, `credentials:use` | **A8** | -| `ctx.settings.*` | `settings:read-core` (readCore only) | **A6** | -| `ctx.notify.channels` / `.send` | `notify:send` | **B17** | -| `ctx.auth.*` | `auth:provide` | **A8** | -| `ctx.auth.completeRedirectLogin` and the rest | `auth:provide` | **C2** | -| `ctx.desktop.openIsolatedWindow` | `desktop:window` | **B8** | -| `ctx.desktop.launchNativeRdp` / `.available` | `desktop:window` (launch only) | **B14** | -| `ctx.credentials.resolveHostProtocol` | `credentials:read` | **B14** | -| `ctx.credentials.registerSecretResolver` | `auth:provide` | **B20** | -| `ctx.audit.record` | none, the actor is the runtime's | **B9** | -| `ctx.schedule.every` / `.after` | none | **B16** | -| `ctx.fetch` | `network:outbound` | **B17**, signal **B18** | -| `ctx.process.run` / `.ensureBinary` | `process:spawn` (+ `network:outbound` to download) | **C3** | -| `ctx.auth.registerKeyboardInteractiveHandler` | `auth:provide` | **C3** | +| Member | Capability | Status | +| ------------------------------------------------ | -------------------------------------------------- | ----------------------------------- | +| `ctx.pluginId`, `ctx.manifest` | none | **A1** | +| `ctx.log.*` | none | **A1** | +| `ctx.events.emit` / `.on` | `events:core` for core topics | **A1** | +| `ctx.kv.get/set/delete/list` | `kv:own` | **A1** | +| `ctx.files.dataDir` | `files:own` | **B13** | +| `ctx.registry.*` | none | **A1** | +| `ctx.services.provide` / `.get` / `.providers` | per-service RBAC | **A1**, named **B12** | +| `ctx.secrets.offer` / `.withdraw` / `.getShared` | per-secret RBAC | **A1** | +| `ctx.secrets.get` / `.set` / `.delete` | `secrets:own` | **B18** | +| `ctx.secrets.seal` / `.unseal` | `secrets:own` | **C1** | +| `ctx.disposables.add` | none | **A1** | +| `ctx.asUser(userId, fn)` | none, always audited | **A1** | +| `ctx.currentActor()` | none | **A1** | +| `ctx.db.define` / `.client` / `.refs` | `db:own` | **A3** | +| `ctx.db.persist` / `.dialect` | `db:own` (persist only) | **A9**, lazy **B16** | +| `ctx.sync.registerEntity` | none | **A3** | +| `ctx.http.router` / `ctx.ws.route` / `.upgrade` | `network:serve` | **A4** | +| `ctx.http.baseUrl(req)` | none | **C2** | +| `ctx.rbac.has` / `.hasFor` / `.require` | own permissions only | **A5** | +| `ctx.capabilities.has` / `.require` | the capability itself | **B11** | +| `ctx.hosts.*` | `hosts:read` / `hosts:write` | **B4**, **B5**, **B18** | +| `ctx.hosts.status.*` | `hosts:read` | **B16** | +| `ctx.ssh.*` | `ssh:connect`, `credentials:use` | **A8** | +| `ctx.settings.*` | `settings:read-core` (readCore only) | **A6** | +| `ctx.notify.channels` / `.send` | `notify:send` | **B17** | +| `ctx.auth.*` | `auth:provide` | **A8** | +| `ctx.auth.completeRedirectLogin` and the rest | `auth:provide` | **C2** | +| `ctx.desktop.openIsolatedWindow` | `desktop:window` | **B8** | +| `ctx.desktop.launchNativeRdp` / `.available` | `desktop:window` (launch only) | **B14** | +| `ctx.credentials.resolveHostProtocol` | `credentials:read` | **B14** | +| `ctx.credentials.registerSecretResolver` | `auth:provide` | **B20** | +| `ctx.audit.record` | none, the actor is the runtime's | **B9** | +| `ctx.schedule.every` / `.after` | none | **B16** | +| `ctx.fetch` | `network:outbound` | **B17**, signal **B18**, tls **C4** | +| `ctx.process.run` / `.ensureBinary` | `process:spawn` (+ `network:outbound` to download) | **C3** | +| `ctx.auth.registerKeyboardInteractiveHandler` | `auth:provide` | **C3** | **B11** added `ctx.capabilities.has(capability)` / `.require(capability)`, a generic check for a capability no other ctx member wraps. Unlike every other @@ -1956,6 +1973,10 @@ notification)` delivers to the ones among them that are theirs and `network:outbound`, audited as `plugin_fetch`. Automations' HTTP step passes the admin allowlist it reads through `ctx.settings.readCore` (`notification_private_endpoint_allowlist` joined `CORE_SETTINGS_ALLOWLIST`). + **C4** added `tls: { ca, rejectUnauthorized }` for a private CA: `ca` + trusts a PEM bundle, and `rejectUnauthorized: false` is for the one request + that fetches a CA root by fingerprint, which the caller then checks + (step-ca, like `step ca bootstrap`). **B18** added three members for the AI assistant: @@ -2519,7 +2540,7 @@ Then, with the app running: The bundled plugins predate the SDK, apart from workspaces (A9), snippets (B2), remote-desktop (B14), docker (B15), host-metrics (B16), automations (B17), ai (B18), homepage (B19), totp and webauthn (C1), sso and ldap (C2), -opkssh and warpgate (C3), +opkssh and warpgate (C3), step-ca (C4), which import nothing from core. The others still reach core by relative path (`../../../../src/backend/...`), which an esbuild plugin, `packages/plugin-sdk/cli/lib/legacy-core-imports.mjs`, keeps out of the bundle diff --git a/packages/plugin-sdk/FINISH-LIST.md b/packages/plugin-sdk/FINISH-LIST.md index c43775f16..66994b3ea 100644 --- a/packages/plugin-sdk/FINISH-LIST.md +++ b/packages/plugin-sdk/FINISH-LIST.md @@ -483,21 +483,13 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite (`h.pluginSettings?.["web-endpoint"]`). The file is untouched since before C2. Owner: D0. -- **C3 (opkssh), for the Step-CA step:** Step-CA is still core and no longer - shares OPKSSH's pieces. Its interaction is `stepca` and its socket messages - `stepca_*`, but nothing draws them yet: until its plugin contributes a - `terminal.overlay` (the opkssh plugin's `OpksshOverlay` and `OpksshDialog` - are the model), a Step-CA host asks for a sign-in the terminal cannot show. - Its issued certificates are cached in memory (`step-ca-auth.ts`), so they - are lost on restart; its plugin should own a table the way opkssh owns - `p_opkssh_tokens`. Step-CA certificates 2.8 cached in `opkssh_tokens` stay - in the adopted `p_opkssh_tokens`, unused, until they expire. Owner: the - Phase C step that moves Step-CA. -- **C3 (opkssh):** `database/routes/host-compat-routes.ts` answers the 2.8 - redirect URI `/host/opkssh-callback` with a 307 to the plugin and so names - the plugin's path. Keep it until identity providers have moved to - `/plugin-api/opkssh/callback` (the admin page shows it, and the - `legacyCallback` setting switches over). Owner: 3.0.0. +- **C3 (opkssh), C4 (step-ca):** `database/routes/host-compat-routes.ts` + answers the 2.8 redirect URIs `/host/opkssh-callback` and + `/host/step-ca-callback` with a 307 to the plugins and so names their + paths. Keep it until identity providers have moved to + `/plugin-api/opkssh/callback` and `/plugin-api/step-ca/callback` (each + plugin's admin page shows it, and its `legacyCallback` setting switches + over). Owner: 3.0.0. - **C3 (opkssh/warpgate):** `ssh_data.use_warpgate` and the `.opk` folder under `DATA_DIR` stay on disk, unused (the drizzle drop is `SELECT 1;`, the config is copied, not moved). Remove them in 3.0.0. Owner: 3.0.0. @@ -517,6 +509,20 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite `useWarpgate` field inline (the host editor sends plugin settings). The bulk list should come from the registered providers plus `contributes.auth.sshAuthTypes`. Owner: D1. +- **C4 (step-ca):** the 2.8 `step_ca_url`, `step_ca_fingerprint`, + `step_ca_provisioner` and `step_ca_private_endpoint_allowlist` rows stay in + core `settings`, unused, after `step-ca-settings-migration.ts` copies them + (kept for one release so a downgrade works). Delete them in 3.0.0. + Owner: 3.0.0. +- **C4 (step-ca):** the admin fields are plain settings now, so a bad CA URL + or fingerprint is reported when someone signs in (in the terminal dialog) + instead of when the admin saves, as the 2.8 route did. A settings + validation hook for plugins would bring the save-time check back. + Owner: D0. +- **C4 (step-ca):** the Termix docs still describe Step CA under Admin + Settings and the `/host/step-ca-callback` redirect URI. They need the + plugin settings page, the new redirect URI and the `legacyCallback` + setting. Owner: D0 (docs repo). - **Pre-existing, found in C3:** the Docker `opkssh-downloader` stage's `OPKSSH_VERSION` build arg is not passed on at runtime, so building with another version gives a prebaked binary the plugin's pinned checksum diff --git a/packages/plugin-sdk/src/backend.ts b/packages/plugin-sdk/src/backend.ts index f882b1142..f608a8da8 100644 --- a/packages/plugin-sdk/src/backend.ts +++ b/packages/plugin-sdk/src/backend.ts @@ -1490,6 +1490,12 @@ export interface PluginFetchInit { allowPrivateHosts?: readonly string[]; /** Aborts the request, and a streamed body, when the caller gives up. */ signal?: AbortSignal; + /** + * TLS for a private CA. `ca` trusts this PEM bundle instead of the system + * store. `rejectUnauthorized: false` skips verification, only for fetching + * a CA root by fingerprint, where the caller checks the answer itself. + */ + tls?: { ca?: string; rejectUnauthorized?: boolean }; } /** diff --git a/packages/plugin-sdk/src/testing.ts b/packages/plugin-sdk/src/testing.ts index e40681724..25d748786 100644 --- a/packages/plugin-sdk/src/testing.ts +++ b/packages/plugin-sdk/src/testing.ts @@ -878,7 +878,7 @@ export function createFakeContext( requiresSecret: (authType) => ["password", "key", "credential", "agent"].includes(authType), supportsBackground: (authType) => - !["none", "stepca"].includes(authType) && + authType !== "none" && !auth.sshAuthProviders.some( (provider) => provider.type === authType && provider.supportsBackground === false, diff --git a/plugins/step-ca/CHANGELOG.md b/plugins/step-ca/CHANGELOG.md new file mode 100644 index 000000000..d3fc5a77e --- /dev/null +++ b/plugins/step-ca/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +## 1.0.0 + +Bundled with Termix 2.9.0. + +- Step CA sign-in moved out of core. The callback is now + `/plugin-api/step-ca/callback`; the 2.8 URI `/host/step-ca-callback` + still works and upgraded installs keep sending it until an admin turns + off "Use the old redirect URI". +- The CA URL, fingerprint, provisioner and private endpoint list moved from + Admin Settings into this plugin's settings. +- Issued certificates are stored encrypted in the plugin's own table, so + they survive a restart. Certificates cached by 2.8 are not carried over; + the next connection asks for one sign-in. +- The terminal shows the sign-in dialog for Step CA hosts again. diff --git a/plugins/step-ca/README.md b/plugins/step-ca/README.md new file mode 100644 index 000000000..499642349 --- /dev/null +++ b/plugins/step-ca/README.md @@ -0,0 +1,31 @@ +# Step CA + +Adds the `stepca` SSH auth type: Termix signs you in with the identity +provider behind a [smallstep step-ca](https://smallstep.com/docs/step-ca/) +OIDC provisioner, has the CA sign a fresh key, and keeps the short-lived SSH +certificate until it expires. + +Termix never runs the `step` binary. It talks to the CA's HTTPS API +(`/root/`, `/provisioners`, `/1.0/ssh/sign`) and to the identity +provider directly, through core's outbound request guard. + +## Settings + +Admin settings, under Settings > Plugins > Step CA: + +- **CA URL**, **Root fingerprint** and **OIDC provisioner name**. Leave all + three empty to turn Step CA off. +- **Allowed private Step CA hosts**: the CA and, if it is internal, the + identity provider. Anything private that is not listed is refused. +- **Redirect URI**: register it with the identity provider: + `/plugin-api/step-ca/callback`, or + `/host/step-ca-callback` while "Use the old redirect URI" is on. + +An install upgraded from 2.8 has its Step CA settings copied in and keeps +the old redirect URI turned on. + +## Several instances + +With `REDIS_URL` set, a callback that reaches another Termix instance is +handed to the one that started the sign-in. `TERMIX_STEP_CA_REDIS_PREFIX` +changes the key prefix (default `termix:step-ca`). diff --git a/plugins/step-ca/locales/en.json b/plugins/step-ca/locales/en.json new file mode 100644 index 000000000..61c6d3993 --- /dev/null +++ b/plugins/step-ca/locales/en.json @@ -0,0 +1,48 @@ +{ + "hosts": { + "filterAuthStepca": "Step CA" + }, + "editor": { + "label": "Step CA", + "description": "Sign in through your identity provider and let your Step CA issue a short-lived SSH certificate for this host. The CA URL, root fingerprint and OIDC provisioner are set by an administrator in the Step CA plugin settings.", + "docs": "View docs" + }, + "dialog": { + "title": "Step CA Authentication Required", + "description": "Complete authentication in your browser to continue. The certificate stays valid for as long as your CA allows.", + "openBrowser": "Open Browser to Authenticate", + "waiting": "Waiting for authentication in browser...", + "authenticating": "Processing authentication..." + }, + "errors": { + "authFailed": "Authentication failed. Please check your credentials and try again.", + "timeout": "Authentication timed out. Please try again." + }, + "settings": { + "caUrl": { + "label": "CA URL", + "description": "The https URL of your smallstep CA. Leave the CA URL, fingerprint and provisioner empty to turn Step CA off.", + "placeholder": "https://ca.internal:9000" + }, + "fingerprint": { + "label": "Root fingerprint (SHA-256)", + "description": "The root certificate fingerprint, as shown by step ca bootstrap." + }, + "provisioner": { + "label": "OIDC provisioner name", + "description": "The name of the CA's OIDC provisioner. Then choose \"Step CA\" as a host's authentication type." + }, + "privateEndpoints": { + "label": "Allowed private Step CA hosts", + "description": "Private hosts the Step CA certificate flow may contact: the CA itself and, if internal, your identity provider. Separate them with commas or new lines." + }, + "redirectUri": { + "label": "Redirect URI", + "description": "Register this URL as an allowed redirect URI with the identity provider behind the OIDC provisioner." + }, + "legacyCallback": { + "label": "Use the old redirect URI", + "description": "Keep sending /host/step-ca-callback, the redirect URI used before 2.9. Turn this off once your identity provider allows the new one." + } + } +} diff --git a/plugins/step-ca/locales/translated/af_ZA.json b/plugins/step-ca/locales/translated/af_ZA.json new file mode 100644 index 000000000..4eeb03736 --- /dev/null +++ b/plugins/step-ca/locales/translated/af_ZA.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Stap CA" + }, + "editor": { + "label": "Stap CA", + "description": "Meld aan deur jou identiteitsverskaffer en laat jou Step CA 'n kortstondige SSH-sertifikaat vir hierdie gasheer uitreik. Die CA-URL, wortelvingerafdruk en OIDC-voorsiening word deur 'n administrateur onder Admin-instellings ingestel.", + "docs": "Bekyk dokumente" + }, + "dialog": { + "openBrowser": "Maak blaaier oop om te verifieer", + "waiting": "Wag vir verifikasie in blaaier...", + "authenticating": "Verwerk verifikasie..." + }, + "errors": { + "authFailed": "Verifikasie het misluk. Kontroleer asseblief jou geloofsbriewe en probeer weer.", + "timeout": "Verifikasie het verstryk. Probeer asseblief weer." + }, + "settings": { + "fingerprint": { + "label": "Wortelvingerafdruk (SHA-256)" + }, + "provisioner": { + "label": "OIDC-voorsieningsnaam" + }, + "privateEndpoints": { + "label": "Toegelate private Step CA-gashere", + "description": "Privaat gashere, die Step CA-sertifikaatvloei, mag kontak maak met: die CA self en, indien intern, jou identiteitsverskaffer. Skei hulle met kommas." + } + } +} diff --git a/plugins/step-ca/locales/translated/ar_SA.json b/plugins/step-ca/locales/translated/ar_SA.json new file mode 100644 index 000000000..4068f4e0a --- /dev/null +++ b/plugins/step-ca/locales/translated/ar_SA.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "الخطوة ج" + }, + "editor": { + "label": "الخطوة ج", + "description": "سجّل الدخول عبر موفر الهوية الخاص بك، ودع جهة إصدار الشهادات المعتمدة (SA) التابعة لـ Step CA تُصدر شهادة SSH مؤقتة لهذا المضيف. يتم تحديد عنوان URL الخاص بجهة إصدار الشهادات المعتمدة، وبصمة الجذر، وموفر OIDC بواسطة مسؤول النظام ضمن إعدادات المسؤول.", + "docs": "عرض المستندات" + }, + "dialog": { + "openBrowser": "افتح المتصفح للمصادقة", + "waiting": "في انتظار المصادقة في المتصفح...", + "authenticating": "جارٍ معالجة المصادقة..." + }, + "errors": { + "authFailed": "فشلت المصادقة. يرجى التحقق من بيانات الاعتماد والمحاولة مرة أخرى.", + "timeout": "انتهت مهلة المصادقة. يرجى المحاولة مرة أخرى." + }, + "settings": { + "fingerprint": { + "label": "بصمة الجذر (SHA-256)" + }, + "provisioner": { + "label": "اسم موفر OIDC" + }, + "privateEndpoints": { + "label": "مضيفو شهادات الخطوة الخاصة المسموح بها", + "description": "قد تتصل جهات الاستضافة الخاصة بعملية شهادة Step CA بما يلي: جهة إصدار الشهادات نفسها، وإذا كانت داخلية، مزود الهوية الخاص بك. افصل بينهما بفواصل." + } + } +} diff --git a/plugins/step-ca/locales/translated/bg_BG.json b/plugins/step-ca/locales/translated/bg_BG.json new file mode 100644 index 000000000..243c96ca8 --- /dev/null +++ b/plugins/step-ca/locales/translated/bg_BG.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Стъпка CA" + }, + "editor": { + "label": "Стъпка CA", + "description": "Влезте чрез вашия доставчик на самоличност и позволете на вашия Step CA да издаде краткотраен SSH сертификат за този хост. URL адресът на CA, root пръстовият отпечатък и OIDC provisioner се задават от администратор в „Настройки на администратора“.", + "docs": "Преглед на документи" + }, + "dialog": { + "openBrowser": "Отворете браузъра за удостоверяване", + "waiting": "Чака се удостоверяване в браузъра...", + "authenticating": "Обработва се удостоверяване..." + }, + "errors": { + "authFailed": "Удостоверяването не бе успешно. Моля, проверете идентификационните си данни и опитайте отново.", + "timeout": "Времето за изчакване на удостоверяването изтече. Моля, опитайте отново." + }, + "settings": { + "fingerprint": { + "label": "Коренен пръстов отпечатък (SHA-256)" + }, + "provisioner": { + "label": "Име на доставчика на OIDC" + }, + "privateEndpoints": { + "label": "Разрешени частни хостове на Step CA", + "description": "Частни хостове, с които потокът от сертификати на Step CA може да се свърже: самия CA и, ако е вътрешен, вашият доставчик на идентичност. Разделете ги със запетаи." + } + } +} diff --git a/plugins/step-ca/locales/translated/bn_BD.json b/plugins/step-ca/locales/translated/bn_BD.json new file mode 100644 index 000000000..993619805 --- /dev/null +++ b/plugins/step-ca/locales/translated/bn_BD.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "স্টেপ সিএ" + }, + "editor": { + "label": "স্টেপ সিএ", + "description": "আপনার আইডেন্টিটি প্রোভাইডারের মাধ্যমে সাইন ইন করুন এবং আপনার স্টেপ সিএ-কে এই হোস্টের জন্য একটি স্বল্পস্থায়ী এসএসএইচ সার্টিফিকেট ইস্যু করতে দিন। সিএ ইউআরএল, রুট ফিঙ্গারপ্রিন্ট এবং ওআইডিসি প্রোভিশনার একজন অ্যাডমিনিস্ট্রেটর দ্বারা অ্যাডমিন সেটিংস-এর অধীনে সেট করা হয়।", + "docs": "নথি দেখুন" + }, + "dialog": { + "openBrowser": "প্রমাণীকরণের জন্য ব্রাউজার খুলুন", + "waiting": "ব্রাউজারে প্রমাণীকরণের জন্য অপেক্ষা করা হচ্ছে...", + "authenticating": "প্রমাণীকরণ প্রক্রিয়া চলছে..." + }, + "errors": { + "authFailed": "প্রমাণীকরণ ব্যর্থ হয়েছে। অনুগ্রহ করে আপনার পরিচয়পত্র যাচাই করে আবার চেষ্টা করুন।", + "timeout": "প্রমাণীকরণের সময়সীমা শেষ হয়ে গেছে। অনুগ্রহ করে আবার চেষ্টা করুন।" + }, + "settings": { + "fingerprint": { + "label": "রুট ফিঙ্গারপ্রিন্ট (SHA-256)" + }, + "provisioner": { + "label": "OIDC সরবরাহকারীর নাম" + }, + "privateEndpoints": { + "label": "ব্যক্তিগত স্টেপ সিএ হোস্টদের অনুমতি দেওয়া হয়েছে", + "description": "স্টেপ সিএ সার্টিফিকেট ফ্লো যেসব প্রাইভেট হোস্টের সাথে যোগাযোগ করতে পারে, সেগুলো হলো: স্বয়ং সিএ এবং, যদি অভ্যন্তরীণ হয়, আপনার আইডেন্টিটি প্রোভাইডার। এদেরকে কমা দিয়ে আলাদা করুন।" + } + } +} diff --git a/plugins/step-ca/locales/translated/ca_ES.json b/plugins/step-ca/locales/translated/ca_ES.json new file mode 100644 index 000000000..76905a717 --- /dev/null +++ b/plugins/step-ca/locales/translated/ca_ES.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Pas CA" + }, + "editor": { + "label": "Pas CA", + "description": "Inicieu la sessió a través del vostre proveïdor d'identitat i deixeu que la vostra Step CA emeti un certificat SSH de curta durada per a aquest amfitrió. L'URL de la CA, l'empremta digital arrel i el proveïdor OIDC els estableix un administrador a Configuració de l'administrador.", + "docs": "Veure documents" + }, + "dialog": { + "openBrowser": "Obre el navegador per autenticar-te", + "waiting": "Esperant l'autenticació al navegador...", + "authenticating": "Processant l'autenticació..." + }, + "errors": { + "authFailed": "L'autenticació ha fallat. Si us plau, comproveu les vostres credencials i torneu-ho a intentar.", + "timeout": "S'ha esgotat el temps d'autenticació. Torna-ho a provar." + }, + "settings": { + "fingerprint": { + "label": "Empremta digital arrel (SHA-256)" + }, + "provisioner": { + "label": "Nom del proveïdor OIDC" + }, + "privateEndpoints": { + "label": "Amfitrions privats permesos de Step CA", + "description": "Amfitrions privats amb els quals el flux de certificats de la CA de Step pot contactar: la mateixa CA i, si és intern, el vostre proveïdor d'identitat. Separeu-los amb comes." + } + } +} diff --git a/plugins/step-ca/locales/translated/cs_CZ.json b/plugins/step-ca/locales/translated/cs_CZ.json new file mode 100644 index 000000000..6e298e648 --- /dev/null +++ b/plugins/step-ca/locales/translated/cs_CZ.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Krok CA" + }, + "editor": { + "label": "Krok CA", + "description": "Přihlaste se prostřednictvím svého poskytovatele identity a nechte svou certifikační autoritu Step CA vydat krátkodobý SSH certifikát pro tohoto hostitele. URL certifikační autority, otisk kořenového serveru a zřizovatel OIDC nastavuje administrátor v Nastavení správce.", + "docs": "Zobrazit dokumenty" + }, + "dialog": { + "openBrowser": "Otevřít prohlížeč pro ověření", + "waiting": "Čekání na ověření v prohlížeči...", + "authenticating": "Zpracování ověřování..." + }, + "errors": { + "authFailed": "Ověření se nezdařilo. Zkontrolujte prosím své přihlašovací údaje a zkuste to znovu.", + "timeout": "Časový limit ověření vypršel. Zkuste to prosím znovu." + }, + "settings": { + "fingerprint": { + "label": "Kořenový otisk (SHA-256)" + }, + "provisioner": { + "label": "Název zřizovatele OIDC" + }, + "privateEndpoints": { + "label": "Povolení soukromí hostitelé Step CA", + "description": "Soukromí hostitelé, které může tok certifikátů Step CA kontaktovat: samotná CA a v případě interní certifikace také váš poskytovatel identity. Oddělte je čárkami." + } + } +} diff --git a/plugins/step-ca/locales/translated/da_DK.json b/plugins/step-ca/locales/translated/da_DK.json new file mode 100644 index 000000000..816f8c9ec --- /dev/null +++ b/plugins/step-ca/locales/translated/da_DK.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Trin CA" + }, + "editor": { + "label": "Trin CA", + "description": "Log ind via din identitetsudbyder, og lad din Step CA udstede et kortvarigt SSH-certifikat til denne vært. CA-URL'en, root-fingeraftrykket og OIDC-provisioneren indstilles af en administrator under administratorindstillinger.", + "docs": "Se dokumenter" + }, + "dialog": { + "openBrowser": "Åbn browseren for at godkende", + "waiting": "Venter på godkendelse i browseren...", + "authenticating": "Behandler godkendelse..." + }, + "errors": { + "authFailed": "Godkendelse mislykkedes. Tjek dine loginoplysninger, og prøv igen.", + "timeout": "Godkendelsen udløb. Prøv igen." + }, + "settings": { + "fingerprint": { + "label": "Rodfingeraftryk (SHA-256)" + }, + "provisioner": { + "label": "OIDC-provisioneringsnavn" + }, + "privateEndpoints": { + "label": "Tilladte private Step CA-værter", + "description": "Private værter, som Step CA-certifikatflowet kan kontakte: selve CA'en og, hvis den er intern, din identitetsudbyder. Adskil dem med kommaer." + } + } +} diff --git a/plugins/step-ca/locales/translated/de_DE.json b/plugins/step-ca/locales/translated/de_DE.json new file mode 100644 index 000000000..3523e7054 --- /dev/null +++ b/plugins/step-ca/locales/translated/de_DE.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Schritt CA" + }, + "editor": { + "label": "Schritt CA", + "description": "Melden Sie sich über Ihren Identitätsanbieter an und lassen Sie Ihre Step-Zertifizierungsstelle ein kurzlebiges SSH-Zertifikat für diesen Host ausstellen. Die URL der Zertifizierungsstelle, der Root-Fingerabdruck und der OIDC-Provisioner werden von einem Administrator unter „Admin-Einstellungen“ festgelegt.", + "docs": "Dokumentation anzeigen" + }, + "dialog": { + "openBrowser": "Browser zum Authentifizieren öffnen", + "waiting": "Warten auf Authentifizierung im Browser...", + "authenticating": "Authentifizierung wird verarbeitet..." + }, + "errors": { + "authFailed": "Authentifizierung fehlgeschlagen. Bitte überprüfen Sie Ihre Anmeldeinformationen und versuchen Sie es erneut.", + "timeout": "Authentifizierung abgelaufen. Bitte versuchen Sie es erneut." + }, + "settings": { + "fingerprint": { + "label": "Root-Fingerabdruck (SHA-256)" + }, + "provisioner": { + "label": "OIDC-Provisionername" + }, + "privateEndpoints": { + "label": "Zulässige private Step CA-Hosts", + "description": "Private Hosts, mit denen der Step CA-Zertifikatsfluss in Kontakt treten kann, kontaktieren möglicherweise die Zertifizierungsstelle selbst und, falls intern, Ihren Identitätsanbieter. Trennen Sie diese durch Kommas." + } + } +} diff --git a/plugins/step-ca/locales/translated/el_GR.json b/plugins/step-ca/locales/translated/el_GR.json new file mode 100644 index 000000000..41649a79b --- /dev/null +++ b/plugins/step-ca/locales/translated/el_GR.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Βήμα CA" + }, + "editor": { + "label": "Βήμα CA", + "description": "Συνδεθείτε μέσω του παρόχου ταυτότητας και αφήστε την Step CA σας να εκδώσει ένα βραχύβιο πιστοποιητικό SSH για αυτόν τον κεντρικό υπολογιστή. Η διεύθυνση URL CA, το δακτυλικό αποτύπωμα root και ο πάροχος OIDC ορίζονται από έναν διαχειριστή στις Ρυθμίσεις διαχειριστή.", + "docs": "Προβολή εγγράφων" + }, + "dialog": { + "openBrowser": "Άνοιγμα προγράμματος περιήγησης για έλεγχο ταυτότητας", + "waiting": "Αναμονή για έλεγχο ταυτότητας στο πρόγραμμα περιήγησης...", + "authenticating": "Επεξεργασία ελέγχου ταυτότητας..." + }, + "errors": { + "authFailed": "Ο έλεγχος ταυτότητας απέτυχε. Ελέγξτε τα διαπιστευτήριά σας και προσπαθήστε ξανά.", + "timeout": "Το χρονικό όριο ελέγχου ταυτότητας έληξε. Δοκιμάστε ξανά." + }, + "settings": { + "fingerprint": { + "label": "Δακτυλικό αποτύπωμα ρίζας (SHA-256)" + }, + "provisioner": { + "label": "Όνομα παρόχου OIDC" + }, + "privateEndpoints": { + "label": "Επιτρεπόμενοι ιδιωτικοί κεντρικοί υπολογιστές Step CA", + "description": "Οι ιδιωτικοί κεντρικοί υπολογιστές της ροής πιστοποιητικών Step CA ενδέχεται να επικοινωνήσουν με: την ίδια την CA και, εάν είναι εσωτερική, τον πάροχο ταυτότητάς σας. Διαχωρίστε τους με κόμματα." + } + } +} diff --git a/plugins/step-ca/locales/translated/es_ES.json b/plugins/step-ca/locales/translated/es_ES.json new file mode 100644 index 000000000..686e95c8f --- /dev/null +++ b/plugins/step-ca/locales/translated/es_ES.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Paso CA" + }, + "editor": { + "label": "Paso CA", + "description": "Inicie sesión a través de su proveedor de identidad y permita que su CA de Step emita un certificado SSH de corta duración para este host. La URL de la CA, la huella digital raíz y el proveedor OIDC se configuran mediante un administrador en la sección de Configuración de administración.", + "docs": "Ver documentación" + }, + "dialog": { + "openBrowser": "Abrir navegador para autenticarse", + "waiting": "Esperando autenticación en el navegador...", + "authenticating": "Procesando autenticación..." + }, + "errors": { + "authFailed": "La autenticación falló. Revisa tus credenciales e inténtalo de nuevo.", + "timeout": "La autenticación expiró. Inténtalo de nuevo." + }, + "settings": { + "fingerprint": { + "label": "Huella digital de la raíz (SHA-256)" + }, + "provisioner": { + "label": "nombre del proveedor OIDC" + }, + "privateEndpoints": { + "label": "Hosts de CA de paso privado permitidos", + "description": "Los hosts privados con los que puede contactar el flujo de certificados de Step CA son: la propia CA y, si es interna, su proveedor de identidad. Sepárelos con comas." + } + } +} diff --git a/plugins/step-ca/locales/translated/fi_FI.json b/plugins/step-ca/locales/translated/fi_FI.json new file mode 100644 index 000000000..b69d52532 --- /dev/null +++ b/plugins/step-ca/locales/translated/fi_FI.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Vaihe CA" + }, + "editor": { + "label": "Vaihe CA", + "description": "Kirjaudu sisään identiteetintarjoajasi kautta ja anna vaiheen varmentajan myöntää lyhytaikainen SSH-varmenne tälle isännälle. Varmentajan URL-osoitteen, pääkäyttäjän sormenjäljen ja OIDC-varmenteen määrittäjän määrittää järjestelmänvalvojan asetuksissa.", + "docs": "Näytä dokumentit" + }, + "dialog": { + "openBrowser": "Avaa selain todennusta varten", + "waiting": "Odotetaan selaimen todennusta...", + "authenticating": "Todennusta käsitellään..." + }, + "errors": { + "authFailed": "Todennus epäonnistui. Tarkista tunnistetietosi ja yritä uudelleen.", + "timeout": "Todennus aikakatkaistiin. Yritä uudelleen." + }, + "settings": { + "fingerprint": { + "label": "Juurisormenjälki (SHA-256)" + }, + "provisioner": { + "label": "OIDC-palveluntarjoajan nimi" + }, + "privateEndpoints": { + "label": "Sallitut yksityiset Step CA -isännät", + "description": "Yksityiset isännöitsijät, joilla Step CA -varmennetyönkulku voi ottaa yhteyttä: itse CA:han ja, jos kyseessä on sisäinen varmenne, identiteetintarjoajaasi. Erota ne pilkuilla." + } + } +} diff --git a/plugins/step-ca/locales/translated/fr_FR.json b/plugins/step-ca/locales/translated/fr_FR.json new file mode 100644 index 000000000..88a61f081 --- /dev/null +++ b/plugins/step-ca/locales/translated/fr_FR.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Étape CA" + }, + "editor": { + "label": "Étape CA", + "description": "Connectez-vous via votre fournisseur d'identité et laissez votre autorité de certification Step CA émettre un certificat SSH temporaire pour cet hôte. L'URL de l'autorité de certification, l'empreinte racine et le provisionneur OIDC sont configurés par un administrateur dans les paramètres d'administration.", + "docs": "Voir la documentation" + }, + "dialog": { + "openBrowser": "Ouvrir le navigateur pour s'authentifier", + "waiting": "En attente de l'authentification dans le navigateur...", + "authenticating": "Traitement de l'authentification..." + }, + "errors": { + "authFailed": "Échec de l'authentification. Veuillez vérifier vos identifiants et réessayer.", + "timeout": "L'authentification a expiré. Veuillez réessayer." + }, + "settings": { + "fingerprint": { + "label": "Empreinte racine (SHA-256)" + }, + "provisioner": { + "label": "Nom du fournisseur OIDC" + }, + "privateEndpoints": { + "label": "Hôtes d'autorité de certification privés autorisés", + "description": "Les hôtes privés pour lesquels le flux de certificats Step CA peut être sollicité peuvent contacter : l’autorité de certification elle-même et, si elle est interne, votre fournisseur d’identité. Séparez-les par des virgules." + } + } +} diff --git a/plugins/step-ca/locales/translated/he_IL.json b/plugins/step-ca/locales/translated/he_IL.json new file mode 100644 index 000000000..27545db14 --- /dev/null +++ b/plugins/step-ca/locales/translated/he_IL.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "שלב CA" + }, + "editor": { + "label": "שלב CA", + "description": "היכנס דרך ספק הזהויות שלך ותן ל-Step CA שלך להנפיק אישור SSH קצר מועד עבור מארח זה. כתובת ה-URL של CA, טביעת האצבע של הבסיס וספק ה-OIDC מוגדרים על ידי מנהל מערכת תחת הגדרות מנהל מערכת.", + "docs": "הצג מסמכים" + }, + "dialog": { + "openBrowser": "פתח את הדפדפן כדי לאמת", + "waiting": "ממתין לאימות בדפדפן...", + "authenticating": "מעבד אימות..." + }, + "errors": { + "authFailed": "האימות נכשל. אנא בדוק את פרטי הגישה שלך ונסה שוב.", + "timeout": "הזמן שהוקצב לאימות הסתיים. אנא נסה שוב." + }, + "settings": { + "fingerprint": { + "label": "טביעת אצבע של שורש (SHA-256)" + }, + "provisioner": { + "label": "שם ספק OIDC" + }, + "privateEndpoints": { + "label": "מארחים פרטיים של Step CA מורשים", + "description": "מארחים פרטיים, זרימת האישורים של Step CA עשויה ליצור קשר עם: רשות האישורים עצמה, ואם היא פנימית, עם ספק הזהויות שלך. הפרידו ביניהם באמצעות פסיקים." + } + } +} diff --git a/plugins/step-ca/locales/translated/hi_IN.json b/plugins/step-ca/locales/translated/hi_IN.json new file mode 100644 index 000000000..e3f9804fd --- /dev/null +++ b/plugins/step-ca/locales/translated/hi_IN.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "स्टेप सीए" + }, + "editor": { + "label": "स्टेप सीए", + "description": "अपने आइडेंटिटी प्रोवाइडर के माध्यम से साइन इन करें और अपने स्टेप सीए को इस होस्ट के लिए एक अल्पकालिक एसएसएच प्रमाणपत्र जारी करने दें। सीए यूआरएल, रूट फिंगरप्रिंट और ओआईडीसी प्रोविजनर को एडमिनिस्ट्रेटर द्वारा एडमिन सेटिंग्स के अंतर्गत सेट किया जाता है।", + "docs": "दस्तावेज़ देखें" + }, + "dialog": { + "openBrowser": "प्रमाणीकरण के लिए ब्राउज़र खोलें", + "waiting": "ब्राउज़र में प्रमाणीकरण की प्रतीक्षा हो रही है...", + "authenticating": "प्रमाणीकरण प्रक्रिया में है..." + }, + "errors": { + "authFailed": "प्रमाणीकरण विफल। कृपया अपने क्रेडेंशियल जाँचें और पुनः प्रयास करें।", + "timeout": "प्रमाणीकरण का समय समाप्त हो गया। कृपया पुनः प्रयास करें।" + }, + "settings": { + "fingerprint": { + "label": "रूट फिंगरप्रिंट (SHA-256)" + }, + "provisioner": { + "label": "OIDC प्रोविज़नर नाम" + }, + "privateEndpoints": { + "label": "निजी स्टेप सीए होस्ट की अनुमति है", + "description": "निजी होस्ट, स्टेप सीए प्रमाणपत्र प्रवाह के दौरान निम्नलिखित से संपर्क कर सकते हैं: सीए स्वयं और, यदि आंतरिक है, तो आपका पहचान प्रदाता। इन्हें अल्पविराम से अलग करें।" + } + } +} diff --git a/plugins/step-ca/locales/translated/hu_HU.json b/plugins/step-ca/locales/translated/hu_HU.json new file mode 100644 index 000000000..18e49efc5 --- /dev/null +++ b/plugins/step-ca/locales/translated/hu_HU.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "CA lépés" + }, + "editor": { + "label": "CA lépés", + "description": "Jelentkezzen be az identitásszolgáltatóján keresztül, és hagyja, hogy a lépéses hitelesítésszolgáltatója rövid életű SSH-tanúsítványt állítson ki ehhez a gazdagéphez. A hitelesítésszolgáltató URL-címét, a root ujjlenyomatát és az OIDC-kiszolgálót a rendszergazda állítja be az Adminisztrátori beállítások alatt.", + "docs": "Dokumentumok megtekintése" + }, + "dialog": { + "openBrowser": "Böngésző megnyitása a hitelesítéshez", + "waiting": "Várakozás a böngésző hitelesítésére...", + "authenticating": "Hitelesítés feldolgozása..." + }, + "errors": { + "authFailed": "Sikertelen hitelesítés. Kérjük, ellenőrizze a hitelesítő adatait, és próbálja újra.", + "timeout": "A hitelesítés időtúllépést okozott. Kérjük, próbálja újra." + }, + "settings": { + "fingerprint": { + "label": "Gyökér ujjlenyomat (SHA-256)" + }, + "provisioner": { + "label": "OIDC-szolgáltató neve" + }, + "privateEndpoints": { + "label": "Engedélyezett privát Step CA-hosztok", + "description": "A Step CA tanúsítványfolyamat által érintett privát hosztok a következőkkel vehetik fel a kapcsolatot: maga a CA, és ha belső, akkor az identitásszolgáltató. Vesszővel válassza el őket." + } + } +} diff --git a/plugins/step-ca/locales/translated/id_ID.json b/plugins/step-ca/locales/translated/id_ID.json new file mode 100644 index 000000000..d05081a6f --- /dev/null +++ b/plugins/step-ca/locales/translated/id_ID.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Langkah CA" + }, + "editor": { + "label": "Langkah CA", + "description": "Masuk melalui penyedia identitas Anda dan biarkan Step CA Anda menerbitkan sertifikat SSH berumur pendek untuk host ini. URL CA, sidik jari root, dan penyedia OIDC diatur oleh administrator di bawah Pengaturan Admin.", + "docs": "Lihat dokumen" + }, + "dialog": { + "openBrowser": "Buka Browser untuk Melakukan Otentikasi", + "waiting": "Menunggu otentikasi di browser...", + "authenticating": "Memproses otentikasi..." + }, + "errors": { + "authFailed": "Autentikasi gagal. Harap periksa kredensial Anda dan coba lagi.", + "timeout": "Autentikasi habis waktu. Silakan coba lagi." + }, + "settings": { + "fingerprint": { + "label": "Sidik jari akar (SHA-256)" + }, + "provisioner": { + "label": "Nama penyedia OIDC" + }, + "privateEndpoints": { + "label": "Host CA Langkah pribadi yang diizinkan", + "description": "Host privat yang dapat dihubungi oleh alur sertifikat Step CA adalah: CA itu sendiri dan, jika internal, penyedia identitas Anda. Pisahkan mereka dengan koma." + } + } +} diff --git a/plugins/step-ca/locales/translated/it_IT.json b/plugins/step-ca/locales/translated/it_IT.json new file mode 100644 index 000000000..59704efd5 --- /dev/null +++ b/plugins/step-ca/locales/translated/it_IT.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Passo CA" + }, + "editor": { + "label": "Passo CA", + "description": "Accedi tramite il tuo provider di identità e lascia che la tua CA Step emetta un certificato SSH temporaneo per questo host. L'URL della CA, l'impronta digitale della radice e il provisioner OIDC vengono impostati da un amministratore nelle Impostazioni di amministrazione.", + "docs": "Visualizza documentazione" + }, + "dialog": { + "openBrowser": "Apri il browser per autenticarti", + "waiting": "In attesa dell'autenticazione nel browser...", + "authenticating": "Autenticazione in corso..." + }, + "errors": { + "authFailed": "Autenticazione fallita. Controlla le credenziali e riprova.", + "timeout": "Autenticazione scaduta. Riprova." + }, + "settings": { + "fingerprint": { + "label": "Impronta digitale radice (SHA-256)" + }, + "provisioner": { + "label": "Nome del fornitore OIDC" + }, + "privateEndpoints": { + "label": "Host Step CA privati consentiti", + "description": "Gli host privati che utilizzano il flusso di certificati Step CA potrebbero contattare: la CA stessa e, se interna, il provider di identità. Separarli con delle virgole." + } + } +} diff --git a/plugins/step-ca/locales/translated/ja_JP.json b/plugins/step-ca/locales/translated/ja_JP.json new file mode 100644 index 000000000..0336a947d --- /dev/null +++ b/plugins/step-ca/locales/translated/ja_JP.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "ステップCA" + }, + "editor": { + "label": "ステップCA", + "description": "IDプロバイダー経由でサインインし、Step CAにこのホスト用の有効期限の短いSSH証明書を発行させてください。CAのURL、ルートフィンガープリント、およびOIDCプロビジョナーは、管理者が「管理設定」で設定します。", + "docs": "ドキュメントを表示" + }, + "dialog": { + "openBrowser": "ブラウザを開いて認証", + "waiting": "ブラウザでの認証を待機中...", + "authenticating": "認証を処理中..." + }, + "errors": { + "authFailed": "認証に失敗しました。資格情報を確認して再試行してください。", + "timeout": "認証がタイムアウトしました。もう一度お試しください。" + }, + "settings": { + "fingerprint": { + "label": "ルートフィンガープリント(SHA-256)" + }, + "provisioner": { + "label": "OIDCプロビジョナー名" + }, + "privateEndpoints": { + "label": "許可されたプライベートStep CAホスト", + "description": "プライベートホストの場合、Step CA 証明書フローは、認証局自体、および内部の場合は ID プロバイダーと連絡を取ることができます。これらはカンマで区切ってください。" + } + } +} diff --git a/plugins/step-ca/locales/translated/ko_KR.json b/plugins/step-ca/locales/translated/ko_KR.json new file mode 100644 index 000000000..21bbef8ae --- /dev/null +++ b/plugins/step-ca/locales/translated/ko_KR.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "스텝 CA" + }, + "editor": { + "label": "스텝 CA", + "description": "ID 공급자를 통해 로그인하고 Step CA에서 이 호스트에 대한 단기 SSH 인증서를 발급받으세요. CA URL, 루트 지문 및 OIDC 프로비저너는 관리자가 관리자 설정에서 설정합니다.", + "docs": "문서 보기" + }, + "dialog": { + "openBrowser": "브라우저로 인증하기", + "waiting": "브라우저에서 인증 대기 중...", + "authenticating": "인증 처리 중..." + }, + "errors": { + "authFailed": "인증에 실패했습니다. 자격 증명을 확인하고 다시 시도하세요.", + "timeout": "인증 시간이 초과되었습니다. 다시 시도하세요." + }, + "settings": { + "fingerprint": { + "label": "루트 지문(SHA-256)" + }, + "provisioner": { + "label": "OIDC 프로비저너 이름" + }, + "privateEndpoints": { + "label": "허용된 개인 Step CA 호스트", + "description": "Step CA 인증서 흐름이 접촉할 수 있는 개인 호스트는 CA 자체와 (내부 호스트인 경우) ID 공급자입니다. 이들을 쉼표로 구분하세요." + } + } +} diff --git a/plugins/step-ca/locales/translated/nl_NL.json b/plugins/step-ca/locales/translated/nl_NL.json new file mode 100644 index 000000000..ece2e42fe --- /dev/null +++ b/plugins/step-ca/locales/translated/nl_NL.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Stap CA" + }, + "editor": { + "label": "Stap CA", + "description": "Meld u aan via uw identiteitsprovider en laat uw Step CA een kortstondig SSH-certificaat voor deze host uitgeven. De CA-URL, root-fingerprint en OIDC-provisioner worden door een beheerder ingesteld onder Beheerinstellingen.", + "docs": "Documenten bekijken" + }, + "dialog": { + "openBrowser": "Open de browser om te authenticeren.", + "waiting": "Wachten op authenticatie in de browser...", + "authenticating": "Authenticatie wordt verwerkt..." + }, + "errors": { + "authFailed": "Authenticatie mislukt. Controleer uw inloggegevens en probeer het opnieuw.", + "timeout": "De authenticatie is mislukt vanwege een time-out. Probeer het opnieuw." + }, + "settings": { + "fingerprint": { + "label": "Root-vingerafdruk (SHA-256)" + }, + "provisioner": { + "label": "Naam van de OIDC-leverancier" + }, + "privateEndpoints": { + "label": "Toegestane privé Step CA-hosts", + "description": "De private hosts waarmee de Step CA-certificaatstroom contact kan opnemen, zijn: de certificeringsinstantie zelf en, indien intern, uw identiteitsprovider. Scheid deze met komma's." + } + } +} diff --git a/plugins/step-ca/locales/translated/no_NO.json b/plugins/step-ca/locales/translated/no_NO.json new file mode 100644 index 000000000..fbc6753a6 --- /dev/null +++ b/plugins/step-ca/locales/translated/no_NO.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Trinn CA" + }, + "editor": { + "label": "Trinn CA", + "description": "Logg på via identitetsleverandøren din, og la Step CA utstede et kortvarig SSH-sertifikat for denne verten. CA-URL-en, rotfingeravtrykket og OIDC-provisioneren angis av en administrator under Administratorinnstillinger.", + "docs": "Vis dokumenter" + }, + "dialog": { + "openBrowser": "Åpne nettleseren for å autentisere", + "waiting": "Venter på autentisering i nettleseren...", + "authenticating": "Behandler autentisering ..." + }, + "errors": { + "authFailed": "Autentiseringen mislyktes. Sjekk legitimasjonen din og prøv på nytt.", + "timeout": "Autentiseringen ble tidsavbrutt. Prøv på nytt." + }, + "settings": { + "fingerprint": { + "label": "Rotfingeravtrykk (SHA-256)" + }, + "provisioner": { + "label": "OIDC-provisionernavn" + }, + "privateEndpoints": { + "label": "Tillatte private Step CA-verter", + "description": "Private verter som Step CA-sertifikatflyten kan kontakte: selve CA-en og, hvis intern, identitetsleverandøren din. Skill dem med komma." + } + } +} diff --git a/plugins/step-ca/locales/translated/pl_PL.json b/plugins/step-ca/locales/translated/pl_PL.json new file mode 100644 index 000000000..80b7b7a5f --- /dev/null +++ b/plugins/step-ca/locales/translated/pl_PL.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Krok CA" + }, + "editor": { + "label": "Krok CA", + "description": "Zaloguj się za pomocą swojego dostawcy tożsamości i pozwól, aby urząd certyfikacji Step CA wydał tymczasowy certyfikat SSH dla tego hosta. Adres URL urzędu certyfikacji, odcisk palca głównego i dostawca OIDC są ustawiane przez administratora w Ustawieniach administratora.", + "docs": "Zobacz dokumentację" + }, + "dialog": { + "openBrowser": "Otwórz przeglądarkę, aby uwierzytelnić", + "waiting": "Oczekiwanie na uwierzytelnienie w przeglądarce...", + "authenticating": "Przetwarzanie uwierzytelniania..." + }, + "errors": { + "authFailed": "Uwierzytelnienie nie powiodło się. Sprawdź poświadczenia i spróbuj ponownie.", + "timeout": "Upłynął limit czasu uwierzytelniania. Spróbuj ponownie." + }, + "settings": { + "fingerprint": { + "label": "Odcisk palca głównego (SHA-256)" + }, + "provisioner": { + "label": "Nazwa dostawcy OIDC" + }, + "privateEndpoints": { + "label": "Dozwolone prywatne hosty Step CA", + "description": "Prywatni hosty, z których przepływ certyfikatów Step CA może się kontaktować: z samym CA oraz, jeśli jest wewnętrzny, z Twoim dostawcą tożsamości. Rozdziel je przecinkami." + } + } +} diff --git a/plugins/step-ca/locales/translated/pt_BR.json b/plugins/step-ca/locales/translated/pt_BR.json new file mode 100644 index 000000000..18f962845 --- /dev/null +++ b/plugins/step-ca/locales/translated/pt_BR.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Etapa CA" + }, + "editor": { + "label": "Etapa CA", + "description": "Faça login através do seu provedor de identidade e permita que a sua Autoridade Certificadora (CA) Step emita um certificado SSH de curta duração para este host. O URL da CA, a impressão digital da raiz e o provisionador OIDC são definidos por um administrador em Configurações de Administrador.", + "docs": "Ver documentos" + }, + "dialog": { + "openBrowser": "Abra o navegador para autenticar.", + "waiting": "Aguardando autenticação no navegador...", + "authenticating": "Processando autenticação..." + }, + "errors": { + "authFailed": "A autenticação falhou. Verifique suas credenciais e tente novamente.", + "timeout": "A autenticação expirou. Tente novamente." + }, + "settings": { + "fingerprint": { + "label": "Impressão digital raiz (SHA-256)" + }, + "provisioner": { + "label": "nome do provisionador OIDC" + }, + "privateEndpoints": { + "label": "Hosts CA privados permitidos", + "description": "Para hosts privados, o fluxo de certificados da Step CA pode contatar: a própria CA e, se interna, seu provedor de identidade. Separe-os com vírgulas." + } + } +} diff --git a/plugins/step-ca/locales/translated/pt_PT.json b/plugins/step-ca/locales/translated/pt_PT.json new file mode 100644 index 000000000..b42b91917 --- /dev/null +++ b/plugins/step-ca/locales/translated/pt_PT.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Etapa CA" + }, + "editor": { + "label": "Etapa CA", + "description": "Faça login através do seu fornecedor de identidade e permita que a sua Autoridade Certificadora (CA) Step emita um certificado SSH de curta duração para este host. O URL da AC, a impressão digital da raiz e o provisionador OIDC são definidos por um administrador em Definições de Administrador.", + "docs": "Ver documentação" + }, + "dialog": { + "openBrowser": "Abrir navegador para autenticar", + "waiting": "A aguardar autenticação no navegador...", + "authenticating": "A processar autenticação..." + }, + "errors": { + "authFailed": "A autenticação falhou. Verifique as suas credenciais e tente novamente.", + "timeout": "A autenticação expirou. Tente novamente." + }, + "settings": { + "fingerprint": { + "label": "Impressão digital de raiz (SHA-256)" + }, + "provisioner": { + "label": "nome do provisionador OIDC" + }, + "privateEndpoints": { + "label": "Hosts CA privados permitidos", + "description": "Para hosts privados, o fluxo de certificados da Step CA pode contactar: a própria CA e, se for interna, o seu fornecedor de identidade. Separe-os com vírgulas." + } + } +} diff --git a/plugins/step-ca/locales/translated/ro_RO.json b/plugins/step-ca/locales/translated/ro_RO.json new file mode 100644 index 000000000..ae7d31583 --- /dev/null +++ b/plugins/step-ca/locales/translated/ro_RO.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Pasul CA" + }, + "editor": { + "label": "Pasul CA", + "description": "Conectați-vă prin intermediul furnizorului de identitate și permiteți autorității de certificare Step CA să emită un certificat SSH de scurtă durată pentru această gazdă. Adresa URL a autorității de certificare, amprenta rădăcină și furnizorul OIDC sunt setate de un administrator în Setări administrator.", + "docs": "Vizualizați documentele" + }, + "dialog": { + "openBrowser": "Deschideți browserul pentru autentificare", + "waiting": "Se așteaptă autentificarea în browser...", + "authenticating": "Se procesează autentificarea..." + }, + "errors": { + "authFailed": "Autentificarea a eșuat. Verificați acreditările și încercați din nou.", + "timeout": "Autentificarea a expirat. Vă rugăm să încercați din nou." + }, + "settings": { + "fingerprint": { + "label": "Amprentă digitală rădăcină (SHA-256)" + }, + "provisioner": { + "label": "Numele furnizorului OIDC" + }, + "privateEndpoints": { + "label": "Gazde private permise de Step CA", + "description": "Gazde private pe care fluxul de certificate Step CA le poate contacta: CA-ul în sine și, dacă este intern, furnizorul dvs. de identitate. Separați-le prin virgule." + } + } +} diff --git a/plugins/step-ca/locales/translated/ru_RU.json b/plugins/step-ca/locales/translated/ru_RU.json new file mode 100644 index 000000000..0762d5b2e --- /dev/null +++ b/plugins/step-ca/locales/translated/ru_RU.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Шаг CA" + }, + "editor": { + "label": "Шаг CA", + "description": "Войдите в систему через своего поставщика идентификации и позвольте вашему центру сертификации Step CA выдать краткосрочный SSH-сертификат для этого хоста. URL-адрес центра сертификации, отпечаток корневого сертификата и средство подготовки OIDC задаются администратором в разделе «Настройки администратора».", + "docs": "Документация" + }, + "dialog": { + "openBrowser": "Открыть браузер для аутентификации", + "waiting": "Ожидание аутентификации в браузере...", + "authenticating": "Обработка аутентификации..." + }, + "errors": { + "authFailed": "Аутентификация не удалась. Проверьте учётные данные и попробуйте снова.", + "timeout": "Время аутентификации истекло. Попробуйте снова." + }, + "settings": { + "fingerprint": { + "label": "Отпечаток корневого ключа (SHA-256)" + }, + "provisioner": { + "label": "имя поставщика OIDC" + }, + "privateEndpoints": { + "label": "Разрешены частные хосты Step CA", + "description": "Частные хосты, для которых выполняется процедура сертификации Step CA, могут связаться с: самим центром сертификации и, если он внутренний, с вашим поставщиком идентификации. Разделяйте их запятыми." + } + } +} diff --git a/plugins/step-ca/locales/translated/sr_SP.json b/plugins/step-ca/locales/translated/sr_SP.json new file mode 100644 index 000000000..06649d65d --- /dev/null +++ b/plugins/step-ca/locales/translated/sr_SP.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Корак CA" + }, + "editor": { + "label": "Корак CA", + "description": "Пријавите се преко свог добављача идентитета и дозволите свом Step CA да изда краткотрајни SSH сертификат за овај хост. CA URL, root отисак прста и OIDC provisioner подешава администратор у подешавањима администратора.", + "docs": "Прикажи документе" + }, + "dialog": { + "openBrowser": "Отворите прегледач за аутентификацију", + "waiting": "Чекање аутентификације у прегледачу...", + "authenticating": "Обрада аутентификације..." + }, + "errors": { + "authFailed": "Аутентификација није успела. Проверите своје акредитиве и покушајте поново.", + "timeout": "Временско ограничење за аутентификацију је истекло. Молимо покушајте поново." + }, + "settings": { + "fingerprint": { + "label": "Отисак коренског идентификатора (SHA-256)" + }, + "provisioner": { + "label": "Име OIDC провизора" + }, + "privateEndpoints": { + "label": "Дозвољени приватни хостови Step CA", + "description": "Приватни хостови које ток сертификата Step CA може контактирати: сам CA и, ако је интерно, ваш добављач идентитета. Раздвојите их зарезима." + } + } +} diff --git a/plugins/step-ca/locales/translated/sv_SE.json b/plugins/step-ca/locales/translated/sv_SE.json new file mode 100644 index 000000000..aad2ab60f --- /dev/null +++ b/plugins/step-ca/locales/translated/sv_SE.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Steg CA" + }, + "editor": { + "label": "Steg CA", + "description": "Logga in via din identitetsleverantör och låt din Step CA utfärda ett kortlivat SSH-certifikat för den här värden. CA-URL:en, rotfingeravtrycket och OIDC-provisionern ställs in av en administratör under administratörsinställningar.", + "docs": "Visa dokument" + }, + "dialog": { + "openBrowser": "Öppna webbläsaren för att autentisera", + "waiting": "Väntar på autentisering i webbläsaren...", + "authenticating": "Bearbetar autentisering..." + }, + "errors": { + "authFailed": "Autentiseringen misslyckades. Kontrollera dina inloggningsuppgifter och försök igen.", + "timeout": "Autentiseringen har gått ut. Försök igen." + }, + "settings": { + "fingerprint": { + "label": "Rotfingeravtryck (SHA-256)" + }, + "provisioner": { + "label": "OIDC-provisioneringsnamn" + }, + "privateEndpoints": { + "label": "Tillåtna privata Step CA-värdar", + "description": "Privata värdar som Step CA-certifikatflödet kan kontakta: själva CA:n och, om den är intern, din identitetsleverantör. Separera dem med kommatecken." + } + } +} diff --git a/plugins/step-ca/locales/translated/th_TH.json b/plugins/step-ca/locales/translated/th_TH.json new file mode 100644 index 000000000..de7b48d5a --- /dev/null +++ b/plugins/step-ca/locales/translated/th_TH.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "ขั้นตอน CA" + }, + "editor": { + "label": "ขั้นตอน CA", + "description": "ลงชื่อเข้าใช้ผ่านผู้ให้บริการยืนยันตัวตนของคุณ และให้ Step CA ออกใบรับรอง SSH ที่มีอายุสั้นสำหรับโฮสต์นี้ URL ของ CA, ลายนิ้วมือรูท และตัวจัดเตรียม OIDC จะถูกตั้งค่าโดยผู้ดูแลระบบภายใต้การตั้งค่าผู้ดูแลระบบ", + "docs": "ดูเอกสาร" + }, + "dialog": { + "openBrowser": "เปิดเบราว์เซอร์เพื่อยืนยันตัวตน", + "waiting": "กำลังรอการยืนยันตัวตนในเบราว์เซอร์...", + "authenticating": "กำลังตรวจสอบสิทธิ์..." + }, + "errors": { + "authFailed": "การตรวจสอบสิทธิ์ล้มเหลว โปรดตรวจสอบข้อมูลประจำตัวของคุณและลองอีกครั้ง", + "timeout": "การตรวจสอบสิทธิ์หมดเวลา โปรดลองอีกครั้ง" + }, + "settings": { + "fingerprint": { + "label": "ลายนิ้วมือราก (SHA-256)" + }, + "provisioner": { + "label": "ชื่อผู้จัดเตรียม OIDC" + }, + "privateEndpoints": { + "label": "อนุญาตให้โฮสต์ Step CA ส่วนตัว", + "description": "สำหรับโฮสต์ส่วนตัว กระบวนการขอใบรับรอง Step CA อาจติดต่อกับ: CA เอง และหากเป็นโฮสต์ภายใน ก็อาจติดต่อกับผู้ให้บริการยืนยันตัวตนของคุณด้วย คั่นด้วยเครื่องหมายจุลภาค" + } + } +} diff --git a/plugins/step-ca/locales/translated/tr_TR.json b/plugins/step-ca/locales/translated/tr_TR.json new file mode 100644 index 000000000..636191cec --- /dev/null +++ b/plugins/step-ca/locales/translated/tr_TR.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Adım CA" + }, + "editor": { + "label": "Adım CA", + "description": "Kimlik sağlayıcınız aracılığıyla oturum açın ve Step CA'nızın bu sunucu için kısa süreli bir SSH sertifikası düzenlemesine izin verin. CA URL'si, kök parmak izi ve OIDC sağlayıcısı, yönetici tarafından Yönetici Ayarları altında belirlenir.", + "docs": "Dokümanları görüntüle" + }, + "dialog": { + "openBrowser": "Kimlik Doğrulaması için Tarayıcı Aç", + "waiting": "Tarayıcıda kimlik doğrulaması bekleniyor...", + "authenticating": "Kimlik doğrulaması işleniyor..." + }, + "errors": { + "authFailed": "Kimlik doğrulaması başarısız oldu. Lütfen kimlik bilgilerinizi kontrol edip tekrar deneyin.", + "timeout": "Kimlik doğrulaması zaman aşımına uğradı. Lütfen tekrar deneyin." + }, + "settings": { + "fingerprint": { + "label": "Kök parmak izi (SHA-256)" + }, + "provisioner": { + "label": "OIDC tedarikçi adı" + }, + "privateEndpoints": { + "label": "Özel Step CA sunucularına izin verildi.", + "description": "Özel sunucular, Step CA sertifika akışı kapsamında şu kuruluşlarla iletişime geçebilir: CA'nın kendisi ve (dahili ise) kimlik sağlayıcınız. Bunları virgülle ayırın." + } + } +} diff --git a/plugins/step-ca/locales/translated/uk_UA.json b/plugins/step-ca/locales/translated/uk_UA.json new file mode 100644 index 000000000..767ac8ec6 --- /dev/null +++ b/plugins/step-ca/locales/translated/uk_UA.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Крок CA" + }, + "editor": { + "label": "Крок CA", + "description": "Увійдіть через свого постачальника ідентифікаційних даних і дозвольте вашому центру сертифікації Step CA видати короткочасний SSH-сертифікат для цього хоста. URL-адресу центру сертифікації, кореневий відбиток і OIDC-провайдер встановлює адміністратор у розділі «Налаштування адміністратора».", + "docs": "Переглянути документи" + }, + "dialog": { + "openBrowser": "Відкрийте браузер для автентифікації", + "waiting": "Очікування автентифікації у браузері...", + "authenticating": "Обробка автентифікації..." + }, + "errors": { + "authFailed": "Помилка автентифікації. Перевірте свої облікові дані та спробуйте ще раз.", + "timeout": "Час очікування автентифікації минув. Спробуйте ще раз." + }, + "settings": { + "fingerprint": { + "label": "Кореневий відбиток (SHA-256)" + }, + "provisioner": { + "label": "Ім'я постачальника OIDC" + }, + "privateEndpoints": { + "label": "Дозволені приватні хости Step CA", + "description": "Приватні хости, з якими може зв’язатися потік сертифікатів Step CA: сам CA та, якщо внутрішній, ваш постачальник ідентифікаційних даних. Розділяйте їх комами." + } + } +} diff --git a/plugins/step-ca/locales/translated/vi_VN.json b/plugins/step-ca/locales/translated/vi_VN.json new file mode 100644 index 000000000..05987279b --- /dev/null +++ b/plugins/step-ca/locales/translated/vi_VN.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "Bước CA" + }, + "editor": { + "label": "Bước CA", + "description": "Đăng nhập thông qua nhà cung cấp danh tính của bạn và cho phép Step CA cấp chứng chỉ SSH có thời hạn ngắn cho máy chủ này. URL CA, dấu vân tay gốc và trình cung cấp OIDC được quản trị viên thiết lập trong Cài đặt quản trị.", + "docs": "Xem tài liệu" + }, + "dialog": { + "openBrowser": "Mở trình duyệt để xác thực", + "waiting": "Đang chờ xác thực trên trình duyệt...", + "authenticating": "Đang xử lý xác thực..." + }, + "errors": { + "authFailed": "Xác thực thất bại. Vui lòng kiểm tra thông tin đăng nhập và thử lại.", + "timeout": "Xác thực đã hết thời gian chờ. Vui lòng thử lại." + }, + "settings": { + "fingerprint": { + "label": "Dấu vân tay gốc (SHA-256)" + }, + "provisioner": { + "label": "Tên trình cung cấp OIDC" + }, + "privateEndpoints": { + "label": "Cho phép máy chủ Step CA riêng tư", + "description": "Các máy chủ riêng mà quy trình chứng chỉ CA từng bước có thể liên hệ: chính CA và, nếu là máy chủ nội bộ, nhà cung cấp danh tính của bạn. Phân tách chúng bằng dấu phẩy." + } + } +} diff --git a/plugins/step-ca/locales/translated/zh_CN.json b/plugins/step-ca/locales/translated/zh_CN.json new file mode 100644 index 000000000..7ea6ec6b9 --- /dev/null +++ b/plugins/step-ca/locales/translated/zh_CN.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "步骤 CA" + }, + "editor": { + "label": "步骤 CA", + "description": "通过您的身份提供商登录,并让您的 Step CA 为此主机颁发一个短期 SSH 证书。CA URL、根指纹和 OIDC 配置器由管理员在“管理设置”中设置。", + "docs": "查看文档" + }, + "dialog": { + "openBrowser": "打开浏览器进行认证", + "waiting": "等待浏览器认证...", + "authenticating": "正在处理认证..." + }, + "errors": { + "authFailed": "认证失败,请检查凭据并重试。", + "timeout": "认证超时,请重试。" + }, + "settings": { + "fingerprint": { + "label": "根指纹(SHA-256)" + }, + "provisioner": { + "label": "OIDC配置器名称" + }, + "privateEndpoints": { + "label": "允许的私有步骤 CA 主机", + "description": "私有主机在执行步骤 CA 证书流程时可能会联系:CA 本身,以及(如果是内部)您的身份提供商。请用逗号分隔它们。" + } + } +} diff --git a/plugins/step-ca/locales/translated/zh_TW.json b/plugins/step-ca/locales/translated/zh_TW.json new file mode 100644 index 000000000..5f5cba9a5 --- /dev/null +++ b/plugins/step-ca/locales/translated/zh_TW.json @@ -0,0 +1,31 @@ +{ + "hosts": { + "filterAuthStepca": "步驟 CA" + }, + "editor": { + "label": "步驟 CA", + "description": "透過您的身分提供者登錄,並讓您的 Step CA 為此主機頒發短期 SSH 憑證。 CA URL、根指紋和 OIDC 配置器由管理員在「管理設定」中設定。", + "docs": "檢視文件" + }, + "dialog": { + "openBrowser": "開啟瀏覽器以進行驗證", + "waiting": "正在等待瀏覽器中的驗證…", + "authenticating": "正在處理驗證…" + }, + "errors": { + "authFailed": "驗證失敗。請檢查您的憑證並再試一次。", + "timeout": "驗證逾時。請再試一次。" + }, + "settings": { + "fingerprint": { + "label": "根指紋(SHA-256)" + }, + "provisioner": { + "label": "OIDC配置器名稱" + }, + "privateEndpoints": { + "label": "允許的私有步驟 CA 主機", + "description": "私有主機在執行步驟 CA 憑證流程時可能會聯絡:CA 本身,以及(如果是內部)您的身分提供者。請用逗號分隔它們。" + } + } +} diff --git a/plugins/step-ca/manifest.json b/plugins/step-ca/manifest.json new file mode 100644 index 000000000..3c1d92be7 --- /dev/null +++ b/plugins/step-ca/manifest.json @@ -0,0 +1,77 @@ +{ + "id": "step-ca", + "name": "Step CA", + "version": "1.0.0", + "description": "Sign in to hosts with short-lived SSH certificates from a smallstep step-ca server, using its OIDC provisioner in the browser. Termix calls the CA's HTTPS API directly and never runs the step binary.", + "author": { + "name": "Termix" + }, + "license": "MIT", + "repository": "https://github.com/Termix-SSH/Termix", + "category": "Access & Security", + "icon": "ShieldCheck", + "engine": { + "termix": ">=2.9.0", + "api": "1" + }, + "capabilities": [ + "db:own", + "secrets:own", + "network:outbound", + "network:serve", + "auth:provide", + "ui:surface" + ], + "contributes": { + "auth": { + "sshAuthTypes": ["stepca"] + }, + "settings": { + "admin": [ + { + "key": "caUrl", + "type": "string", + "labelKey": "settings.caUrl.label", + "descriptionKey": "settings.caUrl.description", + "placeholderKey": "settings.caUrl.placeholder", + "default": "" + }, + { + "key": "fingerprint", + "type": "string", + "labelKey": "settings.fingerprint.label", + "descriptionKey": "settings.fingerprint.description", + "default": "" + }, + { + "key": "provisioner", + "type": "string", + "labelKey": "settings.provisioner.label", + "descriptionKey": "settings.provisioner.description", + "default": "" + }, + { + "key": "privateEndpoints", + "type": "textarea", + "labelKey": "settings.privateEndpoints.label", + "descriptionKey": "settings.privateEndpoints.description", + "default": "" + }, + { + "key": "redirectUri", + "type": "custom", + "component": "redirectUri" + }, + { + "key": "legacyCallback", + "type": "boolean", + "labelKey": "settings.legacyCallback.label", + "descriptionKey": "settings.legacyCallback.description", + "default": false + } + ] + } + }, + "locales": "locales", + "platforms": ["linux", "win32", "darwin"] +} diff --git a/plugins/step-ca/migrations/mysql/0001_create_certs.sql b/plugins/step-ca/migrations/mysql/0001_create_certs.sql new file mode 100644 index 000000000..f37e8e92c --- /dev/null +++ b/plugins/step-ca/migrations/mysql/0001_create_certs.sql @@ -0,0 +1,16 @@ +-- step-ca 0001: create_certs +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS `p_step_ca_certs` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `user_id` varchar(255) NOT NULL, + `host_id` int NOT NULL, + `ssh_cert` text NOT NULL, + `private_key` text NOT NULL, + `email` text, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `expires_at` varchar(255) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data` (`id`) ON DELETE CASCADE +); +CREATE UNIQUE INDEX `idx_step_ca_certs_user_host` ON `p_step_ca_certs` (`user_id`, `host_id`); diff --git a/plugins/step-ca/migrations/postgres/0001_create_certs.sql b/plugins/step-ca/migrations/postgres/0001_create_certs.sql new file mode 100644 index 000000000..5b3e48c03 --- /dev/null +++ b/plugins/step-ca/migrations/postgres/0001_create_certs.sql @@ -0,0 +1,16 @@ +-- step-ca 0001: create_certs +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS "p_step_ca_certs" ( + "id" serial PRIMARY KEY, + "user_id" varchar(255) NOT NULL, + "host_id" integer NOT NULL, + "ssh_cert" text NOT NULL, + "private_key" text NOT NULL, + "email" text, + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expires_at" varchar(255) NOT NULL, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE, + FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE +); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_step_ca_certs_user_host" ON "p_step_ca_certs" ("user_id", "host_id"); diff --git a/plugins/step-ca/migrations/snapshot.json b/plugins/step-ca/migrations/snapshot.json new file mode 100644 index 000000000..c90de0e8e --- /dev/null +++ b/plugins/step-ca/migrations/snapshot.json @@ -0,0 +1,51 @@ +{ + "version": 1, + "tables": { + "certs": { + "columns": { + "id": { + "type": "id", + "primaryKey": true, + "notNull": true + }, + "userId": { + "type": "refUser", + "length": 255, + "notNull": true + }, + "hostId": { + "type": "refHost", + "notNull": true + }, + "sshCert": { + "type": "text", + "notNull": true + }, + "privateKey": { + "type": "text", + "notNull": true + }, + "email": { + "type": "text" + }, + "createdAt": { + "type": "text", + "notNull": true, + "defaultNow": true + }, + "expiresAt": { + "type": "varchar", + "length": 255, + "notNull": true + } + }, + "indexes": [ + { + "name": "idx_step_ca_certs_user_host", + "columns": ["userId", "hostId"], + "unique": true + } + ] + } + } +} diff --git a/plugins/step-ca/migrations/sqlite/0001_create_certs.sql b/plugins/step-ca/migrations/sqlite/0001_create_certs.sql new file mode 100644 index 000000000..68aa897c8 --- /dev/null +++ b/plugins/step-ca/migrations/sqlite/0001_create_certs.sql @@ -0,0 +1,16 @@ +-- step-ca 0001: create_certs +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS "p_step_ca_certs" ( + "id" integer PRIMARY KEY AUTOINCREMENT, + "user_id" text NOT NULL, + "host_id" integer NOT NULL, + "ssh_cert" text NOT NULL, + "private_key" text NOT NULL, + "email" text, + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expires_at" text NOT NULL, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE, + FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE +); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_step_ca_certs_user_host" ON "p_step_ca_certs" ("user_id", "host_id"); diff --git a/plugins/step-ca/package.json b/plugins/step-ca/package.json new file mode 100644 index 000000000..5b70ef305 --- /dev/null +++ b/plugins/step-ca/package.json @@ -0,0 +1,18 @@ +{ + "name": "@termix-plugin/step-ca", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "build": "termix-plugin build", + "test": "termix-plugin test", + "typecheck": "tsc -p tsconfig.json", + "validate": "termix-plugin validate" + }, + "dependencies": { + "redis": "^6.2.1" + }, + "devDependencies": { + "@termix/plugin-sdk": "*" + } +} diff --git a/plugins/step-ca/src/backend/auth-session.ts b/plugins/step-ca/src/backend/auth-session.ts new file mode 100644 index 000000000..9b02efd3f --- /dev/null +++ b/plugins/step-ca/src/backend/auth-session.ts @@ -0,0 +1,378 @@ +import { randomBytes } from "node:crypto"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import type { CertStore } from "./cert-store.js"; +import type { CallbackQuery, CallbackResult, Runtime } from "./runtime.js"; +import { + buildAuthorizationUrl, + createPkce, + decodeJwtClaims, + discoverOidcEndpoints, + exchangeCodeForIdToken, + fetchRootCertificate, + findOidcProvisioner, + generateSshKeyPair, + parseSshCertificate, + signSshCertificate, + type StepCaTarget, +} from "./client.js"; + +/** The redirect URI 2.8 registered with identity providers. */ +export const LEGACY_CALLBACK_PATH = "/host/step-ca-callback"; +export const CALLBACK_PATH = "/plugin-api/step-ca/callback"; + +const AUTH_TIMEOUT_MS = 5 * 60 * 1000; +const REMOTE_CALLBACK_WAIT_MS = 30_000; +const REMOTE_POLL_MS = 250; + +/** The terminal socket, as far as a sign-in needs it. */ +export interface SignInSocket { + send: (data: string) => void; + on: (event: "close", listener: () => void) => void; +} + +export interface StartRequest { + userId: string; + hostId: number; + username: string; + socket: SignInSocket; + requestOrigin: string; +} + +interface Session { + state: string; + userId: string; + hostId: number; + username: string; + socket: SignInSocket; + target: StepCaTarget; + rootPem: string; + clientId: string; + clientSecret?: string; + tokenEndpoint: string; + redirectUri: string; + codeVerifier: string; + nonce: string; + keyPair: { publicKeyLine: string; privateKeyPem: string }; + timeout: ReturnType; + commandPoll: ReturnType | null; + processing: boolean; + completed: boolean; +} + +export interface StepCaSettings { + caUrl: string; + fingerprint: string; + provisioner: string; + privateEndpoints: string[]; +} + +/** Comma or newline separated hosts, lower-cased. */ +export function parseHostList(raw: unknown): string[] { + if (typeof raw !== "string") return []; + return raw + .split(/[\s,]+/) + .map((entry) => entry.trim().toLowerCase()) + .filter(Boolean); +} + +function send(socket: SignInSocket, message: Record): void { + try { + socket.send(JSON.stringify(message)); + } catch { + // socket already gone + } +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +export type AuthSessions = ReturnType; + +/** + * Step CA sign-ins in flight: the OIDC authorization-code flow against the + * CA's provisioner, then the CA signs a fresh key. Created per activation. + */ +export function createAuthSessions( + ctx: PluginContext, + deps: { certs: CertStore; runtime: Runtime }, +) { + const sessions = new Map(); + + async function readSettings(): Promise { + const [caUrl, fingerprint, provisioner, privateEndpoints] = + await Promise.all([ + ctx.settings.get("caUrl"), + ctx.settings.get("fingerprint"), + ctx.settings.get("provisioner"), + ctx.settings.get("privateEndpoints"), + ]); + if (!caUrl?.trim() || !fingerprint?.trim() || !provisioner?.trim()) { + return null; + } + return { + caUrl: caUrl.trim(), + fingerprint: fingerprint.trim(), + provisioner: provisioner.trim(), + privateEndpoints: parseHostList(privateEndpoints), + }; + } + + async function callbackPath(): Promise { + const legacy = await ctx.settings.get("legacyCallback"); + return legacy ? LEGACY_CALLBACK_PATH : CALLBACK_PATH; + } + + function end(session: Session, removeRuntime = true): void { + clearTimeout(session.timeout); + if (session.commandPoll) clearInterval(session.commandPoll); + sessions.delete(session.state); + if (removeRuntime) void deps.runtime.remove(session.state); + } + + async function finish( + session: Session, + query: CallbackQuery, + removeRuntime = true, + ): Promise { + if (query.error || !query.code) { + const message = + query.error_description || query.error || "Sign-in failed"; + send(session.socket, { + type: "stepca_error", + requestId: session.state, + error: `Step CA: ${message}`, + }); + end(session, removeRuntime); + return { ok: false, message }; + } + + try { + send(session.socket, { + type: "stepca_status", + requestId: session.state, + stage: "authenticating", + }); + const idToken = await exchangeCodeForIdToken({ + fetch: ctx.fetch, + tokenEndpoint: session.tokenEndpoint, + clientId: session.clientId, + clientSecret: session.clientSecret, + code: query.code, + redirectUri: session.redirectUri, + codeVerifier: session.codeVerifier, + allowedPrivateHosts: session.target.allowedPrivateHosts, + }); + const claims = decodeJwtClaims(idToken); + if (claims.nonce !== session.nonce) { + throw new Error("The identity token does not match this sign-in"); + } + const email = typeof claims.email === "string" ? claims.email : undefined; + + const certificate = await signSshCertificate( + session.target, + session.rootPem, + { + publicKeyLine: session.keyPair.publicKeyLine, + ott: idToken, + principals: [session.username], + keyId: email ?? session.username, + }, + ); + + const info = parseSshCertificate(certificate); + if (info.publicKeyLine !== session.keyPair.publicKeyLine) { + throw new Error("The CA returned a certificate for a different key"); + } + if (!info.principals.includes(session.username)) { + throw new Error( + "The CA certificate does not include the host username", + ); + } + const now = Date.now(); + if ( + info.validBefore.getTime() <= now || + info.validAfter.getTime() > now + 60_000 + ) { + throw new Error( + "The CA returned a certificate outside its validity window", + ); + } + + await deps.certs.save( + session.userId, + session.hostId, + { sshCert: certificate, privateKey: session.keyPair.privateKeyPem }, + info.validBefore, + email, + ); + + session.completed = true; + send(session.socket, { + type: "stepca_completed", + requestId: session.state, + expiresAt: info.validBefore.toISOString(), + }); + end(session, removeRuntime); + return { ok: true, message: "Signed in. You can close this window." }; + } catch (error) { + const message = errorMessage(error); + ctx.log.error(`Step CA certificate issuance failed: ${message}`); + send(session.socket, { + type: "stepca_error", + requestId: session.state, + error: `Step CA: ${message}`, + }); + end(session, removeRuntime); + return { ok: false, message }; + } + } + + return { + async start(request: StartRequest): Promise { + const settings = await readSettings(); + if (!settings) { + send(request.socket, { + type: "stepca_config_error", + requestId: "", + error: + "Step CA is not configured. An administrator must set the CA URL, root fingerprint and OIDC provisioner in the Step CA plugin settings.", + }); + return; + } + + const state = randomBytes(24).toString("base64url"); + try { + const target: StepCaTarget = { + fetch: ctx.fetch, + caUrl: settings.caUrl, + fingerprint: settings.fingerprint, + allowedPrivateHosts: settings.privateEndpoints, + }; + const rootPem = await fetchRootCertificate(target); + const provisioner = await findOidcProvisioner( + target, + rootPem, + settings.provisioner, + ); + const endpoints = await discoverOidcEndpoints( + ctx.fetch, + provisioner.configurationEndpoint, + target.allowedPrivateHosts, + ); + const pkce = createPkce(); + const nonce = randomBytes(16).toString("base64url"); + const redirectUri = `${request.requestOrigin}${await callbackPath()}`; + + const session: Session = { + state, + userId: request.userId, + hostId: request.hostId, + username: request.username, + socket: request.socket, + target, + rootPem, + clientId: provisioner.clientID, + clientSecret: provisioner.clientSecret, + tokenEndpoint: endpoints.tokenEndpoint, + redirectUri, + codeVerifier: pkce.verifier, + nonce, + keyPair: generateSshKeyPair(), + processing: false, + completed: false, + commandPoll: null, + timeout: setTimeout(() => { + const current = sessions.get(state); + if (!current || current.completed || current.processing) return; + send(request.socket, { type: "stepca_timeout", requestId: state }); + end(current); + }, AUTH_TIMEOUT_MS), + }; + sessions.set(state, session); + await deps.runtime.register(state); + // Another instance may receive the callback and queue it for us. + session.commandPoll = setInterval(() => { + if (session.processing || session.completed) return; + void deps.runtime.takeCommand(state).then(async (query) => { + if (!query || session.processing || session.completed) return; + session.processing = true; + const result = await finish(session, query, false); + await deps.runtime.complete(state, result); + }); + }, REMOTE_POLL_MS); + session.commandPoll.unref?.(); + request.socket.on("close", () => { + const current = sessions.get(state); + if (current && !current.completed) end(current); + }); + + send(request.socket, { + type: "stepca_status", + requestId: state, + stage: "chooser", + url: buildAuthorizationUrl({ + authorizationEndpoint: endpoints.authorizationEndpoint, + clientId: provisioner.clientID, + redirectUri, + state, + nonce, + codeChallenge: pkce.challenge, + }), + }); + } catch (error) { + ctx.log.error( + `Failed to start Step CA authentication: ${errorMessage(error)}`, + ); + send(request.socket, { + type: "stepca_error", + requestId: state, + error: `Step CA: ${errorMessage(error)}`, + }); + } + }, + + cancel(requestId: string): boolean { + const session = sessions.get(requestId); + if (!session) return false; + end(session); + return true; + }, + + /** + * The identity provider's redirect: exchanges the code, has the CA sign + * the key and stores the certificate. A sign-in another instance started + * is handed to it through the runtime. + */ + async complete(query: CallbackQuery): Promise { + const session = query.state ? sessions.get(query.state) : undefined; + if (!session) { + if (!query.state || !(await deps.runtime.submit(query.state, query))) { + return { + ok: false, + message: "This sign-in request is no longer active.", + }; + } + const deadline = Date.now() + REMOTE_CALLBACK_WAIT_MS; + while (Date.now() < deadline) { + const result = await deps.runtime.takeResult(query.state); + if (result) return result; + await new Promise((resolve) => setTimeout(resolve, REMOTE_POLL_MS)); + } + return { + ok: false, + message: "The Termix instance handling this sign-in did not respond.", + }; + } + if (session.processing || session.completed) { + return { ok: false, message: "This sign-in request was already used." }; + } + session.processing = true; + return finish(session, query); + }, + + closeAll(): void { + for (const session of [...sessions.values()]) end(session); + }, + }; +} diff --git a/plugins/step-ca/src/backend/cert-store.ts b/plugins/step-ca/src/backend/cert-store.ts new file mode 100644 index 000000000..c69797e30 --- /dev/null +++ b/plugins/step-ca/src/backend/cert-store.ts @@ -0,0 +1,96 @@ +import { and, eq } from "drizzle-orm"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +// ctx.db.define hands the table back untyped and the drizzle handle is the +// server's own, so both are typed at this module's edge. +type Table = any; +type Drizzle = any; +/* eslint-enable @typescript-eslint/no-explicit-any */ + +export interface IssuedCertificate { + sshCert: string; + privateKey: string; +} + +export type CertStore = ReturnType; + +export function createCertStore(ctx: PluginContext, table: Table) { + const client = () => ctx.db.client(); + const byUserAndHost = (userId: string, hostId: number) => + and(eq(table.userId, userId), eq(table.hostId, hostId)); + + async function find(userId: string, hostId: number) { + const rows = await ( + await client() + ) + .select() + .from(table) + .where(byUserAndHost(userId, hostId)) + .limit(1); + return rows[0] ?? null; + } + + async function remove(userId: string, hostId: number): Promise { + if (!(await find(userId, hostId))) return false; + await (await client()).delete(table).where(byUserAndHost(userId, hostId)); + await ctx.db.persist(); + return true; + } + + return { + async save( + userId: string, + hostId: number, + certificate: IssuedCertificate, + expiresAt: Date, + email?: string, + ): Promise { + const values = { + sshCert: await ctx.secrets.seal(certificate.sshCert), + privateKey: await ctx.secrets.seal(certificate.privateKey), + email: email ?? null, + createdAt: new Date().toISOString(), + expiresAt: expiresAt.toISOString(), + }; + if (await find(userId, hostId)) { + await ( + await client() + ) + .update(table) + .set(values) + .where(byUserAndHost(userId, hostId)); + } else { + await ( + await client() + ) + .insert(table) + .values({ userId, hostId, ...values }); + } + await ctx.db.persist(); + }, + + /** The usable certificate, or null. Expired or unreadable rows are deleted. */ + async get( + userId: string, + hostId: number, + ): Promise { + const row = await find(userId, hostId); + if (!row) return null; + const expiresAt = new Date(row.expiresAt).getTime(); + if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) { + await remove(userId, hostId); + return null; + } + const sshCert = await ctx.secrets.unseal(row.sshCert); + const privateKey = await ctx.secrets.unseal(row.privateKey); + if (!sshCert || !privateKey) { + await remove(userId, hostId); + return null; + } + return { sshCert, privateKey }; + }, + + remove, + }; +} diff --git a/src/backend/utils/step-ca-client.ts b/plugins/step-ca/src/backend/client.ts similarity index 91% rename from src/backend/utils/step-ca-client.ts rename to plugins/step-ca/src/backend/client.ts index ce54d7201..47d56f13e 100644 --- a/src/backend/utils/step-ca-client.ts +++ b/plugins/step-ca/src/backend/client.ts @@ -1,5 +1,5 @@ -import crypto from "crypto"; -import { safeOutboundFetch } from "./safe-outbound-fetch.js"; +import crypto from "node:crypto"; +import type { PluginFetch } from "@termix/plugin-sdk/backend"; /** * A minimal client for smallstep's step-ca SSH user-certificate flow, done @@ -12,6 +12,8 @@ import { safeOutboundFetch } from "./safe-outbound-fetch.js"; */ export interface StepCaTarget { + /** ctx.fetch: core's SSRF guard. */ + fetch: PluginFetch; caUrl: string; fingerprint: string; /** Hosts the SSRF guard may reach even when they resolve to private ranges. */ @@ -87,11 +89,13 @@ export async function fetchRootCertificate( target: StepCaTarget, ): Promise { const fingerprint = normalizeFingerprint(target.fingerprint); - const response = await safeOutboundFetch( + const response = await target.fetch( `${normalizeCaUrl(target.caUrl)}/root/${fingerprint}`, - { method: "GET", signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }, - target.allowedPrivateHosts, - { rejectUnauthorized: false }, + { + timeoutMs: FETCH_TIMEOUT_MS, + allowPrivateHosts: target.allowedPrivateHosts, + tls: { rejectUnauthorized: false }, + }, ); const { ca } = await readJson<{ ca?: string }>( response, @@ -111,11 +115,13 @@ export async function findOidcProvisioner( const base = normalizeCaUrl(target.caUrl); let cursor = ""; for (let page = 0; page < 20; page++) { - const response = await safeOutboundFetch( + const response = await target.fetch( `${base}/provisioners?limit=100${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`, - { method: "GET", signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }, - target.allowedPrivateHosts, - { ca: rootPem }, + { + timeoutMs: FETCH_TIMEOUT_MS, + allowPrivateHosts: target.allowedPrivateHosts, + tls: { ca: rootPem }, + }, ); const body = await readJson<{ provisioners?: Array>; @@ -148,14 +154,14 @@ export async function findOidcProvisioner( } export async function discoverOidcEndpoints( + fetch: PluginFetch, configurationEndpoint: string, allowedPrivateHosts: readonly string[], ): Promise { - const response = await safeOutboundFetch( - configurationEndpoint, - { method: "GET", signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }, - allowedPrivateHosts, - ); + const response = await fetch(configurationEndpoint, { + timeoutMs: FETCH_TIMEOUT_MS, + allowPrivateHosts: allowedPrivateHosts, + }); const doc = await readJson<{ authorization_endpoint?: string; token_endpoint?: string; @@ -199,6 +205,7 @@ export function buildAuthorizationUrl(input: { } export async function exchangeCodeForIdToken(input: { + fetch: PluginFetch; tokenEndpoint: string; clientId: string; clientSecret?: string; @@ -215,16 +222,13 @@ export async function exchangeCodeForIdToken(input: { code_verifier: input.codeVerifier, }); if (input.clientSecret) form.set("client_secret", input.clientSecret); - const response = await safeOutboundFetch( - input.tokenEndpoint, - { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: form.toString(), - signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), - }, - input.allowedPrivateHosts, - ); + const response = await input.fetch(input.tokenEndpoint, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: form.toString(), + timeoutMs: FETCH_TIMEOUT_MS, + allowPrivateHosts: input.allowedPrivateHosts, + }); const body = await readJson<{ id_token?: string }>( response, "OIDC token exchange", @@ -272,7 +276,7 @@ export async function signSshCertificate( ): Promise { const blob = input.publicKeyLine.trim().split(/\s+/)[1]; if (!blob) throw new Error("Invalid public key line"); - const response = await safeOutboundFetch( + const response = await target.fetch( `${normalizeCaUrl(target.caUrl)}/1.0/ssh/sign`, { method: "POST", @@ -284,10 +288,10 @@ export async function signSshCertificate( principals: input.principals, keyID: input.keyId, }), - signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), + timeoutMs: FETCH_TIMEOUT_MS, + allowPrivateHosts: target.allowedPrivateHosts, + tls: { ca: rootPem }, }, - target.allowedPrivateHosts, - { ca: rootPem }, ); if (!response.ok) { let detail = ""; diff --git a/plugins/step-ca/src/backend/index.ts b/plugins/step-ca/src/backend/index.ts new file mode 100644 index 000000000..9fc090636 --- /dev/null +++ b/plugins/step-ca/src/backend/index.ts @@ -0,0 +1,28 @@ +import type { Router } from "express"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import { certs as certsTable } from "./tables.js"; +import { createCertStore } from "./cert-store.js"; +import { createRuntime } from "./runtime.js"; +import { createAuthSessions } from "./auth-session.js"; +import { createStepCaProvider } from "./provider.js"; +import { PUBLIC_PATHS, registerRoutes } from "./routes.js"; + +export async function activate(ctx: PluginContext) { + const table = await ctx.db.define(certsTable); + const certs = createCertStore(ctx, table); + + const runtime = createRuntime(ctx); + const sessions = createAuthSessions(ctx, { certs, runtime }); + ctx.disposables.add(async () => { + sessions.closeAll(); + await runtime.close(); + }); + + ctx.auth.registerSshAuthProvider(createStepCaProvider(ctx, certs, sessions)); + + registerRoutes(ctx.http.router({ public: PUBLIC_PATHS }), sessions); +} + +export async function deactivate() { + // Everything above was registered through ctx and is disposed by core. +} diff --git a/plugins/step-ca/src/backend/provider.ts b/plugins/step-ca/src/backend/provider.ts new file mode 100644 index 000000000..146241b9c --- /dev/null +++ b/plugins/step-ca/src/backend/provider.ts @@ -0,0 +1,88 @@ +import type { + PluginContext, + PluginSshAuthProvider, +} from "@termix/plugin-sdk/backend"; +import { applyCertificateAuth } from "@termix/plugin-sdk/ssh-certs"; +import type { AuthSessions, SignInSocket } from "./auth-session.js"; +import type { CertStore } from "./cert-store.js"; + +export const AUTH_TYPE = "stepca"; + +const REQUIRED_MESSAGE = + "Step CA authentication required. Please open a Terminal connection to this host first to complete browser-based authentication."; + +const AUTH_FAILED_PATTERN = /All configured authentication methods failed/i; + +/** + * Hosts with auth type "stepca" connect with the certificate the CA issued. + * Without one, the terminal is asked for a browser sign-in + * ("stepca_auth_required"), which startInteraction runs. + */ +export function createStepCaProvider( + ctx: PluginContext, + certs: CertStore, + sessions: AuthSessions, +): PluginSshAuthProvider { + return { + type: AUTH_TYPE, + labelKey: "hosts.filterAuthStepca", + needsUserInteraction: true, + supportsBackground: false, + interaction: AUTH_TYPE, + prepare: async (config, host, env) => { + const certificate = await certs.get(env.userId, env.hostId); + if (!certificate) { + env.log("info", "No valid certificate found, requesting sign-in"); + return { + status: "interaction-required", + interaction: AUTH_TYPE, + message: REQUIRED_MESSAGE, + }; + } + try { + await applyCertificateAuth( + config as never, + env.client as never, + { + privateKey: certificate.privateKey, + certificate: certificate.sshCert, + }, + host.username, + ); + } catch (error) { + return { + status: "error", + code: "failed", + message: + "Step CA authentication failed: " + + (error instanceof Error ? error.message : String(error)), + }; + } + env.log("info", "Using cached SSH certificate"); + return { status: "ready" }; + }, + onAuthFailed: (_host, env, context) => { + if (!AUTH_FAILED_PATTERN.test(context.error.message)) return undefined; + ctx.log.warn("Step CA authentication failed, forgetting the certificate"); + void certs.remove(env.userId, env.hostId).catch(() => undefined); + return { + status: "interaction-required", + interaction: AUTH_TYPE, + message: + "Step CA authentication failed or expired. Please authenticate again.", + }; + }, + startInteraction: async (request) => { + await sessions.start({ + userId: request.userId, + hostId: request.hostId, + username: request.host.username, + socket: request.socket as SignInSocket, + requestOrigin: request.requestOrigin, + }); + }, + cancelInteraction: async (request) => { + if (request.requestId) sessions.cancel(request.requestId); + }, + }; +} diff --git a/plugins/step-ca/src/backend/routes.ts b/plugins/step-ca/src/backend/routes.ts new file mode 100644 index 000000000..54bac32b0 --- /dev/null +++ b/plugins/step-ca/src/backend/routes.ts @@ -0,0 +1,65 @@ +import type { Request, Response, Router } from "express"; +import type { AuthSessions } from "./auth-session.js"; + +/** + * The identity provider redirects here without a Termix session: the sign-in + * may finish in another browser, so it is matched by the OAuth state. + */ +export const PUBLIC_PATHS = ["/callback"]; + +export function escapeHtml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +export function resultPage(ok: boolean, message: string): string { + return `Termix + +

${ok ? "Signed in" : "Sign-in failed"}

${escapeHtml(message)}

`; +} + +export function registerRoutes(router: Router, sessions: AuthSessions): void { + /** + * @openapi + * /plugin-api/step-ca/callback: + * get: + * summary: OIDC callback for a Step CA sign-in + * description: Public. The redirect URI registered with the identity provider behind the CA's OIDC provisioner. Exchanges the code, has the CA sign the key and tells the terminal to reconnect. The 2.8 URI /host/step-ca-callback redirects here. + * tags: [Step CA] + * parameters: + * - name: state + * in: query + * schema: + * type: string + * - name: code + * in: query + * schema: + * type: string + * responses: + * 200: + * description: Signed in; an HTML page the browser can close. + * 400: + * description: The sign-in failed or is no longer active. + */ + router.get("/callback", async (req: Request, res: Response) => { + const query = (name: string): string | undefined => { + const value = req.query[name]; + return typeof value === "string" ? value : undefined; + }; + const result = await sessions.complete({ + state: query("state"), + code: query("code"), + error: query("error"), + error_description: query("error_description"), + }); + res + .status(result.ok ? 200 : 400) + .type("html") + .send(resultPage(result.ok, result.message)); + }); +} diff --git a/plugins/step-ca/src/backend/runtime.ts b/plugins/step-ca/src/backend/runtime.ts new file mode 100644 index 000000000..4c5603eae --- /dev/null +++ b/plugins/step-ca/src/backend/runtime.ts @@ -0,0 +1,164 @@ +import { createClient } from "redis"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; + +export interface CallbackQuery { + state?: string; + code?: string; + error?: string; + error_description?: string; +} + +export interface CallbackResult { + ok: boolean; + message: string; +} + +const SESSION_TTL_SECONDS = 5 * 60; +const RESULT_TTL_SECONDS = 60; +const CONNECT_RETRY_MS = 15_000; + +export type Runtime = ReturnType; + +/** + * Hands an identity provider callback to the Termix instance that started + * the sign-in, through Redis when REDIS_URL is set. With one instance, or no + * Redis, every call is a no-op and the callback is handled locally. + */ +export function createRuntime(ctx: PluginContext) { + const prefix = + process.env.TERMIX_STEP_CA_REDIS_PREFIX?.trim() || "termix:step-ca"; + let client: ReturnType | null = null; + let connecting: Promise | null = null; + let nextConnectAttempt = 0; + + const routeKey = (state: string) => `${prefix}:route:${state}`; + const commandKey = (state: string) => `${prefix}:command:${state}`; + const resultKey = (state: string) => `${prefix}:result:${state}`; + + function logFailure(operation: string, error: unknown): void { + ctx.log.warn( + `Step CA Redis runtime unavailable (${operation}): ${error instanceof Error ? error.message : String(error)}`, + ); + } + + async function seal(value: unknown): Promise { + return ctx.secrets.seal(JSON.stringify(value)); + } + + async function unseal(raw: string): Promise { + const plain = await ctx.secrets.unseal(raw); + if (!plain) return null; + try { + return JSON.parse(plain) as T; + } catch { + return null; + } + } + + async function connect(url: string): Promise { + try { + client = createClient({ + url, + socket: { connectTimeout: 1500, reconnectStrategy: false }, + }); + client.on("error", (error) => logFailure("client", error)); + await client.connect(); + nextConnectAttempt = 0; + return true; + } catch (error) { + nextConnectAttempt = Date.now() + CONNECT_RETRY_MS; + logFailure("connect", error); + await client?.disconnect().catch(() => {}); + client = null; + return false; + } + } + + async function ready(): Promise { + const url = process.env.REDIS_URL?.trim(); + if (!url) return false; + if (client?.isReady) return true; + if (Date.now() < nextConnectAttempt) return false; + if (connecting) return connecting; + connecting = connect(url).finally(() => { + connecting = null; + }); + return connecting; + } + + return { + async register(state: string): Promise { + if (!(await ready()) || !client) return; + await client + .set(routeKey(state), "active", { EX: SESSION_TTL_SECONDS }) + .catch((error) => logFailure("register", error)); + }, + + /** Queues a callback for the instance that owns the sign-in. */ + async submit(state: string, query: CallbackQuery): Promise { + if (!(await ready()) || !client) return false; + try { + if (!(await client.exists(routeKey(state)))) return false; + const stored = await client.set(commandKey(state), await seal(query), { + EX: SESSION_TTL_SECONDS, + NX: true, + }); + return stored === "OK"; + } catch (error) { + logFailure("submit", error); + return false; + } + }, + + async takeCommand(state: string): Promise { + if (!(await ready()) || !client) return null; + try { + const raw = await client.getDel(commandKey(state)); + return raw ? unseal(raw.toString()) : null; + } catch (error) { + logFailure("take_command", error); + return null; + } + }, + + async complete(state: string, result: CallbackResult): Promise { + if (!(await ready()) || !client) return; + try { + await client + .multi() + .set(resultKey(state), await seal(result), { + EX: RESULT_TTL_SECONDS, + }) + .del(routeKey(state)) + .del(commandKey(state)) + .exec(); + } catch (error) { + logFailure("complete", error); + } + }, + + async takeResult(state: string): Promise { + if (!(await ready()) || !client) return null; + try { + const raw = await client.getDel(resultKey(state)); + return raw ? unseal(raw.toString()) : null; + } catch (error) { + logFailure("take_result", error); + return null; + } + }, + + async remove(state: string): Promise { + if (!(await ready()) || !client) return; + await client + .del([routeKey(state), commandKey(state), resultKey(state)]) + .catch((error) => logFailure("remove", error)); + }, + + async close(): Promise { + const current = client; + client = null; + if (current?.isOpen) await current.quit().catch(() => {}); + }, + }; +} diff --git a/plugins/step-ca/src/backend/tables.ts b/plugins/step-ca/src/backend/tables.ts new file mode 100644 index 000000000..6a8737e4a --- /dev/null +++ b/plugins/step-ca/src/backend/tables.ts @@ -0,0 +1,34 @@ +import { + defineTable, + id, + refHost, + refUser, + text, + varchar, +} from "@termix/plugin-sdk/db"; + +/** + * One issued certificate per user and host. sshCert and privateKey are + * sealed with ctx.secrets.seal. 2.8 kept Step CA certificates in + * opkssh_tokens; those are left to expire. + */ +export const certs = defineTable( + "certs", + { + id: id(), + userId: refUser(), + hostId: refHost(), + sshCert: text().notNull(), + privateKey: text().notNull(), + email: text(), + createdAt: text().notNull().defaultNow(), + expiresAt: varchar().notNull(), + }, + { + uniques: [ + { name: "idx_step_ca_certs_user_host", columns: ["userId", "hostId"] }, + ], + }, +); + +export const tables = [certs]; diff --git a/plugins/step-ca/src/frontend/RedirectUriSetting.tsx b/plugins/step-ca/src/frontend/RedirectUriSetting.tsx new file mode 100644 index 000000000..5ff1a7ddf --- /dev/null +++ b/plugins/step-ca/src/frontend/RedirectUriSetting.tsx @@ -0,0 +1,54 @@ +import { toast } from "sonner"; +import { + useTranslation, + type SettingsComponentProps, +} from "@termix/plugin-sdk/frontend"; +import { Button, Input, copyToClipboard } from "@termix/plugin-sdk/ui"; + +const CALLBACK_PATH = "plugin-api/step-ca/callback"; +const LEGACY_CALLBACK_PATH = "host/step-ca-callback"; + +/** The callback URI to register with the provisioner's identity provider. */ +export function redirectUri( + legacy: boolean, + baseUri = document.baseURI, +): string { + return new URL( + legacy ? LEGACY_CALLBACK_PATH : CALLBACK_PATH, + baseUri, + ).toString(); +} + +export function RedirectUriSetting({ values }: SettingsComponentProps) { + const { t } = useTranslation(); + const uri = redirectUri(values.legacyCallback === true); + + return ( +
+ + {t("settings.redirectUri.label")} + +
+ + +
+

+ {t("settings.redirectUri.description")} +

+
+ ); +} diff --git a/plugins/step-ca/src/frontend/StepCaAuthEditor.tsx b/plugins/step-ca/src/frontend/StepCaAuthEditor.tsx new file mode 100644 index 000000000..2aa75ab04 --- /dev/null +++ b/plugins/step-ca/src/frontend/StepCaAuthEditor.tsx @@ -0,0 +1,29 @@ +import { useTranslation } from "@termix/plugin-sdk/frontend"; + +const DOCS_URL = + "https://smallstep.com/docs/step-ca/provisioners/#oauthoidc-single-sign-on"; + +/** The host editor's "stepca" auth method: nothing to fill in, just a pointer. */ +export function StepCaAuthEditor() { + const { t } = useTranslation(); + return ( +
+
+ + {t("editor.label")} + + + {t("editor.docs")} + +
+

+ {t("editor.description")} +

+
+ ); +} diff --git a/plugins/step-ca/src/frontend/StepCaDialog.tsx b/plugins/step-ca/src/frontend/StepCaDialog.tsx new file mode 100644 index 000000000..fb8fde4b4 --- /dev/null +++ b/plugins/step-ca/src/frontend/StepCaDialog.tsx @@ -0,0 +1,102 @@ +import { Shield, ExternalLink, Loader2, AlertCircle } from "lucide-react"; +import { useTranslation } from "@termix/plugin-sdk/frontend"; +import { Button } from "@termix/plugin-sdk/ui"; + +export type SignInStage = "chooser" | "waiting" | "authenticating" | "error"; + +interface StepCaDialogProps { + stage: SignInStage; + error?: string; + onCancel: () => void; + onOpenUrl: () => void; + backgroundColor?: string; +} + +const actionButton = + "border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 rounded-none text-[10px] font-bold uppercase tracking-widest w-full flex items-center gap-2"; +const footerButton = + "rounded-none text-[10px] font-bold uppercase tracking-widest"; + +/** The browser sign-in the terminal shows while Step CA waits. */ +export function StepCaDialog({ + stage, + error, + onCancel, + onOpenUrl, + backgroundColor, +}: StepCaDialogProps) { + const { t } = useTranslation(); + + return ( +
+
+
+
+
+ +

+ {t("dialog.title")} +

+
+ {stage === "chooser" && ( +

+ {t("dialog.description")} +

+ )} +
+
+ {stage === "chooser" && ( + + )} + + {(stage === "waiting" || stage === "authenticating") && ( +
+ +

+ {stage === "waiting" + ? t("dialog.waiting") + : t("dialog.authenticating")} +

+
+ )} + + {stage === "error" && error && ( +
+ +
+

+ {t("common.error")} +

+

+ {error} +

+
+
+ )} + +
+ +
+
+
+
+ ); +} diff --git a/plugins/step-ca/src/frontend/StepCaOverlay.tsx b/plugins/step-ca/src/frontend/StepCaOverlay.tsx new file mode 100644 index 000000000..e1f34712e --- /dev/null +++ b/plugins/step-ca/src/frontend/StepCaOverlay.tsx @@ -0,0 +1,187 @@ +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "@termix/plugin-sdk/frontend"; +import { StepCaDialog, type SignInStage } from "./StepCaDialog"; + +/** What the ssh-terminal plugin hands a "terminal.overlay" component. */ +export interface TerminalOverlayProps { + host: { id: number | string; [key: string]: unknown } | undefined; + backgroundColor?: string; + subscribe: ( + listener: (message: { type: string; [key: string]: unknown }) => void, + ) => () => void; + holdConnectTimeout: (held: boolean) => void; + fail: (message: string) => void; + disconnect: () => void; + send: (type: string, data?: unknown) => void; + connectPayload: () => Record; +} + +interface DialogState { + authUrl: string; + requestId: string; + stage: SignInStage; + error?: string; +} + +/** How long the sign-in dialog waits before giving up on the connection. */ +const DIALOG_TIMEOUT_MS = 300_000; + +/** + * Runs the Step CA browser sign-in for a terminal: asks the server to start + * it, opens the identity provider, and reconnects once the CA has issued the + * certificate. + */ +export function StepCaOverlay({ + subscribe, + holdConnectTimeout, + fail, + disconnect, + send, + connectPayload, + backgroundColor, +}: TerminalOverlayProps) { + const { t } = useTranslation(); + const [dialog, setDialog] = useState(null); + const timerRef = useRef | null>(null); + const heldRef = useRef(false); + // A second "auth required" after a sign-in means the certificate did not work. + const failedRef = useRef(false); + + useEffect(() => { + const clearTimer = () => { + if (timerRef.current) clearTimeout(timerRef.current); + timerRef.current = null; + }; + const hold = (held: boolean) => { + if (heldRef.current === held) return; + heldRef.current = held; + holdConnectTimeout(held); + }; + const showError = (requestId: unknown, error: string) => { + clearTimer(); + setDialog((current) => + current + ? { ...current, stage: "error", error } + : { + authUrl: "", + requestId: typeof requestId === "string" ? requestId : "", + stage: "error", + error, + }, + ); + }; + + const unsubscribe = subscribe((message) => { + switch (message.type) { + case "stepca_auth_required": + hold(true); + if (failedRef.current) { + clearTimer(); + setDialog(null); + hold(false); + fail(t("errors.authFailed")); + } else { + failedRef.current = true; + send("stepca_start_auth", { hostId: message.hostId }); + } + break; + case "stepca_status": + if (message.stage === "chooser") { + setDialog({ + authUrl: typeof message.url === "string" ? message.url : "", + requestId: + typeof message.requestId === "string" ? message.requestId : "", + stage: "chooser", + }); + clearTimer(); + timerRef.current = setTimeout(() => { + setDialog(null); + hold(false); + disconnect(); + }, DIALOG_TIMEOUT_MS); + } else { + setDialog((current) => + current + ? { ...current, stage: message.stage as SignInStage } + : null, + ); + } + break; + case "stepca_completed": + clearTimer(); + setDialog(null); + send("stepca_auth_completed", connectPayload()); + break; + case "stepca_error": + failedRef.current = true; + showError(message.requestId, String(message.error ?? "")); + break; + case "stepca_timeout": + failedRef.current = true; + showError(message.requestId, t("errors.timeout")); + break; + case "stepca_config_error": + showError(message.requestId, String(message.error ?? "")); + break; + case "connected": + case "sessionAttached": + failedRef.current = false; + hold(false); + break; + case "session_closed": + clearTimer(); + setDialog(null); + hold(false); + break; + } + }); + + return () => { + unsubscribe(); + clearTimer(); + if (heldRef.current) { + heldRef.current = false; + holdConnectTimeout(false); + } + }; + }, [ + subscribe, + holdConnectTimeout, + fail, + disconnect, + send, + connectPayload, + t, + ]); + + if (!dialog) return null; + + const close = () => { + if (timerRef.current) clearTimeout(timerRef.current); + timerRef.current = null; + if (dialog.requestId) { + send("stepca_cancel", { requestId: dialog.requestId }); + } + setDialog(null); + if (heldRef.current) { + heldRef.current = false; + holdConnectTimeout(false); + } + }; + + return ( + { + if (!dialog.authUrl) return; + window.open(dialog.authUrl, "_blank"); + setDialog((current) => + current ? { ...current, stage: "waiting" } : null, + ); + }} + backgroundColor={backgroundColor} + /> + ); +} diff --git a/plugins/step-ca/src/frontend/index.tsx b/plugins/step-ca/src/frontend/index.tsx new file mode 100644 index 000000000..a207d6d7d --- /dev/null +++ b/plugins/step-ca/src/frontend/index.tsx @@ -0,0 +1,29 @@ +import type { ComponentType } from "react"; +import type { TermixApp } from "@termix/plugin-sdk/frontend"; +import { StepCaAuthEditor } from "./StepCaAuthEditor"; +import { StepCaOverlay } from "./StepCaOverlay"; +import { RedirectUriSetting } from "./RedirectUriSetting"; + +export function activate(app: TermixApp): void { + app.registerSshAuthEditor({ + authType: "stepca", + titleKey: "hosts.filterAuthStepca", + component: StepCaAuthEditor, + }); + + // The browser sign-in while a terminal connects to a Step CA host. + app.registerSlotContribution("terminal.overlay", { + actionId: "step-ca.signIn", + titleKey: "dialog.title", + kind: "component", + component: StepCaOverlay as unknown as ComponentType< + Record + >, + }); + + app.registerSettingsComponent("redirectUri", RedirectUriSetting); +} + +export function deactivate(): void { + // Registrations through app are disposed by core. +} diff --git a/src/backend/tests/utils/step-ca-client.test.ts b/plugins/step-ca/tests/backend/client.test.ts similarity index 59% rename from src/backend/tests/utils/step-ca-client.test.ts rename to plugins/step-ca/tests/backend/client.test.ts index ff4147dea..a7f9e033e 100644 --- a/src/backend/tests/utils/step-ca-client.test.ts +++ b/plugins/step-ca/tests/backend/client.test.ts @@ -1,19 +1,19 @@ +import crypto from "node:crypto"; import { describe, expect, it } from "vitest"; -import crypto from "crypto"; import { buildAuthorizationUrl, certificateFingerprint, createPkce, decodeJwtClaims, + fetchRootCertificate, + findOidcProvisioner, generateSshKeyPair, normalizeCaUrl, normalizeFingerprint, parseSshCertificate, -} from "../../utils/step-ca-client.js"; -import { - generateCa, - signUserCertificate, -} from "../../database/routes/ssh-certificate.js"; +} from "../../src/backend/client.js"; +import { parseHostList } from "../../src/backend/auth-session.js"; +import { CA_URL, createFakeCa, createSshCa, fakeRoot } from "./fake-ca.js"; describe("step-ca client helpers", () => { it("normalizes the CA url and fingerprint the way step does", () => { @@ -21,17 +21,14 @@ describe("step-ca client helpers", () => { "https://ca.internal:9000", ); expect(() => normalizeCaUrl("http://ca.internal")).toThrow(/https/); - const fp = "AB:cd".repeat(16).replace(/:/g, "") + ""; + const fp = "AB:cd".repeat(16).replace(/:/g, ""); expect(normalizeFingerprint("AB:cd".repeat(16))).toBe(fp.toLowerCase()); expect(() => normalizeFingerprint("abcd")).toThrow(/SHA-256/); }); it("fingerprints a PEM certificate by the sha256 of its DER", () => { - const der = crypto.randomBytes(64); - const pem = `-----BEGIN CERTIFICATE-----\n${der.toString("base64")}\n-----END CERTIFICATE-----\n`; - expect(certificateFingerprint(pem)).toBe( - crypto.createHash("sha256").update(der).digest("hex"), - ); + const root = fakeRoot(); + expect(certificateFingerprint(root.pem)).toBe(root.fingerprint); }); it("builds a PKCE authorization request", () => { @@ -55,23 +52,19 @@ describe("step-ca client helpers", () => { expect(url.searchParams.get("scope")).toContain("openid"); }); - it("generates an ed25519 key whose public line a CA can certify, and reads the cert back", () => { + it("generates an ed25519 key a CA can certify, and reads the cert back", () => { const { publicKeyLine, privateKeyPem } = generateSshKeyPair(); expect(publicKeyLine).toMatch(/^ssh-ed25519 [A-Za-z0-9+/=]+$/); expect(privateKeyPem).toContain("BEGIN PRIVATE KEY"); - const ca = generateCa(); - const cert = signUserCertificate({ - userPublicKeyLine: publicKeyLine, - caPrivateKeyPem: ca.privateKeyPem, - caPublicKeyLine: ca.publicKeyLine, + const cert = createSshCa().sign({ + publicKeyBlob: publicKeyLine.split(" ")[1], keyId: "alice@example", principals: ["alice", "ops"], validAfter: 1_700_000_000, validBefore: 1_700_057_600, }); - expect(cert).not.toBeNull(); - const info = parseSshCertificate(cert!); + const info = parseSshCertificate(cert); expect(info).toMatchObject({ keyType: "ssh-ed25519-cert-v01@openssh.com", publicKeyLine, @@ -87,6 +80,46 @@ describe("step-ca client helpers", () => { parseSshCertificate("ssh-ed25519-cert-v01@openssh.com AAAA"), ).toThrow(/certificate/); }); + + it("reads the private endpoint list with commas or new lines", () => { + expect(parseHostList("ca.internal, SSO.internal\nidp.lan")).toEqual([ + "ca.internal", + "sso.internal", + "idp.lan", + ]); + expect(parseHostList(undefined)).toEqual([]); + }); +}); + +describe("talking to the CA", () => { + it("refuses a root whose fingerprint does not match", async () => { + const ca = createFakeCa({ nonce: () => "" }); + await expect( + fetchRootCertificate({ + fetch: ca.fetch, + caUrl: CA_URL, + fingerprint: "b".repeat(64), + allowedPrivateHosts: [], + }), + ).rejects.toThrow(); + }); + + it("finds the OIDC provisioner by name and passes the private hosts", async () => { + const ca = createFakeCa({ nonce: () => "" }); + const target = { + fetch: ca.fetch, + caUrl: CA_URL, + fingerprint: ca.root.fingerprint, + allowedPrivateHosts: ["ca.test"], + }; + const root = await fetchRootCertificate(target); + const provisioner = await findOidcProvisioner(target, root, "oidc"); + expect(provisioner.clientID).toBe("termix"); + expect(ca.calls.every((call) => call.init?.allowPrivateHosts)).toBe(true); + await expect(findOidcProvisioner(target, root, "jwk")).rejects.toThrow( + /not found/, + ); + }); }); describe("decodeJwtClaims", () => { diff --git a/plugins/step-ca/tests/backend/fake-ca.ts b/plugins/step-ca/tests/backend/fake-ca.ts new file mode 100644 index 000000000..47a76f6c9 --- /dev/null +++ b/plugins/step-ca/tests/backend/fake-ca.ts @@ -0,0 +1,160 @@ +import crypto from "node:crypto"; +import type { PluginFetchInit } from "@termix/plugin-sdk/backend"; + +const CERT_TYPE = "ssh-ed25519-cert-v01@openssh.com"; + +function sshString(value: Buffer | string): Buffer { + const data = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8"); + const len = Buffer.alloc(4); + len.writeUInt32BE(data.length, 0); + return Buffer.concat([len, data]); +} + +function uint64(value: bigint): Buffer { + const out = Buffer.alloc(8); + out.writeBigUInt64BE(value, 0); + return out; +} + +function uint32(value: number): Buffer { + const out = Buffer.alloc(4); + out.writeUInt32BE(value, 0); + return out; +} + +/** An ed25519 CA that signs OpenSSH user certificates, like step-ca does. */ +export function createSshCa() { + const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519"); + const raw = Buffer.from( + (publicKey.export({ format: "jwk" }) as { x: string }).x, + "base64url", + ); + const caBlob = Buffer.concat([sshString("ssh-ed25519"), sshString(raw)]); + + return { + sign(input: { + publicKeyBlob: string; + keyId: string; + principals: string[]; + validAfter: number; + validBefore: number; + }): string { + const userBlob = Buffer.from(input.publicKeyBlob, "base64"); + // ssh-ed25519 blob: string type, string pk + const pk = userBlob.subarray(4 + 11 + 4); + const body = Buffer.concat([ + sshString(CERT_TYPE), + sshString(crypto.randomBytes(32)), + sshString(pk), + uint64(1n), + uint32(1), + sshString(input.keyId), + sshString(Buffer.concat(input.principals.map((p) => sshString(p)))), + uint64(BigInt(input.validAfter)), + uint64(BigInt(input.validBefore)), + sshString(Buffer.alloc(0)), + sshString(Buffer.alloc(0)), + sshString(Buffer.alloc(0)), + sshString(caBlob), + ]); + const signature = Buffer.concat([ + sshString("ssh-ed25519"), + sshString(crypto.sign(null, body, privateKey)), + ]); + return `${CERT_TYPE} ${Buffer.concat([body, sshString(signature)]).toString("base64")}`; + }, + }; +} + +/** A root "certificate" the client fingerprints by the sha256 of its DER. */ +export function fakeRoot() { + const der = crypto.randomBytes(128); + return { + pem: `-----BEGIN CERTIFICATE-----\n${der.toString("base64")}\n-----END CERTIFICATE-----\n`, + fingerprint: crypto.createHash("sha256").update(der).digest("hex"), + }; +} + +export function jwt(claims: Record): string { + const part = (value: object) => + Buffer.from(JSON.stringify(value)).toString("base64url"); + return `${part({ alg: "none" })}.${part(claims)}.sig`; +} + +export const CA_URL = "https://ca.test"; +export const DISCOVERY_URL = + "https://idp.test/.well-known/openid-configuration"; + +export interface FakeCaOptions { + /** Principals the CA puts on the certificate; defaults to what was asked. */ + principals?: string[]; + /** Nonce the identity provider puts in the id token. */ + nonce: () => string; + /** Status for /1.0/ssh/sign. */ + signStatus?: number; +} + +/** + * Answers ctx.fetch as a step-ca server plus its identity provider would. + * Records every call so a test can check the TLS options. + */ +export function createFakeCa(options: FakeCaOptions) { + const root = fakeRoot(); + const ca = createSshCa(); + const calls: Array<{ url: string; init?: PluginFetchInit }> = []; + const json = (body: unknown, status = 200) => + new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" }, + }); + + const fetch = async (url: string, init?: PluginFetchInit) => { + calls.push({ url, init }); + if (url === `${CA_URL}/root/${root.fingerprint}`) { + return json({ ca: root.pem }); + } + if (url.startsWith(`${CA_URL}/provisioners`)) { + return json({ + provisioners: [ + { name: "jwk", type: "JWK" }, + { + name: "oidc", + type: "OIDC", + clientID: "termix", + configurationEndpoint: DISCOVERY_URL, + }, + ], + }); + } + if (url === DISCOVERY_URL) { + return json({ + authorization_endpoint: "https://idp.test/authorize", + token_endpoint: "https://idp.test/token", + }); + } + if (url === "https://idp.test/token") { + return json({ + id_token: jwt({ email: "alice@example.com", nonce: options.nonce() }), + }); + } + if (url === `${CA_URL}/1.0/ssh/sign`) { + if (options.signStatus && options.signStatus !== 200) { + return json({ message: "not allowed" }, options.signStatus); + } + const body = JSON.parse(init?.body ?? "{}"); + const now = Math.floor(Date.now() / 1000); + return json({ + crt: ca.sign({ + publicKeyBlob: body.publicKey, + keyId: body.keyID, + principals: options.principals ?? body.principals, + validAfter: now - 60, + validBefore: now + 16 * 3600, + }), + }); + } + return new Response("not found", { status: 404 }); + }; + + return { fetch, calls, root, ca }; +} diff --git a/plugins/step-ca/tests/backend/helpers.ts b/plugins/step-ca/tests/backend/helpers.ts new file mode 100644 index 000000000..9a740cfa1 --- /dev/null +++ b/plugins/step-ca/tests/backend/helpers.ts @@ -0,0 +1,105 @@ +import http from "node:http"; +import type { AddressInfo } from "node:net"; +import { fileURLToPath } from "node:url"; +import express, { type Router } from "express"; +import { + createMockCtx, + createTestDb, + type MockPluginContext, + type TestDb, +} from "@termix/plugin-sdk/testing"; +import type { + PluginFetchInit, + PluginSshAuthProvider, +} from "@termix/plugin-sdk/backend"; +import type { PluginManifest } from "@termix/plugin-sdk/manifest"; +import manifestJson from "../../manifest.json"; + +export const pluginDir = fileURLToPath(new URL("../..", import.meta.url)); +export const manifest = manifestJson as unknown as PluginManifest; + +/** A fake terminal socket that records what it was sent. */ +export function fakeSocket() { + const sent: Array> = []; + const closers: Array<() => void> = []; + return { + sent, + socket: { + send: (data: string) => sent.push(JSON.parse(data)), + on: (_event: "close", listener: () => void) => closers.push(listener), + }, + close: () => closers.forEach((listener) => listener()), + }; +} + +export interface TestServer { + db: TestDb; + mock: MockPluginContext; + provider: () => PluginSshAuthProvider; + get: (path: string) => Promise<{ status: number; body: string }>; + close: () => Promise; +} + +export async function startServer( + options: { + settings?: Record; + fetch?: (url: string, init?: PluginFetchInit) => Promise; + } = {}, +): Promise { + const db = await createTestDb(pluginDir); + db.sqlite + .prepare("INSERT OR IGNORE INTO users (id) VALUES (?)") + .run("user-1"); + db.sqlite.prepare("INSERT OR IGNORE INTO ssh_data (id) VALUES (?)").run(7); + + let router: Router | null = null; + const mock = createMockCtx({ + pluginId: manifest.id, + manifest, + capabilities: manifest.capabilities, + db: db.database, + router: () => (router = express.Router()), + settings: options.settings, + fetch: options.fetch, + baseUrl: "https://termix.test", + }); + + const { activate } = await import("../../src/backend/index.js"); + await activate(mock.ctx); + + const app = express(); + app.use((req, res, next) => router!(req, res, next)); + const server = http.createServer(app); + await new Promise((resolve) => server.listen(0, resolve)); + const { port } = server.address() as AddressInfo; + + return { + db, + mock, + provider: () => + mock.auth.sshAuthProviders.find( + (provider) => provider.type === "stepca", + )!, + async get(path) { + const response = await fetch(`http://127.0.0.1:${port}${path}`, { + redirect: "manual", + }); + return { status: response.status, body: await response.text() }; + }, + async close() { + await new Promise((resolve) => server.close(() => resolve())); + db.close(); + }, + }; +} + +export const env = (hostId = 7) => ({ + client: {}, + userId: "user-1", + hostId, + purpose: "terminal", + interactive: true, + log: () => {}, +}); + +export const HOST = { id: 7, ip: "10.0.0.7", port: 22, username: "root" }; diff --git a/plugins/step-ca/tests/backend/step-ca.test.ts b/plugins/step-ca/tests/backend/step-ca.test.ts new file mode 100644 index 000000000..69164bffe --- /dev/null +++ b/plugins/step-ca/tests/backend/step-ca.test.ts @@ -0,0 +1,295 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { PluginCapabilityError } from "@termix/plugin-sdk/backend"; +import { createMockCtx, createTestDb } from "@termix/plugin-sdk/testing"; +import { CA_URL, createFakeCa } from "./fake-ca.js"; +import { + HOST, + env, + fakeSocket, + manifest, + pluginDir, + startServer, + type TestServer, +} from "./helpers.js"; + +const certs = vi.hoisted(() => ({ applyCertificateAuth: vi.fn() })); +vi.mock("@termix/plugin-sdk/ssh-certs", () => certs); + +let server: TestServer | null = null; +let nonce = ""; + +beforeEach(() => { + certs.applyCertificateAuth.mockReset(); + nonce = ""; +}); + +afterEach(async () => { + await server?.close(); + server = null; +}); + +async function startWithCa( + options: { + principals?: string[]; + signStatus?: number; + wrongNonce?: boolean; + settings?: Record; + } = {}, +) { + const ca = createFakeCa({ + principals: options.principals, + signStatus: options.signStatus, + nonce: () => (options.wrongNonce ? "other" : nonce), + }); + server = await startServer({ + fetch: ca.fetch, + settings: { + caUrl: CA_URL, + fingerprint: ca.root.fingerprint, + provisioner: "oidc", + ...options.settings, + }, + }); + return ca; +} + +/** Starts a sign-in and returns the socket and the authorization URL. */ +async function startSignIn() { + const socket = fakeSocket(); + await server!.provider().startInteraction!({ + userId: "user-1", + hostId: 7, + host: { name: "box", ip: "10.0.0.7", username: "root" }, + socket: socket.socket, + requestOrigin: "https://termix.test", + payload: {}, + }); + const chooser = socket.sent.find((m) => m.stage === "chooser"); + const url = chooser ? new URL(String(chooser.url)) : null; + nonce = url?.searchParams.get("nonce") ?? ""; + return { socket, url, state: url?.searchParams.get("state") ?? "" }; +} + +describe("the certs table", () => { + it("is created on a fresh install", async () => { + const db = await createTestDb(pluginDir); + expect( + db.sqlite + .prepare( + "SELECT name FROM sqlite_master WHERE name = 'p_step_ca_certs'", + ) + .get(), + ).toBeTruthy(); + db.close(); + }); +}); + +describe("activate", () => { + it("registers the stepca auth type and its callback route", async () => { + server = await startServer(); + expect(server.provider()).toMatchObject({ + type: "stepca", + interaction: "stepca", + supportsBackground: false, + }); + expect(server.mock.httpRouters).toHaveLength(1); + }); + + it.each(["db:own", "auth:provide", "network:serve"])( + "fails closed without %s", + async (capability) => { + const mock = createMockCtx({ + pluginId: manifest.id, + manifest, + capabilities: manifest.capabilities.filter((c) => c !== capability), + db: (await createTestDb(pluginDir)).database, + }); + const { activate } = await import("../../src/backend/index.js"); + await expect(activate(mock.ctx)).rejects.toThrow(PluginCapabilityError); + }, + ); +}); + +describe("issuing a certificate", () => { + it("runs the OIDC flow against the CA and stores the certificate sealed", async () => { + const ca = await startWithCa(); + const { socket, url, state } = await startSignIn(); + + expect(url?.origin + url?.pathname).toBe("https://idp.test/authorize"); + expect(url?.searchParams.get("client_id")).toBe("termix"); + expect(url?.searchParams.get("redirect_uri")).toBe( + "https://termix.test/plugin-api/step-ca/callback", + ); + expect(url?.searchParams.get("code_challenge_method")).toBe("S256"); + + // The root is fetched unverified and checked by fingerprint, then pinned. + expect(ca.calls[0].init?.tls).toEqual({ rejectUnauthorized: false }); + expect(ca.calls[1].init?.tls).toEqual({ ca: ca.root.pem }); + + const response = await server!.get(`/callback?state=${state}&code=abc`); + expect(response.status).toBe(200); + expect(response.body).toContain("Signed in"); + expect(socket.sent.at(-1)).toMatchObject({ + type: "stepca_completed", + requestId: state, + }); + + const sign = ca.calls.find((call) => call.url.endsWith("/1.0/ssh/sign")); + expect(JSON.parse(sign!.init!.body!)).toMatchObject({ + certType: "user", + principals: ["root"], + keyID: "alice@example.com", + }); + expect(sign!.init!.tls).toEqual({ ca: ca.root.pem }); + + const row = server!.db.sqlite + .prepare("SELECT * FROM p_step_ca_certs") + .get() as Record; + expect(row.user_id).toBe("user-1"); + expect(row.email).toBe("alice@example.com"); + expect(row.ssh_cert).toMatch(/^sealed:/); + expect(row.private_key).toMatch(/^sealed:/); + }); + + it("answers a second use of the same callback with an error", async () => { + await startWithCa(); + const { state } = await startSignIn(); + await server!.get(`/callback?state=${state}&code=abc`); + const again = await server!.get(`/callback?state=${state}&code=abc`); + expect(again.status).toBe(400); + expect(again.body).toContain("no longer active"); + }); + + it("refuses an id token issued for another sign-in", async () => { + await startWithCa({ wrongNonce: true }); + const { socket, state } = await startSignIn(); + const response = await server!.get(`/callback?state=${state}&code=abc`); + expect(response.status).toBe(400); + expect(socket.sent.at(-1)).toMatchObject({ + type: "stepca_error", + error: expect.stringContaining("does not match"), + }); + expect( + server!.db.sqlite.prepare("SELECT * FROM p_step_ca_certs").all(), + ).toHaveLength(0); + }); + + it("reports the CA refusing to sign", async () => { + await startWithCa({ signStatus: 403 }); + const { socket, state } = await startSignIn(); + await server!.get(`/callback?state=${state}&code=abc`); + expect(socket.sent.at(-1)).toMatchObject({ + type: "stepca_error", + error: expect.stringContaining("HTTP 403"), + }); + }); + + it("rejects a certificate without the host username", async () => { + await startWithCa({ principals: ["someone-else"] }); + const { socket, state } = await startSignIn(); + await server!.get(`/callback?state=${state}&code=abc`); + expect(socket.sent.at(-1)).toMatchObject({ + type: "stepca_error", + error: expect.stringContaining("host username"), + }); + }); + + it("passes an identity provider error through", async () => { + await startWithCa(); + const { socket, state } = await startSignIn(); + const response = await server!.get( + `/callback?state=${state}&error=access_denied&error_description=Denied`, + ); + expect(response.status).toBe(400); + expect(socket.sent.at(-1)).toMatchObject({ + type: "stepca_error", + error: "Step CA: Denied", + }); + }); + + it("sends the 2.8 redirect URI while legacyCallback is on", async () => { + await startWithCa({ settings: { legacyCallback: true } }); + const { url } = await startSignIn(); + expect(url?.searchParams.get("redirect_uri")).toBe( + "https://termix.test/host/step-ca-callback", + ); + }); + + it("tells the terminal when Step CA is not configured", async () => { + server = await startServer(); + const { socket } = await startSignIn(); + expect(socket.sent).toEqual([ + expect.objectContaining({ type: "stepca_config_error" }), + ]); + }); + + it("lets a pending sign-in be cancelled", async () => { + await startWithCa(); + const { state } = await startSignIn(); + await server!.provider().cancelInteraction!({ + userId: "user-1", + requestId: state, + }); + const response = await server!.get(`/callback?state=${state}&code=abc`); + expect(response.status).toBe(400); + }); +}); + +describe("the stepca provider", () => { + it("asks for a sign-in without a certificate", async () => { + server = await startServer(); + const outcome = await server.provider().prepare({}, HOST, env()); + expect(outcome).toMatchObject({ + status: "interaction-required", + interaction: "stepca", + }); + expect(certs.applyCertificateAuth).not.toHaveBeenCalled(); + }); + + it("connects with the issued certificate", async () => { + await startWithCa(); + const { state } = await startSignIn(); + await server!.get(`/callback?state=${state}&code=abc`); + + const config: Record = {}; + const outcome = await server!.provider().prepare(config, HOST, env()); + expect(outcome).toEqual({ status: "ready" }); + const [passedConfig, , credentials, username] = + certs.applyCertificateAuth.mock.calls[0]; + expect(passedConfig).toBe(config); + expect(username).toBe("root"); + expect(credentials.certificate).toMatch( + /^ssh-ed25519-cert-v01@openssh\.com /, + ); + expect(credentials.privateKey).toContain("BEGIN PRIVATE KEY"); + }); + + it("forgets an expired certificate", async () => { + await startWithCa(); + const { state } = await startSignIn(); + await server!.get(`/callback?state=${state}&code=abc`); + server!.db.sqlite + .prepare("UPDATE p_step_ca_certs SET expires_at = ?") + .run("2000-01-01T00:00:00.000Z"); + const outcome = await server!.provider().prepare({}, HOST, env()); + expect(outcome.status).toBe("interaction-required"); + expect( + server!.db.sqlite.prepare("SELECT * FROM p_step_ca_certs").all(), + ).toHaveLength(0); + }); + + it("drops the certificate when the host rejects it", async () => { + await startWithCa(); + const { state } = await startSignIn(); + await server!.get(`/callback?state=${state}&code=abc`); + const decision = server!.provider().onAuthFailed!(HOST, env(), { + error: new Error("All configured authentication methods failed"), + } as never); + expect(decision).toMatchObject({ status: "interaction-required" }); + await vi.waitFor(() => + expect( + server!.db.sqlite.prepare("SELECT * FROM p_step_ca_certs").all(), + ).toHaveLength(0), + ); + }); +}); diff --git a/plugins/step-ca/tests/frontend/activate.test.tsx b/plugins/step-ca/tests/frontend/activate.test.tsx new file mode 100644 index 000000000..b9d8852da --- /dev/null +++ b/plugins/step-ca/tests/frontend/activate.test.tsx @@ -0,0 +1,137 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { act, fireEvent, render, screen } from "@testing-library/react"; +import { + renderWithApp, + type RenderedPluginApp, +} from "@termix/plugin-sdk/testing"; +import type { PluginManifest } from "@termix/plugin-sdk/manifest"; +import * as plugin from "../../src/frontend/index"; +import { StepCaOverlay } from "../../src/frontend/StepCaOverlay"; +import { redirectUri } from "../../src/frontend/RedirectUriSetting"; +import manifestJson from "../../manifest.json"; +import locales from "../../locales/en.json"; + +vi.mock("@termix/plugin-sdk/frontend", async (importOriginal) => ({ + ...(await importOriginal()), + useTranslation: () => ({ t: (key: string) => key }), +})); + +const manifest = manifestJson as unknown as PluginManifest; + +let rendered: RenderedPluginApp | null = null; + +afterEach(async () => { + await rendered?.deactivate(); + rendered = null; +}); + +describe("step-ca activate", () => { + it("registers the auth editor, the terminal overlay and the settings field", async () => { + rendered = await renderWithApp(plugin, { manifest, locales }); + expect(rendered.registered.settingsComponents()).toEqual(["redirectUri"]); + expect(rendered.registered.slot("terminal.overlay")).toEqual([ + "step-ca.signIn", + ]); + await rendered.deactivate(); + expect(rendered.registered.settingsComponents()).toEqual([]); + expect(rendered.registered.slot("terminal.overlay")).toEqual([]); + rendered = null; + }); +}); + +describe("redirectUri", () => { + it("names the plugin callback, or the 2.8 one", () => { + expect(redirectUri(false, "https://t.example/app/")).toBe( + "https://t.example/app/plugin-api/step-ca/callback", + ); + expect(redirectUri(true, "https://t.example/")).toBe( + "https://t.example/host/step-ca-callback", + ); + }); +}); + +describe("StepCaOverlay", () => { + function setup() { + let listener: (message: { + type: string; + [key: string]: unknown; + }) => void = () => {}; + const props = { + host: { id: 7 }, + subscribe: vi.fn((next: typeof listener) => { + listener = next; + return () => {}; + }), + holdConnectTimeout: vi.fn(), + fail: vi.fn(), + disconnect: vi.fn(), + send: vi.fn(), + connectPayload: vi.fn(() => ({ hostId: 7, cols: 80, rows: 24 })), + }; + render(); + const emit = (message: { type: string; [key: string]: unknown }) => + act(() => listener(message)); + return { props, emit }; + } + + it("starts the sign-in once, then reports a second request as a failure", () => { + const { props, emit } = setup(); + emit({ type: "stepca_auth_required", hostId: 7 }); + expect(props.send).toHaveBeenCalledWith("stepca_start_auth", { hostId: 7 }); + expect(props.holdConnectTimeout).toHaveBeenCalledWith(true); + + emit({ type: "stepca_auth_required", hostId: 7 }); + expect(props.fail).toHaveBeenCalledWith("errors.authFailed"); + }); + + it("opens the identity provider, waits, and reconnects once signed in", () => { + const open = vi.spyOn(window, "open").mockReturnValue(null); + const { props, emit } = setup(); + emit({ type: "stepca_auth_required", hostId: 7 }); + emit({ + type: "stepca_status", + stage: "chooser", + requestId: "req-1", + url: "https://idp.test/authorize?state=req-1", + }); + fireEvent.click(screen.getByText("dialog.openBrowser")); + expect(open).toHaveBeenCalledWith( + "https://idp.test/authorize?state=req-1", + "_blank", + ); + expect(screen.getByText("dialog.waiting")).toBeTruthy(); + + emit({ type: "stepca_completed", requestId: "req-1" }); + expect(props.send).toHaveBeenCalledWith("stepca_auth_completed", { + hostId: 7, + cols: 80, + rows: 24, + }); + expect(screen.queryByText("dialog.title")).toBeNull(); + open.mockRestore(); + }); + + it("cancels a pending sign-in", () => { + const { props, emit } = setup(); + emit({ + type: "stepca_status", + stage: "chooser", + requestId: "req-2", + url: "https://idp.test/authorize", + }); + fireEvent.click(screen.getByText("common.cancel")); + expect(props.send).toHaveBeenCalledWith("stepca_cancel", { + requestId: "req-2", + }); + }); + + it("shows a config error", () => { + const { emit } = setup(); + emit({ + type: "stepca_config_error", + requestId: "", + error: "Step CA is not configured.", + }); + expect(screen.getByText("Step CA is not configured.")).toBeTruthy(); + }); +}); diff --git a/plugins/step-ca/tsconfig.json b/plugins/step-ca/tsconfig.json new file mode 100644 index 000000000..7b35314c3 --- /dev/null +++ b/plugins/step-ca/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@termix/plugin-sdk/tsconfig.plugin.json", + "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"] +} diff --git a/plugins/step-ca/vitest.config.ts b/plugins/step-ca/vitest.config.ts new file mode 100644 index 000000000..31ba004fc --- /dev/null +++ b/plugins/step-ca/vitest.config.ts @@ -0,0 +1,3 @@ +import { pluginVitestConfig } from "@termix/plugin-sdk/vitest-preset"; + +export default pluginVitestConfig(import.meta.url); diff --git a/src/backend/auth/legacy-providers.ts b/src/backend/auth/legacy-providers.ts index bc499b239..cc68fee11 100644 --- a/src/backend/auth/legacy-providers.ts +++ b/src/backend/auth/legacy-providers.ts @@ -15,80 +15,11 @@ import type { SshConnectHost, } from "../hosts/connect/types.js"; -const STEPCA_REQUIRED_MESSAGE = - "Step CA authentication required. Please open a Terminal connection to this host first to complete browser-based authentication."; - const VAULT_REQUIRED_MESSAGE = "Vault SSH signer authentication required. Please open a Terminal connection first."; const AUTH_FAILED_PATTERN = /All configured authentication methods failed/i; -const stepCaProvider: SshAuthProvider = { - type: "stepca", - pluginId: "core", - labelKey: "hosts.filterAuthStepca", - needsUserInteraction: true, - supportsBackground: false, - interaction: "stepca", - prepare: async (config, host, env) => { - const { getStepCaCert } = await import("../hosts/step-ca-auth.js"); - const cert = getStepCaCert(env.userId, env.hostId); - if (!cert) { - env.log("info", "No valid certificate found, requesting sign-in"); - return { - status: "interaction-required", - interaction: "stepca", - message: STEPCA_REQUIRED_MESSAGE, - }; - } - try { - await applyCertificateAuth( - config, - env.client, - { privateKey: cert.privateKey, certificate: cert.sshCert }, - host.username, - ); - } catch (error) { - return { - status: "error", - code: "failed", - message: "Step CA authentication failed: " + getErrorMessage(error), - }; - } - env.log("info", "Using cached SSH certificate"); - return { status: "ready" }; - }, - onAuthFailed: (_host, env, context) => { - if (!AUTH_FAILED_PATTERN.test(context.error.message)) return; - void import("../hosts/step-ca-auth.js") - .then(({ invalidateStepCaCert }) => - invalidateStepCaCert(env.userId, env.hostId), - ) - .catch(() => {}); - return { - status: "interaction-required", - interaction: "stepca", - message: - "Step CA authentication failed or expired. Please authenticate again.", - }; - }, - startInteraction: async (request) => { - const { startStepCaAuth } = await import("../hosts/step-ca-auth.js"); - await startStepCaAuth( - request.userId, - request.hostId, - request.host.username, - request.socket, - request.requestOrigin, - ); - }, - cancelInteraction: async (request) => { - if (!request.requestId) return; - const { cancelStepCaAuth } = await import("../hosts/step-ca-auth.js"); - cancelStepCaAuth(request.requestId); - }, -}; - function vaultProfileId(host: SshConnectHost): number | undefined { return host.vaultProfile?.id ?? undefined; } @@ -192,7 +123,6 @@ export function registerLegacySshAuthProviders(): void { if (sshRegistered) return; sshRegistered = true; - registerSshAuthProvider(stepCaProvider); registerSshAuthProvider(vaultProvider); } diff --git a/src/backend/database/routes/host-compat-routes.ts b/src/backend/database/routes/host-compat-routes.ts index 07ad6dedf..dfc808f04 100644 --- a/src/backend/database/routes/host-compat-routes.ts +++ b/src/backend/database/routes/host-compat-routes.ts @@ -1,8 +1,8 @@ /** * The 2.8 URLs under /host that something outside Termix still depends on: - * the OPKSSH redirect URI registered with identity providers. It forwards to - * the plugin route that replaced it, so with the plugin off it signs nobody - * in. + * the OPKSSH and Step CA redirect URIs registered with identity providers. + * Each forwards to the plugin route that replaced it, so with the plugin off + * it signs nobody in. */ import type { Request, Router } from "express"; @@ -33,4 +33,20 @@ export function registerHostAuthCompatRoutes(router: Router): void { const rest = req.path.slice("/opkssh-callback".length); res.redirect(307, localUrl(req, `/plugin-api/opkssh/callback${rest}`)); }); + + /** + * @openapi + * /host/step-ca-callback: + * get: + * summary: Step CA callback (2.8 URL) + * description: The redirect URI Step CA identity providers were set up with before 2.9. Redirects to /plugin-api/step-ca/callback with the same query, so existing provider configurations keep working. + * tags: + * - Auth + * responses: + * 307: + * description: Redirect to the step-ca plugin's callback. + */ + router.get("/step-ca-callback", (req, res) => { + res.redirect(307, localUrl(req, "/plugin-api/step-ca/callback")); + }); } diff --git a/src/backend/database/routes/host-step-ca-routes.ts b/src/backend/database/routes/host-step-ca-routes.ts deleted file mode 100644 index 164236e37..000000000 --- a/src/backend/database/routes/host-step-ca-routes.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { Request, Response, Router } from "express"; - -function escapeHtml(value: string): string { - return value - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} - -function resultPage(ok: boolean, message: string): string { - return `Termix - -

${ok ? "Signed in" : "Sign-in failed"}

${escapeHtml(message)}

`; -} - -/** - * The OIDC redirect target for Step CA sign-ins. Unauthenticated on purpose: - * the browser that finishes the sign-in may not be the one running Termix, - * so the request is matched to its session by the OAuth state. - */ -export function registerHostStepCaRoutes(router: Router): void { - router.get("/step-ca-callback", async (req: Request, res: Response) => { - const { completeStepCaAuth } = await import("../../hosts/step-ca-auth.js"); - const stringQuery = (name: string): string | undefined => { - const value = req.query[name]; - return typeof value === "string" ? value : undefined; - }; - const result = await completeStepCaAuth({ - state: stringQuery("state"), - code: stringQuery("code"), - error: stringQuery("error"), - error_description: stringQuery("error_description"), - }); - res - .status(result.ok ? 200 : 400) - .type("html") - .send(resultPage(result.ok, result.message)); - }); -} diff --git a/src/backend/database/routes/host.ts b/src/backend/database/routes/host.ts index 1f4b08e80..a8c32ab4f 100644 --- a/src/backend/database/routes/host.ts +++ b/src/backend/database/routes/host.ts @@ -46,7 +46,6 @@ import { } from "./host-plugin-settings.js"; import { validateParentHostId } from "./host-parent-validation.js"; import { registerHostAuthCompatRoutes } from "./host-compat-routes.js"; -import { registerHostStepCaRoutes } from "./host-step-ca-routes.js"; import { registerHostFolderRoutes } from "./host-folder-routes.js"; import { registerHostAutostartRoutes } from "./host-autostart-routes.js"; import { registerHostInternalRoutes } from "./host-internal-routes.js"; @@ -2661,7 +2660,6 @@ registerHostAutostartRoutes(router, { }); registerHostAuthCompatRoutes(router); -registerHostStepCaRoutes(router); registerHostNetworkRoutes(router, { authenticateJWT, diff --git a/src/backend/database/routes/user-settings-routes.ts b/src/backend/database/routes/user-settings-routes.ts index d5cae300e..c82f6e67a 100644 --- a/src/backend/database/routes/user-settings-routes.ts +++ b/src/backend/database/routes/user-settings-routes.ts @@ -1,5 +1,4 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; -import { getErrorMessage } from "../../utils/error-message.js"; import type { RequestHandler, Router } from "express"; import { authLogger, @@ -13,7 +12,6 @@ import { AUDIT_FORWARD_URL_SETTING, } from "../../utils/audit-forwarder.js"; import { getTelemetryEnvOverride } from "../../utils/analytics.js"; -import { STEP_CA_PRIVATE_ALLOWLIST_KEY } from "../../utils/step-ca-egress.js"; import { NOTIFICATION_PRIVATE_ALLOWLIST_KEY, parseNotificationAllowlist, @@ -464,7 +462,7 @@ export function registerUserSettingsRoutes( /** * GET/PATCH a comma-list of private hosts an outbound feature may reach. - * Shared by notifications and Step CA; each keeps its own setting key. + * One route pair per private endpoint allowlist, each with its own setting key. */ const registerPrivateEndpointAllowlist = ( path: string, @@ -562,131 +560,6 @@ export function registerUserSettingsRoutes( "notification endpoint", ); - /** - * @openapi - * /users/step-ca-private-endpoints: - * get: - * summary: Get the private hosts the Step CA certificate flow may contact (admin only) - * tags: - * - Users - * patch: - * summary: Replace that allowlist (admin only) - * tags: - * - Users - */ - registerPrivateEndpointAllowlist( - "/step-ca-private-endpoints", - STEP_CA_PRIVATE_ALLOWLIST_KEY, - "update_step_ca_private_endpoints", - "Step CA endpoint", - ); - - /** - * @openapi - * /users/step-ca-settings: - * get: - * summary: Step CA settings. Admins get the values; everyone else only whether it is configured. - * tags: - * - Users - * patch: - * summary: Set the Step CA URL, root fingerprint and OIDC provisioner (admin only). Empty values clear the configuration. - * tags: - * - Users - */ - router.get("/step-ca-settings", authenticateJWT, async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; - try { - const { readStepCaSettings } = - await import("../../hosts/step-ca-auth.js"); - const settings = await readStepCaSettings(); - if (!(await getAdminActor(userId))) { - return res.json({ configured: settings !== null }); - } - res.json({ - configured: settings !== null, - caUrl: settings?.caUrl ?? "", - fingerprint: settings?.fingerprint ?? "", - provisioner: settings?.provisioner ?? "", - }); - } catch (err) { - authLogger.error("Failed to get Step CA settings", err); - res.status(500).json({ error: "Failed to get Step CA settings" }); - } - }); - - router.patch("/step-ca-settings", authenticateJWT, async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; - try { - const actor = await getAdminActor(userId); - if (!actor) { - return res.status(403).json({ error: "Not authorized" }); - } - const { caUrl, fingerprint, provisioner } = req.body ?? {}; - if ( - [caUrl, fingerprint, provisioner].some((v) => typeof v !== "string") - ) { - return res.status(400).json({ - error: "caUrl, fingerprint and provisioner must be strings", - }); - } - const values = { - caUrl: caUrl.trim(), - fingerprint: fingerprint.trim(), - provisioner: provisioner.trim(), - }; - const clearing = - !values.caUrl && !values.fingerprint && !values.provisioner; - if (!clearing) { - const { normalizeCaUrl, normalizeFingerprint } = - await import("../../utils/step-ca-client.js"); - try { - values.caUrl = normalizeCaUrl(values.caUrl); - values.fingerprint = normalizeFingerprint(values.fingerprint); - } catch (err) { - return res.status(400).json({ error: getErrorMessage(err) }); - } - if (!values.provisioner) { - return res.status(400).json({ error: "provisioner is required" }); - } - } - - const { STEP_CA_SETTING_KEYS } = - await import("../../hosts/step-ca-auth.js"); - const settings = createCurrentSettingsRepository(); - if (clearing) { - await settings.delete(STEP_CA_SETTING_KEYS.url); - await settings.delete(STEP_CA_SETTING_KEYS.fingerprint); - await settings.delete(STEP_CA_SETTING_KEYS.provisioner); - } else { - await settings.set(STEP_CA_SETTING_KEYS.url, values.caUrl); - await settings.set( - STEP_CA_SETTING_KEYS.fingerprint, - values.fingerprint, - ); - await settings.set( - STEP_CA_SETTING_KEYS.provisioner, - values.provisioner, - ); - } - - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId, - username: actor.username ?? userId, - action: "update_step_ca_settings", - resourceType: "setting", - details: JSON.stringify({ configured: !clearing, caUrl: values.caUrl }), - ipAddress, - userAgent, - success: true, - }); - res.json({ configured: !clearing, ...values }); - } catch (err) { - authLogger.error("Failed to update Step CA settings", err); - res.status(500).json({ error: "Failed to update Step CA settings" }); - } - }); - /** * @openapi * /users/host-defaults: diff --git a/src/backend/hosts/step-ca-auth.ts b/src/backend/hosts/step-ca-auth.ts deleted file mode 100644 index fd49d0269..000000000 --- a/src/backend/hosts/step-ca-auth.ts +++ /dev/null @@ -1,376 +0,0 @@ -import { randomBytes } from "crypto"; -import type { WebSocket } from "ws"; -import { sshLogger } from "../utils/logger.js"; -import { getErrorMessage } from "../utils/error-message.js"; -import { createCurrentSettingsRepository } from "../database/repositories/factory.js"; -import { readStepCaPrivateAllowlist } from "../utils/step-ca-egress.js"; -import { - stepCaRuntime, - type StepCaCallbackQuery, - type StepCaCallbackResult, -} from "./step-ca-runtime.js"; -import { - buildAuthorizationUrl, - createPkce, - decodeJwtClaims, - discoverOidcEndpoints, - exchangeCodeForIdToken, - fetchRootCertificate, - findOidcProvisioner, - generateSshKeyPair, - parseSshCertificate, - signSshCertificate, - type StepCaTarget, -} from "../utils/step-ca-client.js"; - -/** - * Step CA (smallstep) SSH user certificates through its OIDC provisioner. - * - * The CA's HTTP API plus one OIDC redirect back to Termix. Issued - * certificates are kept in memory until they expire. - */ - -export const STEP_CA_CALLBACK_PATH = "/host/step-ca-callback"; -export const STEP_CA_SETTING_KEYS = { - url: "step_ca_url", - fingerprint: "step_ca_fingerprint", - provisioner: "step_ca_provisioner", -} as const; - -const AUTH_TIMEOUT_MS = 5 * 60 * 1000; -const REMOTE_CALLBACK_WAIT_MS = 30_000; -const REMOTE_POLL_MS = 250; - -export interface StepCaSettings { - caUrl: string; - fingerprint: string; - provisioner: string; -} - -export async function readStepCaSettings(): Promise { - const settings = createCurrentSettingsRepository(); - const [caUrl, fingerprint, provisioner] = await Promise.all([ - settings.get(STEP_CA_SETTING_KEYS.url), - settings.get(STEP_CA_SETTING_KEYS.fingerprint), - settings.get(STEP_CA_SETTING_KEYS.provisioner), - ]); - if (!caUrl || !fingerprint || !provisioner) return null; - return { caUrl, fingerprint, provisioner }; -} - -interface StepCaAuthSession { - state: string; - userId: string; - hostId: number; - username: string; - ws: WebSocket; - target: StepCaTarget; - rootPem: string; - clientId: string; - clientSecret?: string; - tokenEndpoint: string; - redirectUri: string; - codeVerifier: string; - nonce: string; - keyPair: { publicKeyLine: string; privateKeyPem: string }; - timeout: NodeJS.Timeout; - commandPoll: NodeJS.Timeout | null; - processing: boolean; - completed: boolean; -} - -const sessions = new Map(); - -interface IssuedCertificate { - sshCert: string; - privateKey: string; - expiresAt: number; -} - -const issued = new Map(); - -function certKey(userId: string, hostId: number): string { - return `${userId}:${hostId}`; -} - -/** The cached certificate for this user and host, while it is valid. */ -export function getStepCaCert( - userId: string, - hostId: number, -): { sshCert: string; privateKey: string } | null { - const key = certKey(userId, hostId); - const cert = issued.get(key); - if (!cert) return null; - if (cert.expiresAt <= Date.now()) { - issued.delete(key); - return null; - } - return { sshCert: cert.sshCert, privateKey: cert.privateKey }; -} - -export function invalidateStepCaCert(userId: string, hostId: number): void { - issued.delete(certKey(userId, hostId)); -} - -function send(ws: WebSocket, message: object): void { - try { - ws.send(JSON.stringify(message)); - } catch { - /* socket already gone */ - } -} - -function endSession(session: StepCaAuthSession, removeRuntime = true): void { - clearTimeout(session.timeout); - if (session.commandPoll) clearInterval(session.commandPoll); - sessions.delete(session.state); - if (removeRuntime) void stepCaRuntime.remove(session.state); -} - -export async function startStepCaAuth( - userId: string, - hostId: number, - username: string, - ws: WebSocket, - requestOrigin: string, -): Promise { - const settings = await readStepCaSettings(); - if (!settings) { - send(ws, { - type: "stepca_config_error", - requestId: "", - error: - "Step CA is not configured. An administrator must set the CA URL, root fingerprint and OIDC provisioner under Admin Settings.", - }); - return; - } - - const state = randomBytes(24).toString("base64url"); - try { - const target: StepCaTarget = { - caUrl: settings.caUrl, - fingerprint: settings.fingerprint, - allowedPrivateHosts: await readStepCaPrivateAllowlist(), - }; - const rootPem = await fetchRootCertificate(target); - const provisioner = await findOidcProvisioner( - target, - rootPem, - settings.provisioner, - ); - const endpoints = await discoverOidcEndpoints( - provisioner.configurationEndpoint, - target.allowedPrivateHosts, - ); - const pkce = createPkce(); - const nonce = randomBytes(16).toString("base64url"); - const redirectUri = `${requestOrigin}${STEP_CA_CALLBACK_PATH}`; - - const session: StepCaAuthSession = { - state, - userId, - hostId, - username, - ws, - target, - rootPem, - clientId: provisioner.clientID, - clientSecret: provisioner.clientSecret, - tokenEndpoint: endpoints.tokenEndpoint, - redirectUri, - codeVerifier: pkce.verifier, - nonce, - keyPair: generateSshKeyPair(), - processing: false, - completed: false, - commandPoll: null, - timeout: setTimeout(() => { - const current = sessions.get(state); - if (!current || current.completed || current.processing) return; - send(ws, { type: "stepca_timeout", requestId: state }); - endSession(current); - }, AUTH_TIMEOUT_MS), - }; - sessions.set(state, session); - await stepCaRuntime.register(state); - session.commandPoll = setInterval(() => { - if (session.processing || session.completed) return; - void stepCaRuntime.takeCommand(state).then(async (query) => { - if (!query || session.processing || session.completed) return; - session.processing = true; - const result = await finishStepCaAuth(session, query, false); - await stepCaRuntime.complete(state, result); - }); - }, REMOTE_POLL_MS); - session.commandPoll.unref(); - ws.once("close", () => { - const current = sessions.get(state); - if (current && !current.completed) endSession(current); - }); - - send(ws, { - type: "stepca_status", - requestId: state, - stage: "chooser", - label: "Step CA", - url: buildAuthorizationUrl({ - authorizationEndpoint: endpoints.authorizationEndpoint, - clientId: provisioner.clientID, - redirectUri, - state, - nonce, - codeChallenge: pkce.challenge, - }), - providers: [], - }); - } catch (error) { - sshLogger.error("Failed to start Step CA authentication", error, { - operation: "step_ca_start_error", - userId, - hostId, - }); - send(ws, { - type: "stepca_error", - requestId: state, - error: `Step CA: ${getErrorMessage(error)}`, - }); - } -} - -export function cancelStepCaAuth(requestId: string): boolean { - const session = sessions.get(requestId); - if (!session) return false; - endSession(session); - return true; -} - -/** - * Finishes the flow once the identity provider redirects back: exchanges - * the code, has the CA sign the key, caches the certificate and tells the - * terminal to reconnect. - */ -export async function completeStepCaAuth( - query: StepCaCallbackQuery, -): Promise { - const session = query.state ? sessions.get(query.state) : undefined; - if (!session) { - if (!query.state || !(await stepCaRuntime.submit(query.state, query))) { - return { - ok: false, - message: "This sign-in request is no longer active.", - }; - } - const deadline = Date.now() + REMOTE_CALLBACK_WAIT_MS; - while (Date.now() < deadline) { - const result = await stepCaRuntime.takeResult(query.state); - if (result) return result; - await new Promise((resolve) => setTimeout(resolve, REMOTE_POLL_MS)); - } - return { - ok: false, - message: "The Termix instance handling this sign-in did not respond.", - }; - } - if (session.processing || session.completed) { - return { ok: false, message: "This sign-in request was already used." }; - } - session.processing = true; - return finishStepCaAuth(session, query); -} - -async function finishStepCaAuth( - session: StepCaAuthSession, - query: StepCaCallbackQuery, - removeRuntime = true, -): Promise { - if (query.error || !query.code) { - const message = query.error_description || query.error || "Sign-in failed"; - send(session.ws, { - type: "stepca_error", - requestId: session.state, - error: `Step CA: ${message}`, - }); - endSession(session, removeRuntime); - return { ok: false, message }; - } - - try { - send(session.ws, { - type: "stepca_status", - requestId: session.state, - stage: "authenticating", - }); - const idToken = await exchangeCodeForIdToken({ - tokenEndpoint: session.tokenEndpoint, - clientId: session.clientId, - clientSecret: session.clientSecret, - code: query.code, - redirectUri: session.redirectUri, - codeVerifier: session.codeVerifier, - allowedPrivateHosts: session.target.allowedPrivateHosts, - }); - const claims = decodeJwtClaims(idToken); - if (claims.nonce !== session.nonce) { - throw new Error("The identity token does not match this sign-in"); - } - const email = typeof claims.email === "string" ? claims.email : undefined; - - const certificate = await signSshCertificate( - session.target, - session.rootPem, - { - publicKeyLine: session.keyPair.publicKeyLine, - ott: idToken, - principals: [session.username], - keyId: email ?? session.username, - }, - ); - - const certificateInfo = parseSshCertificate(certificate); - if (certificateInfo.publicKeyLine !== session.keyPair.publicKeyLine) { - throw new Error("The CA returned a certificate for a different key"); - } - if (!certificateInfo.principals.includes(session.username)) { - throw new Error("The CA certificate does not include the host username"); - } - const now = Date.now(); - if ( - certificateInfo.validBefore.getTime() <= now || - certificateInfo.validAfter.getTime() > now + 60_000 - ) { - throw new Error( - "The CA returned a certificate outside its validity window", - ); - } - const expiresAt = certificateInfo.validBefore; - - issued.set(certKey(session.userId, session.hostId), { - sshCert: certificate, - privateKey: session.keyPair.privateKeyPem, - expiresAt: expiresAt.getTime(), - }); - - session.completed = true; - send(session.ws, { - type: "stepca_completed", - requestId: session.state, - expiresAt: expiresAt.toISOString(), - }); - endSession(session, removeRuntime); - return { ok: true, message: "Signed in. You can close this window." }; - } catch (error) { - sshLogger.error("Step CA certificate issuance failed", error, { - operation: "step_ca_complete_error", - userId: session.userId, - hostId: session.hostId, - }); - const message = getErrorMessage(error); - send(session.ws, { - type: "stepca_error", - requestId: session.state, - error: `Step CA: ${message}`, - }); - endSession(session, removeRuntime); - return { ok: false, message }; - } -} diff --git a/src/backend/hosts/step-ca-runtime.ts b/src/backend/hosts/step-ca-runtime.ts deleted file mode 100644 index a5edf57f6..000000000 --- a/src/backend/hosts/step-ca-runtime.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { createClient } from "redis"; -import { sshLogger } from "../utils/logger.js"; -import { - decryptSystemSecret, - encryptSystemSecret, -} from "../utils/system-secret-crypto.js"; - -export interface StepCaCallbackQuery { - state?: string; - code?: string; - error?: string; - error_description?: string; -} - -export interface StepCaCallbackResult { - ok: boolean; - message: string; -} - -const PREFIX = - process.env.TERMIX_STEP_CA_REDIS_PREFIX?.trim() || "termix:step-ca"; -const SESSION_TTL_SECONDS = 5 * 60; -const RESULT_TTL_SECONDS = 60; -const CONNECT_RETRY_MS = 15_000; - -export class StepCaRuntime { - private client: ReturnType | null = null; - private connecting: Promise | null = null; - private nextConnectAttempt = 0; - - async register(state: string): Promise { - if (!(await this.ensureConnected()) || !this.client) return; - await this.client - .set(this.routeKey(state), "active", { EX: SESSION_TTL_SECONDS }) - .catch((error) => this.logFailure("register", error)); - } - - async submit(state: string, query: StepCaCallbackQuery): Promise { - if (!(await this.ensureConnected()) || !this.client) return false; - try { - if (!(await this.client.exists(this.routeKey(state)))) return false; - const encrypted = await encryptSystemSecret(JSON.stringify(query)); - const stored = await this.client.set(this.commandKey(state), encrypted, { - EX: SESSION_TTL_SECONDS, - NX: true, - }); - return stored === "OK"; - } catch (error) { - this.logFailure("submit", error); - return false; - } - } - - async takeCommand(state: string): Promise { - if (!(await this.ensureConnected()) || !this.client) return null; - try { - const encrypted = await this.client.getDel(this.commandKey(state)); - if (!encrypted) return null; - return this.decode( - await decryptSystemSecret(encrypted.toString()), - ); - } catch (error) { - this.logFailure("take_command", error); - return null; - } - } - - async complete(state: string, result: StepCaCallbackResult): Promise { - if (!(await this.ensureConnected()) || !this.client) return; - try { - const encrypted = await encryptSystemSecret(JSON.stringify(result)); - await this.client - .multi() - .set(this.resultKey(state), encrypted, { EX: RESULT_TTL_SECONDS }) - .del(this.routeKey(state)) - .del(this.commandKey(state)) - .exec(); - } catch (error) { - this.logFailure("complete", error); - } - } - - async takeResult(state: string): Promise { - if (!(await this.ensureConnected()) || !this.client) return null; - try { - const encrypted = await this.client.getDel(this.resultKey(state)); - if (!encrypted) return null; - return this.decode( - await decryptSystemSecret(encrypted.toString()), - ); - } catch (error) { - this.logFailure("take_result", error); - return null; - } - } - - async remove(state: string): Promise { - if (!(await this.ensureConnected()) || !this.client) return; - await this.client - .del([ - this.routeKey(state), - this.commandKey(state), - this.resultKey(state), - ]) - .catch((error) => this.logFailure("remove", error)); - } - - async close(): Promise { - if (this.client?.isOpen) await this.client.quit(); - this.client = null; - } - - private async ensureConnected(): Promise { - const url = process.env.REDIS_URL?.trim(); - if (!url) return false; - if (this.client?.isReady) return true; - if (Date.now() < this.nextConnectAttempt) return false; - if (this.connecting) return this.connecting; - this.connecting = this.connect(url).finally(() => { - this.connecting = null; - }); - return this.connecting; - } - - private async connect(url: string): Promise { - try { - this.client = createClient({ - url, - socket: { connectTimeout: 1500, reconnectStrategy: false }, - }); - this.client.on("error", (error) => this.logFailure("client", error)); - await this.client.connect(); - this.nextConnectAttempt = 0; - return true; - } catch (error) { - this.nextConnectAttempt = Date.now() + CONNECT_RETRY_MS; - this.logFailure("connect", error); - await this.client?.disconnect().catch(() => {}); - this.client = null; - return false; - } - } - - private routeKey(state: string): string { - return `${PREFIX}:route:${state}`; - } - - private commandKey(state: string): string { - return `${PREFIX}:command:${state}`; - } - - private resultKey(state: string): string { - return `${PREFIX}:result:${state}`; - } - - private decode(raw: string): T | null { - try { - return JSON.parse(raw) as T; - } catch { - return null; - } - } - - private logFailure(operation: string, error: unknown): void { - sshLogger.warn("Step CA Redis runtime unavailable", { - operation: `step_ca_redis_${operation}`, - error: error instanceof Error ? error.message : String(error), - }); - } -} - -export const stepCaRuntime = new StepCaRuntime(); diff --git a/src/backend/plugins/ctx-notify.ts b/src/backend/plugins/ctx-notify.ts index 264892f7c..40fdf7f26 100644 --- a/src/backend/plugins/ctx-notify.ts +++ b/src/backend/plugins/ctx-notify.ts @@ -147,6 +147,12 @@ export function createPluginFetch(deps: Deps): PluginFetch { signal: controller.signal, }, (init.allowPrivateHosts ?? []).map((host) => host.toLowerCase()), + { + ...(init.tls?.ca ? { ca: init.tls.ca } : {}), + ...(init.tls?.rejectUnauthorized === false + ? { rejectUnauthorized: false } + : {}), + }, ); } finally { clearTimeout(timer); diff --git a/src/backend/starter.ts b/src/backend/starter.ts index e90de2048..bef231f20 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -377,6 +377,10 @@ async function provisionLocalDesktopUserIfNeeded(): Promise { await import("./utils/crypto-migration/warpgate-settings-migration.js"); await runWarpgateSettingsMigration(); + const { runStepCaSettingsMigration } = + await import("./utils/crypto-migration/step-ca-settings-migration.js"); + await runStepCaSettingsMigration(); + const { runSecretSourcesTokenMigration } = await import("./utils/crypto-migration/secret-sources-token-migration.js"); await runSecretSourcesTokenMigration(); diff --git a/src/backend/tests/database/routes/host-compat-routes.test.ts b/src/backend/tests/database/routes/host-compat-routes.test.ts index b32392f5b..6094be251 100644 --- a/src/backend/tests/database/routes/host-compat-routes.test.ts +++ b/src/backend/tests/database/routes/host-compat-routes.test.ts @@ -1,5 +1,5 @@ /** - * The OPKSSH redirect URI identity providers were registered with before + * The OPKSSH and Step CA redirect URIs identity providers were registered with before * 2.9 must keep reaching the plugin's callback, query intact. */ @@ -49,3 +49,16 @@ describe("the old OPKSSH callback", () => { ); }); }); + +describe("the old Step CA callback", () => { + it("forwards to the plugin's callback with the query", async () => { + const response = await fetch( + `${base}/host/step-ca-callback?code=abc&state=xyz`, + { redirect: "manual" }, + ); + expect(response.status).toBe(307); + expect(response.headers.get("location")).toBe( + "/plugin-api/step-ca/callback?code=abc&state=xyz", + ); + }); +}); diff --git a/src/backend/tests/hosts/connect/pipeline.test.ts b/src/backend/tests/hosts/connect/pipeline.test.ts index 9b5670b04..002b6291f 100644 --- a/src/backend/tests/hosts/connect/pipeline.test.ts +++ b/src/backend/tests/hosts/connect/pipeline.test.ts @@ -18,8 +18,6 @@ vi.mock("ssh2", async (importOriginal) => { const mocks = vi.hoisted(() => ({ verifier: () => {}, createHostVerifier: vi.fn(), - getStepCaCert: vi.fn(), - invalidateStepCaCert: vi.fn(), applyCertificateAuth: vi.fn(), getVaultCert: vi.fn(), deleteVaultCert: vi.fn(), @@ -40,10 +38,6 @@ vi.mock("../../../hosts/host-key-verifier.js", () => ({ createHostVerifier: mocks.createHostVerifier, }, })); -vi.mock("../../../hosts/step-ca-auth.js", () => ({ - getStepCaCert: mocks.getStepCaCert, - invalidateStepCaCert: mocks.invalidateStepCaCert, -})); vi.mock("@termix/plugin-sdk/ssh-certs", () => ({ applyCertificateAuth: mocks.applyCertificateAuth, })); @@ -276,25 +270,20 @@ describe("buildConnectConfig per auth type", () => { // OPKSSH is a plugin-registered SSH auth provider now: see // plugins/opkssh/tests/backend/provider.test.ts. - it("stepca without a cached certificate asks for a sign-in", async () => { - mocks.getStepCaCert.mockReturnValueOnce(null); - const { outcome } = await build(host({ authType: "stepca" })); - expect(outcome).toMatchObject({ - status: "interaction-required", - interaction: "stepca", - }); - expect(mocks.getStepCaCert).toHaveBeenCalledWith("user-1", 7); - }); + // Step CA is the step-ca plugin's provider: see + // plugins/step-ca/tests/backend/step-ca.test.ts. - it("stepca uses the cached certificate", async () => { - mocks.getStepCaCert.mockReturnValueOnce({ - privateKey: "k", - sshCert: "c", + it("a stepca host without the step-ca plugin names it", async () => { + setSshAuthTypeOwnerSource(() => [ + { type: "stepca", pluginId: "step-ca", pluginName: "Step CA" }, + ]); + const { outcome } = await build(host({ authType: "stepca" })); + expect(outcome).toEqual({ + status: "error", + code: "provider-missing", + message: "This host uses stepca, which needs the Step CA plugin", }); - const { config, outcome } = await build(host({ authType: "stepca" })); - expect(outcome.status).toBe("ready"); - expect(config.authHandler).toBe("cert-handler"); - expect(mocks.applyCertificateAuth).toHaveBeenCalledOnce(); + setSshAuthTypeOwnerSource(() => []); }); it("vault needs a profile, then a cached certificate", async () => { @@ -465,18 +454,20 @@ describe("connectHost", () => { }); it("throws the auth outcome instead of connecting", async () => { - mocks.getStepCaCert.mockReturnValue(null); - const attempt = connectHost(host({ authType: "stepca" }), { + mocks.getVaultCert.mockResolvedValue(null); + const vaultHost = () => + host({ authType: "vault", vaultProfile: { id: 3 } }); + const attempt = connectHost(vaultHost(), { userId: "user-1", purpose: "tmux", }); await expect(attempt).rejects.toBeInstanceOf(SshConnectError); await expect( - connectHost(host({ authType: "stepca" }), { + connectHost(vaultHost(), { userId: "user-1", purpose: "tmux", }).catch((error) => error.code), - ).resolves.toBe("stepca-required"); + ).resolves.toBe("vault-required"); }); it("goes through the jump chain and forwards to the target", async () => { @@ -628,18 +619,18 @@ describe("connectHost", () => { it("lets the provider react to an auth failure", async () => { FakeSshClient.nextBehaviour = ["auth-fail"]; - mocks.getStepCaCert.mockReturnValueOnce({ + mocks.getVaultCert.mockResolvedValueOnce({ privateKey: "k", sshCert: "c", }); await expect( - connectHost(host({ authType: "stepca" }), { + connectHost(host({ authType: "vault", vaultProfile: { id: 3 } }), { userId: "user-1", purpose: "fleet", }), ).rejects.toThrow(/All configured authentication methods failed/); await vi.waitFor(() => - expect(mocks.invalidateStepCaCert).toHaveBeenCalledWith("user-1", 7), + expect(mocks.deleteVaultCert).toHaveBeenCalledWith("user-1", 3), ); }); diff --git a/src/backend/tests/hosts/step-ca-auth.test.ts b/src/backend/tests/hosts/step-ca-auth.test.ts deleted file mode 100644 index 1e4530e70..000000000 --- a/src/backend/tests/hosts/step-ca-auth.test.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { WebSocket } from "ws"; - -const state = vi.hoisted(() => ({ - sent: [] as Array>, - parseInfo: { - publicKeyLine: "ssh-ed25519 TESTKEY", - principals: ["alice"], - validAfter: new Date(Date.now() - 60_000), - validBefore: new Date(Date.now() + 60 * 60_000), - }, - idToken: "", - command: null as Record | null, - remoteResult: null as { ok: boolean; message: string } | null, - runtimeComplete: vi.fn(async () => undefined), -})); - -vi.mock("../../utils/logger.js", () => ({ - sshLogger: { error: vi.fn(), warn: vi.fn(), info: vi.fn() }, -})); -vi.mock("../../database/repositories/factory.js", () => ({ - createCurrentSettingsRepository: () => ({ - get: async (key: string) => - ({ - step_ca_url: "https://ca.example", - step_ca_fingerprint: "a".repeat(64), - step_ca_provisioner: "oidc", - })[key], - }), -})); -vi.mock("../../utils/step-ca-egress.js", () => ({ - readStepCaPrivateAllowlist: async () => [], -})); -vi.mock("../../utils/step-ca-client.js", () => ({ - fetchRootCertificate: async () => "ROOT", - findOidcProvisioner: async () => ({ - clientID: "client", - configurationEndpoint: "https://idp.example/.well-known/openid", - }), - discoverOidcEndpoints: async () => ({ - authorizationEndpoint: "https://idp.example/auth", - tokenEndpoint: "https://idp.example/token", - }), - createPkce: () => ({ verifier: "verifier", challenge: "challenge" }), - generateSshKeyPair: () => ({ - publicKeyLine: "ssh-ed25519 TESTKEY", - privateKeyPem: "PRIVATE", - }), - buildAuthorizationUrl: (input: Record) => { - const url = new URL("https://idp.example/auth"); - url.searchParams.set("state", input.state); - url.searchParams.set("nonce", input.nonce); - return url.toString(); - }, - exchangeCodeForIdToken: async () => state.idToken, - decodeJwtClaims: (token: string) => { - const payload = token.split(".")[1]; - return payload - ? JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) - : {}; - }, - signSshCertificate: async () => "CERT", - parseSshCertificate: () => state.parseInfo, -})); -vi.mock("../../hosts/step-ca-runtime.js", () => ({ - stepCaRuntime: { - register: vi.fn(async () => undefined), - remove: vi.fn(async () => undefined), - submit: vi.fn(async () => true), - takeCommand: vi.fn(async () => { - const command = state.command; - state.command = null; - return command; - }), - complete: state.runtimeComplete, - takeResult: vi.fn(async () => { - const result = state.remoteResult; - state.remoteResult = null; - return result; - }), - }, -})); - -const { - cancelStepCaAuth, - completeStepCaAuth, - getStepCaCert, - invalidateStepCaCert, - startStepCaAuth, -} = await import("../../hosts/step-ca-auth.js"); - -function jwt(payload: object): string { - return `x.${Buffer.from(JSON.stringify(payload)).toString("base64url")}.y`; -} - -function fakeWs(): WebSocket { - return { - send: (raw: string) => state.sent.push(JSON.parse(raw)), - once: vi.fn(), - } as unknown as WebSocket; -} - -async function start(): Promise<{ requestId: string; nonce: string }> { - await startStepCaAuth( - "user-1", - 7, - "alice", - fakeWs(), - "https://termix.example", - ); - const chooser = state.sent.at(-1)!; - const url = new URL(String(chooser.url)); - return { - requestId: String(chooser.requestId), - nonce: url.searchParams.get("nonce")!, - }; -} - -beforeEach(() => { - state.sent.length = 0; - invalidateStepCaCert("user-1", 7); - state.runtimeComplete.mockClear(); - state.command = null; - state.remoteResult = null; - state.parseInfo = { - publicKeyLine: "ssh-ed25519 TESTKEY", - principals: ["alice"], - validAfter: new Date(Date.now() - 60_000), - validBefore: new Date(Date.now() + 60 * 60_000), - }; -}); - -describe("Step CA authentication", () => { - it("stores only a certificate bound to the nonce, key, principal and validity window", async () => { - const { requestId, nonce } = await start(); - state.idToken = jwt({ nonce, email: "alice@example.com" }); - - await expect( - completeStepCaAuth({ state: requestId, code: "code" }), - ).resolves.toEqual({ - ok: true, - message: "Signed in. You can close this window.", - }); - expect(getStepCaCert("user-1", 7)).toEqual({ - sshCert: "CERT", - privateKey: "PRIVATE", - }); - expect(state.sent.at(-1)).toMatchObject({ type: "stepca_completed" }); - }); - - it("rejects a token without the requested nonce", async () => { - const { requestId } = await start(); - state.idToken = jwt({ email: "alice@example.com" }); - - const result = await completeStepCaAuth({ state: requestId, code: "code" }); - expect(result.ok).toBe(false); - expect(result.message).toMatch(/does not match/); - expect(getStepCaCert("user-1", 7)).toBeNull(); - }); - - it("routes a callback through Redis to the instance holding the WebSocket", async () => { - const { requestId, nonce } = await start(); - state.idToken = jwt({ nonce }); - state.command = { state: requestId, code: "remote-code" }; - - await vi.waitFor(() => expect(state.runtimeComplete).toHaveBeenCalled(), { - timeout: 2000, - }); - expect(getStepCaCert("user-1", 7)).not.toBeNull(); - }); - - it("returns a result produced by another instance", async () => { - state.remoteResult = { ok: true, message: "remote success" }; - await expect( - completeStepCaAuth({ state: "remote-state", code: "code" }), - ).resolves.toEqual({ ok: true, message: "remote success" }); - }); - - it("cancels local sessions", async () => { - const { requestId } = await start(); - expect(cancelStepCaAuth(requestId)).toBe(true); - expect(cancelStepCaAuth(requestId)).toBe(false); - }); -}); diff --git a/src/backend/tests/plugins/ctx-notify.test.ts b/src/backend/tests/plugins/ctx-notify.test.ts index f5119f807..667390f16 100644 --- a/src/backend/tests/plugins/ctx-notify.test.ts +++ b/src/backend/tests/plugins/ctx-notify.test.ts @@ -23,6 +23,7 @@ const state = vi.hoisted(() => ({ delivered: [] as Array<{ channelId: number; title: string }>, failChannel: null as number | null, fetchCalls: [] as Array<{ url: string; allowlist: readonly string[] }>, + lastTls: null as Record | null, lastSignal: null as AbortSignal | null, })); @@ -61,7 +62,9 @@ vi.mock("../../utils/safe-outbound-fetch.js", () => ({ url: string, init: RequestInit, allowlist: readonly string[], + tls: Record = {}, ) => { + state.lastTls = tls; state.lastSignal = init.signal ?? null; state.fetchCalls.push({ url, allowlist }); return new Response("ok", { status: 200 }); @@ -184,6 +187,18 @@ describe("ctx.fetch", () => { ]); }); + it("passes a pinned CA and the fingerprint bootstrap flag to the guard", async () => { + const ctx = contextFor(["network:outbound"]); + await ctx.fetch("https://ca.lan/root/abc", { + tls: { rejectUnauthorized: false }, + }); + expect(state.lastTls).toEqual({ rejectUnauthorized: false }); + await ctx.fetch("https://ca.lan/provisioners", { tls: { ca: "PEM" } }); + expect(state.lastTls).toEqual({ ca: "PEM" }); + await ctx.fetch("https://example.com"); + expect(state.lastTls).toEqual({}); + }); + it("aborts the request, and a streamed body, when the caller's signal fires", async () => { const ctx = contextFor(["network:outbound"]); const controller = new AbortController(); diff --git a/src/backend/tests/utils/step-ca-settings-migration.test.ts b/src/backend/tests/utils/step-ca-settings-migration.test.ts new file mode 100644 index 000000000..3ea213e8a --- /dev/null +++ b/src/backend/tests/utils/step-ca-settings-migration.test.ts @@ -0,0 +1,116 @@ +/** + * The Step CA settings migration: a 2.8 CA configuration and its private + * endpoint allowlist come through into the step-ca plugin's admin settings, + * the old redirect URI stays in use, and a second run changes nothing. + */ + +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const state = vi.hoisted(() => ({ + legacy: {} as Record, + pluginRows: [] as Array<{ key: string; value: string | null }>, + pluginInstalled: true, +})); + +vi.mock("../../database/repositories/factory.js", () => ({ + createCurrentPluginRepository: () => ({ + findById: async (id: string) => + state.pluginInstalled && id === "step-ca" ? { id } : null, + }), + createCurrentSettingsRepository: () => ({ + get: async (key: string) => state.legacy[key] ?? null, + }), + createCurrentPluginSettingsRepository: () => ({ + get: async ( + _pluginId: string, + _scope: string, + _scopeId: string | null, + key: string, + ) => state.pluginRows.find((row) => row.key === key) ?? null, + set: async ( + _pluginId: string, + _scope: string, + _scopeId: string | null, + key: string, + value: string | null, + ) => { + state.pluginRows = state.pluginRows.filter((row) => row.key !== key); + state.pluginRows.push({ key, value }); + }, + }), +})); + +vi.mock("../../utils/logger.js", () => ({ + databaseLogger: { info: vi.fn(), warn: vi.fn() }, +})); + +import { runStepCaSettingsMigration } from "../../utils/crypto-migration/step-ca-settings-migration.js"; + +const value = (key: string) => { + const row = state.pluginRows.find((candidate) => candidate.key === key); + return row?.value == null ? undefined : JSON.parse(row.value); +}; + +beforeEach(() => { + state.legacy = { + step_ca_url: "https://ca.internal:9000", + step_ca_fingerprint: "a".repeat(64), + step_ca_provisioner: "oidc", + step_ca_private_endpoint_allowlist: JSON.stringify([ + "ca.internal", + "SSO.internal", + ]), + }; + state.pluginRows = []; + state.pluginInstalled = true; +}); + +describe("runStepCaSettingsMigration", () => { + it("moves the 2.8 settings and keeps the old redirect URI", async () => { + const result = await runStepCaSettingsMigration(); + expect(result.moved).toEqual([ + "step_ca_url", + "step_ca_fingerprint", + "step_ca_provisioner", + "step_ca_private_endpoint_allowlist", + ]); + expect(result.legacyCallback).toBe(true); + expect(value("caUrl")).toBe("https://ca.internal:9000"); + expect(value("fingerprint")).toBe("a".repeat(64)); + expect(value("provisioner")).toBe("oidc"); + expect(value("privateEndpoints")).toBe("ca.internal, sso.internal"); + expect(value("legacyCallback")).toBe(true); + }); + + it("changes nothing the second time", async () => { + await runStepCaSettingsMigration(); + const before = JSON.stringify(state.pluginRows); + const again = await runStepCaSettingsMigration(); + expect(again).toEqual({ moved: [], legacyCallback: false }); + expect(JSON.stringify(state.pluginRows)).toBe(before); + }); + + it("never overwrites a value set in the plugin", async () => { + state.pluginRows = [ + { key: "caUrl", value: JSON.stringify("https://new.ca") }, + { key: "legacyCallback", value: JSON.stringify(false) }, + ]; + await runStepCaSettingsMigration(); + expect(value("caUrl")).toBe("https://new.ca"); + expect(value("legacyCallback")).toBe(false); + expect(value("provisioner")).toBe("oidc"); + }); + + it("leaves an install that never used Step CA alone", async () => { + state.legacy = {}; + const result = await runStepCaSettingsMigration(); + expect(result).toEqual({ moved: [], legacyCallback: false }); + expect(state.pluginRows).toEqual([]); + }); + + it("does nothing until the plugin row exists", async () => { + state.pluginInstalled = false; + await runStepCaSettingsMigration(); + expect(state.pluginRows).toEqual([]); + }); +}); diff --git a/src/backend/utils/crypto-migration/step-ca-settings-migration.ts b/src/backend/utils/crypto-migration/step-ca-settings-migration.ts new file mode 100644 index 000000000..e29cfd864 --- /dev/null +++ b/src/backend/utils/crypto-migration/step-ca-settings-migration.ts @@ -0,0 +1,110 @@ +/** + * Moves the 2.8 Step CA configuration into the step-ca plugin. + * + * The CA URL, root fingerprint, OIDC provisioner and private endpoint + * allowlist lived in core `settings` rows. They become the plugin's admin + * settings. An install that had a CA configured keeps sending the pre-2.9 + * redirect URI (/host/step-ca-callback), because that is the one registered + * with its identity provider: the plugin's legacyCallback setting is turned + * on. + * + * Idempotent: a value already present in plugin_settings is never + * overwritten. The old rows stay for one release so a downgrade still works. + */ + +import { databaseLogger } from "../logger.js"; +import { + createCurrentPluginRepository, + createCurrentPluginSettingsRepository, + createCurrentSettingsRepository, +} from "../../database/repositories/factory.js"; +import { parseNotificationAllowlist } from "../notification-egress.js"; + +const PLUGIN_ID = "step-ca"; + +const MOVES: { legacyKey: string; field: string }[] = [ + { legacyKey: "step_ca_url", field: "caUrl" }, + { legacyKey: "step_ca_fingerprint", field: "fingerprint" }, + { legacyKey: "step_ca_provisioner", field: "provisioner" }, +]; + +const LEGACY_ALLOWLIST_KEY = "step_ca_private_endpoint_allowlist"; + +export interface StepCaSettingsMigrationResult { + moved: string[]; + legacyCallback: boolean; +} + +export async function runStepCaSettingsMigration(): Promise { + const result: StepCaSettingsMigrationResult = { + moved: [], + legacyCallback: false, + }; + + try { + // plugin_settings has a foreign key to plugins: nothing to move into yet. + const plugin = await createCurrentPluginRepository().findById(PLUGIN_ID); + if (!plugin) return result; + + const settings = createCurrentSettingsRepository(); + const pluginSettings = createCurrentPluginSettingsRepository(); + + const writeIfUnset = async (field: string, value: unknown) => { + const existing = await pluginSettings.get( + PLUGIN_ID, + "admin", + null, + field, + ); + if (existing && existing.value !== null) return false; + await pluginSettings.set( + PLUGIN_ID, + "admin", + null, + field, + JSON.stringify(value), + ); + return true; + }; + + for (const move of MOVES) { + const value = (await settings.get(move.legacyKey))?.trim(); + if (!value) continue; + if (await writeIfUnset(move.field, value)) { + result.moved.push(move.legacyKey); + } + } + + const hosts = parseNotificationAllowlist( + await settings.get(LEGACY_ALLOWLIST_KEY), + ); + if ( + hosts.length > 0 && + (await writeIfUnset("privateEndpoints", hosts.join(", "))) + ) { + result.moved.push(LEGACY_ALLOWLIST_KEY); + } + + if ((await settings.get("step_ca_url"))?.trim()) { + result.legacyCallback = await writeIfUnset("legacyCallback", true); + } + + if (result.moved.length > 0) { + databaseLogger.info( + `Moved ${result.moved.length} Step CA setting(s) into plugin settings`, + { + operation: "step_ca_settings_migration", + moved: result.moved.join(", "), + }, + ); + } + } catch (error) { + // Must not stop the backend: the plugin then reports "not configured". + databaseLogger.warn("Step CA settings migration failed", { + operation: "step_ca_settings_migration", + error: error instanceof Error ? error.message : String(error), + }); + } + + return result; +} diff --git a/src/backend/utils/step-ca-egress.ts b/src/backend/utils/step-ca-egress.ts deleted file mode 100644 index 69e0bad5f..000000000 --- a/src/backend/utils/step-ca-egress.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createCurrentSettingsRepository } from "../database/repositories/factory.js"; -import { parseNotificationAllowlist } from "./notification-egress.js"; - -/** Private hosts (a CA on the LAN, an internal IdP) the Step CA flow may reach. */ -export const STEP_CA_PRIVATE_ALLOWLIST_KEY = - "step_ca_private_endpoint_allowlist"; - -export async function readStepCaPrivateAllowlist(): Promise { - const raw = await createCurrentSettingsRepository().get( - STEP_CA_PRIVATE_ALLOWLIST_KEY, - ); - return parseNotificationAllowlist(raw); -} diff --git a/src/ui/api/private-endpoints-api.ts b/src/ui/api/private-endpoints-api.ts index 243abf156..bd155f0a1 100644 --- a/src/ui/api/private-endpoints-api.ts +++ b/src/ui/api/private-endpoints-api.ts @@ -14,25 +14,6 @@ export async function getNotificationPrivateEndpoints(): Promise { } } -export async function getStepCaPrivateEndpoints(): Promise { - try { - return (await authApi.get("/users/step-ca-private-endpoints")).data.hosts; - } catch (error) { - throw handleApiError(error, "get Step CA endpoint allowlist"); - } -} - -export async function setStepCaPrivateEndpoints( - hosts: string[], -): Promise { - try { - return (await authApi.patch("/users/step-ca-private-endpoints", { hosts })) - .data.hosts; - } catch (error) { - throw handleApiError(error, "update Step CA endpoint allowlist"); - } -} - export async function setNotificationPrivateEndpoints( hosts: string[], ): Promise { diff --git a/src/ui/api/settings-api.ts b/src/ui/api/settings-api.ts index bb428fc6e..cf95c7a6b 100644 --- a/src/ui/api/settings-api.ts +++ b/src/ui/api/settings-api.ts @@ -33,39 +33,6 @@ export async function getSessionTimeout(): Promise<{ timeoutHours: number }> { } } -export interface StepCaSettings { - configured: boolean; - caUrl: string; - fingerprint: string; - provisioner: string; -} - -export async function getStepCaSettings(): Promise { - try { - const response = await authApi.get("/users/step-ca-settings"); - return { - configured: !!response.data.configured, - caUrl: response.data.caUrl ?? "", - fingerprint: response.data.fingerprint ?? "", - provisioner: response.data.provisioner ?? "", - }; - } catch (error) { - handleApiError(error, "fetch Step CA settings"); - } -} - -export async function updateStepCaSettings(input: { - caUrl: string; - fingerprint: string; - provisioner: string; -}): Promise { - try { - await authApi.patch("/users/step-ca-settings", input); - } catch (error) { - handleApiError(error, "update Step CA settings"); - } -} - export async function updateSessionTimeout( timeoutHours: number, ): Promise { diff --git a/src/ui/auth/Auth.tsx b/src/ui/auth/Auth.tsx index 1b1673804..764059f32 100644 --- a/src/ui/auth/Auth.tsx +++ b/src/ui/auth/Auth.tsx @@ -46,7 +46,6 @@ import { type SecondFactorRef, } from "@/api/auth-methods-api"; import { useLoginMethods, useSecondFactors } from "@/plugin-host/auth-registry"; -import { ensureLegacyAuthUI } from "./legacy-auth-ui"; import { startPreLoginPlugins } from "@/plugin-host/loader"; import { Checkbox } from "@/components/checkbox"; import { useBranding } from "@/contexts/BrandingContext"; @@ -99,8 +98,6 @@ const LANGUAGES = [ { code: "vi", label: "Tiếng Việt" }, ]; -ensureLegacyAuthUI(); - const STORAGE_KEY = "termix_auth"; /** An empty list means "whichever factors this screen has UI for". */ diff --git a/src/ui/auth/legacy-auth-ui.tsx b/src/ui/auth/legacy-auth-ui.tsx deleted file mode 100644 index 37d5e35b9..000000000 --- a/src/ui/auth/legacy-auth-ui.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/** - * The certificate SSH auth editors that still live in core for 2.9.0, - * registered through the same registry a plugin uses. - * Phase C moves each into its plugin by moving its block out of this file; - * D1 deletes the file once it is empty. - */ -import { useTranslation } from "react-i18next"; -import { registerSshAuthEditor } from "@/plugin-host/auth-registry"; - -function CertificateAuthInfo({ - labelKey, - descriptionKey, - docsUrl, -}: { - labelKey: string; - descriptionKey: string; - docsUrl: string; -}) { - const { t } = useTranslation(); - return ( -
-
- - {t(labelKey)} - - - {t("hosts.docsLink")} - -
-

{t(descriptionKey)}

-
- ); -} - -function StepCaAuthEditor() { - return ( - - ); -} - -let registered = false; - -export function ensureLegacyAuthUI(): void { - if (registered) return; - registered = true; - registerSshAuthEditor({ - id: "stepca", - pluginId: "core", - titleKey: "hosts.filterAuthStepca", - component: StepCaAuthEditor, - }); -} - -/** Test helper. */ -export function resetLegacyAuthUIForTests(): void { - registered = false; -} diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json index 49aeb07fd..a3412f169 100644 --- a/src/ui/locales/en.json +++ b/src/ui/locales/en.json @@ -643,8 +643,6 @@ "keyPassphraseSaved": "Passphrase saved, type to change", "replaceKey": "Replace key", "docsLink": "View docs", - "stepcaLabel": "Step CA", - "stepcaDesc": "Sign in through your identity provider and let your Step CA issue a short-lived SSH certificate for this host. The CA URL, root fingerprint and OIDC provisioner are set by an administrator under Admin Settings.", "agentLabel": "SSH Agent", "agentDesc": "Authenticate using an SSH agent running on the Termix server (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). The agent must be running on the machine where Termix is hosted.", "agentAuthenticationDesc": "Use keys from an SSH agent to authenticate this connection. Leave the socket path empty to use SSH_AUTH_SOCK.", @@ -1995,15 +1993,6 @@ "admin": "Administration" } }, - "stepCa": "Step CA", - "stepCaDesc": "Issue short-lived SSH certificates from a smallstep CA. Enter the CA URL, its root fingerprint (as shown by step ca bootstrap) and the name of the OIDC provisioner; then choose \"Step CA\" as a host's authentication type. Leave all three empty to disable.", - "stepCaFingerprint": "Root fingerprint (SHA-256)", - "stepCaProvisioner": "OIDC provisioner name", - "stepCaSaved": "Step CA settings saved", - "stepCaSaveFailed": "Failed to save Step CA settings", - "stepCaPrivateEndpoints": "Allowed private Step CA hosts", - "stepCaPrivateEndpointsDesc": "Private hosts the Step CA certificate flow may contact: the CA itself and, if internal, your identity provider. Separate them with commas.", - "updateStepCaEndpointsFailed": "Failed to update the Step CA endpoint allowlist", "notificationPrivateEndpoints": "Allowed private notification hosts", "notificationPrivateEndpointsDesc": "Exact private hosts that notification channels may contact. Separate them with commas.", "updateNotificationEndpointsFailed": "Failed to update notification endpoint allowlist", diff --git a/src/ui/locales/translated/af_ZA.json b/src/ui/locales/translated/af_ZA.json index f905601ba..0819a7c9f 100644 --- a/src/ui/locales/translated/af_ZA.json +++ b/src/ui/locales/translated/af_ZA.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Wagwoordfrase gestoor, tik om te verander", "replaceKey": "Vervang sleutel", "docsLink": "Bekyk dokumente", - "stepcaLabel": "Stap CA", - "stepcaDesc": "Meld aan deur jou identiteitsverskaffer en laat jou Step CA 'n kortstondige SSH-sertifikaat vir hierdie gasheer uitreik. Die CA-URL, wortelvingerafdruk en OIDC-voorsiening word deur 'n administrateur onder Admin-instellings ingestel.", "agentLabel": "SSH-agent", "agentDesc": "Verifieer deur 'n SSH-agent te gebruik wat op die Termix-bediener loop (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). Die agent moet loop op die masjien waar Termix gehuisves word.", "agentAuthenticationDesc": "Gebruik sleutels van 'n SSH-agent om hierdie verbinding te verifieer. Los die sokpad leeg om SSH_AUTH_SOCK te gebruik.", @@ -2121,18 +2119,9 @@ "save": "Stoor", "saving": "Stoor..." }, - "stepCa": "Stap CA", - "stepCaDesc": "Reik kortstondige SSH-sertifikate uit vanaf 'n smallstep CA. Voer die CA-URL, die wortelvingerafdruk daarvan (soos getoon deur stap ca se selflaaistelsel) en die naam van die OIDC-voorsiener in; kies dan \"Stap CA\" as 'n gasheer se verifikasietipe. Los al drie leeg om te deaktiveer.", - "stepCaFingerprint": "Wortelvingerafdruk (SHA-256)", - "stepCaProvisioner": "OIDC-voorsieningsnaam", - "stepCaSaved": "Stap CA-instellings gestoor", - "stepCaSaveFailed": "Kon nie Stap CA-instellings stoor nie", "secretSourcePrivateEndpoints": "Toegelate privaat geheime brongashere", "secretSourcePrivateEndpointsDesc": "Privaat gashere wat geheime bronne (1Password Connect) mag kontak. Skei hulle met kommas.", "updateSecretSourceEndpointsFailed": "Kon nie die geheime bron-eindpunt-toelaatlys opdateer nie", - "stepCaPrivateEndpoints": "Toegelate private Step CA-gashere", - "stepCaPrivateEndpointsDesc": "Privaat gashere, die Step CA-sertifikaatvloei, mag kontak maak met: die CA self en, indien intern, jou identiteitsverskaffer. Skei hulle met kommas.", - "updateStepCaEndpointsFailed": "Kon nie die Step CA-eindpunt-toelaatlys opdateer nie", "notificationPrivateEndpoints": "Toegelate privaat kennisgewinggashere", "notificationPrivateEndpointsDesc": "Presiese private gashere wat kennisgewingkanale mag kontak. Skei hulle met kommas.", "updateNotificationEndpointsFailed": "Kon nie kennisgewing-eindpunt-toelaatlys opdateer nie" diff --git a/src/ui/locales/translated/ar_SA.json b/src/ui/locales/translated/ar_SA.json index 24a51413f..5a0c0ce59 100644 --- a/src/ui/locales/translated/ar_SA.json +++ b/src/ui/locales/translated/ar_SA.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "تم حفظ عبارة المرور، اكتب للتغيير", "replaceKey": "استبدال المفتاح", "docsLink": "عرض المستندات", - "stepcaLabel": "الخطوة ج", - "stepcaDesc": "سجّل الدخول عبر موفر الهوية الخاص بك، ودع جهة إصدار الشهادات المعتمدة (SA) التابعة لـ Step CA تُصدر شهادة SSH مؤقتة لهذا المضيف. يتم تحديد عنوان URL الخاص بجهة إصدار الشهادات المعتمدة، وبصمة الجذر، وموفر OIDC بواسطة مسؤول النظام ضمن إعدادات المسؤول.", "agentLabel": "وكيل SSH", "agentDesc": "المصادقة باستخدام وكيل SSH يعمل على خادم Termix (مثل Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). يجب أن يكون الوكيل قيد التشغيل على الجهاز الذي يستضيف Termix.", "agentAuthenticationDesc": "استخدم مفاتيح من وكيل SSH لمصادقة هذا الاتصال. اترك مسار المقبس فارغًا لاستخدام SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "حفظ", "saving": "جارٍ الحفظ..." }, - "stepCa": "الخطوة ج", - "stepCaDesc": "إصدار شهادات SSH قصيرة الأجل من جهة مصدقة صغيرة (Smallstep CA). أدخل عنوان URL الخاص بالجهة المصدقة، وبصمة الجذر (كما هو موضح في أمر step ca bootstrap)، واسم موفر OIDC؛ ثم اختر \"Step CA\" كنوع مصادقة المضيف. اترك الحقول الثلاثة فارغة لتعطيل هذه الميزة.", - "stepCaFingerprint": "بصمة الجذر (SHA-256)", - "stepCaProvisioner": "اسم موفر OIDC", - "stepCaSaved": "تم حفظ إعدادات CA للخطوة", - "stepCaSaveFailed": "فشل حفظ إعدادات الخطوة CA", "secretSourcePrivateEndpoints": "مضيفو مصادر الأسرار الخاصة المسموح بهم", "secretSourcePrivateEndpointsDesc": "المضيفون الخاصون الذين قد تتصل بهم مصادر سرية (مثل 1Password Connect). افصل بينهم بفواصل.", "updateSecretSourceEndpointsFailed": "فشل تحديث قائمة السماح لنقاط نهاية المصدر السرية", - "stepCaPrivateEndpoints": "مضيفو شهادات الخطوة الخاصة المسموح بها", - "stepCaPrivateEndpointsDesc": "قد تتصل جهات الاستضافة الخاصة بعملية شهادة Step CA بما يلي: جهة إصدار الشهادات نفسها، وإذا كانت داخلية، مزود الهوية الخاص بك. افصل بينهما بفواصل.", - "updateStepCaEndpointsFailed": "فشل تحديث قائمة السماح لنقاط نهاية شهادة التحقق من صحة الخطوة", "notificationPrivateEndpoints": "المضيفون المسموح لهم بالإشعارات الخاصة", "notificationPrivateEndpointsDesc": "حدد المضيفين الخاصين الذين يمكن لقنوات الإشعارات الاتصال بهم. افصل بينهم بفواصل.", "updateNotificationEndpointsFailed": "فشل تحديث قائمة السماح لنقاط نهاية الإشعارات" diff --git a/src/ui/locales/translated/bg_BG.json b/src/ui/locales/translated/bg_BG.json index 298084c92..a0e64506c 100644 --- a/src/ui/locales/translated/bg_BG.json +++ b/src/ui/locales/translated/bg_BG.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Паролата е запазена, въведете, за да я промените", "replaceKey": "Сменете ключа", "docsLink": "Преглед на документи", - "stepcaLabel": "Стъпка CA", - "stepcaDesc": "Влезте чрез вашия доставчик на самоличност и позволете на вашия Step CA да издаде краткотраен SSH сертификат за този хост. URL адресът на CA, root пръстовият отпечатък и OIDC provisioner се задават от администратор в „Настройки на администратора“.", "agentLabel": "SSH агент", "agentDesc": "Удостоверете се, използвайки SSH агент, работещ на Termix сървъра (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). Агентът трябва да работи на машината, където се хоства Termix.", "agentAuthenticationDesc": "Използвайте ключове от SSH агент за удостоверяване на тази връзка. Оставете пътя на сокета празен, за да използвате SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "Запазване", "saving": "Запазване..." }, - "stepCa": "Стъпка CA", - "stepCaDesc": "Издавайте краткотрайни SSH сертификати от smallstep CA. Въведете URL адреса на CA, неговия root fingerprint (както е показано от step ca bootstrap) и името на OIDC provisioner; след това изберете „Step CA“ като тип удостоверяване на хоста. Оставете и трите празни, за да деактивирате.", - "stepCaFingerprint": "Коренен пръстов отпечатък (SHA-256)", - "stepCaProvisioner": "Име на доставчика на OIDC", - "stepCaSaved": "Настройките на стъпка CA са запазени", - "stepCaSaveFailed": "Запазването на настройките на стъпка CA не бе успешно", "secretSourcePrivateEndpoints": "Разрешени частни секретни хостове от източници", "secretSourcePrivateEndpointsDesc": "Частни хостове, с които тайни източници (1Password Connect) могат да се свържат. Разделете ги със запетаи.", "updateSecretSourceEndpointsFailed": "Актуализирането на списъка с разрешени крайни точки на секретния източник не бе успешно", - "stepCaPrivateEndpoints": "Разрешени частни хостове на Step CA", - "stepCaPrivateEndpointsDesc": "Частни хостове, с които потокът от сертификати на Step CA може да се свърже: самия CA и, ако е вътрешен, вашият доставчик на идентичност. Разделете ги със запетаи.", - "updateStepCaEndpointsFailed": "Актуализирането на списъка с разрешени крайни точки на Step CA не бе успешно", "notificationPrivateEndpoints": "Разрешени хостове за частни известия", "notificationPrivateEndpointsDesc": "Точните частни хостове, с които каналите за известия могат да се свържат. Разделете ги със запетаи.", "updateNotificationEndpointsFailed": "Актуализирането на списъка с разрешени крайни точки за известия не бе успешно" diff --git a/src/ui/locales/translated/bn_BD.json b/src/ui/locales/translated/bn_BD.json index b2bdbd2ad..f8ef2b06f 100644 --- a/src/ui/locales/translated/bn_BD.json +++ b/src/ui/locales/translated/bn_BD.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "পাসফ্রেজ সংরক্ষিত হয়েছে, পরিবর্তন করতে টাইপ করুন", "replaceKey": "চাবি প্রতিস্থাপন করুন", "docsLink": "নথি দেখুন", - "stepcaLabel": "স্টেপ সিএ", - "stepcaDesc": "আপনার আইডেন্টিটি প্রোভাইডারের মাধ্যমে সাইন ইন করুন এবং আপনার স্টেপ সিএ-কে এই হোস্টের জন্য একটি স্বল্পস্থায়ী এসএসএইচ সার্টিফিকেট ইস্যু করতে দিন। সিএ ইউআরএল, রুট ফিঙ্গারপ্রিন্ট এবং ওআইডিসি প্রোভিশনার একজন অ্যাডমিনিস্ট্রেটর দ্বারা অ্যাডমিন সেটিংস-এর অধীনে সেট করা হয়।", "agentLabel": "এসএসএইচ এজেন্ট", "agentDesc": "Termix সার্ভারে চলমান একটি SSH এজেন্ট (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent) ব্যবহার করে প্রমাণীকরণ করুন। এজেন্টটি অবশ্যই সেই মেশিনে চালু থাকতে হবে যেখানে Termix হোস্ট করা আছে।", "agentAuthenticationDesc": "এই সংযোগটি প্রমাণীকরণের জন্য একটি SSH এজেন্ট থেকে কী ব্যবহার করুন। SSH_AUTH_SOCK ব্যবহার করতে সকেট পাথটি খালি রাখুন।", @@ -2121,18 +2119,9 @@ "save": "সংরক্ষণ করুন", "saving": "সঞ্চয়..." }, - "stepCa": "স্টেপ সিএ", - "stepCaDesc": "একটি smallstep CA থেকে স্বল্পস্থায়ী SSH সার্টিফিকেট ইস্যু করুন। CA URL, এর রুট ফিঙ্গারপ্রিন্ট (যেমনটি step ca bootstrap দ্বারা দেখানো হয়েছে) এবং OIDC প্রোভিশনারের নাম লিখুন; তারপর হোস্টের অথেনটিকেশন টাইপ হিসেবে \"Step CA\" বেছে নিন। নিষ্ক্রিয় করতে তিনটিই খালি রাখুন।", - "stepCaFingerprint": "রুট ফিঙ্গারপ্রিন্ট (SHA-256)", - "stepCaProvisioner": "OIDC সরবরাহকারীর নাম", - "stepCaSaved": "ধাপ CA সেটিংস সংরক্ষিত হয়েছে", - "stepCaSaveFailed": "স্টেপ সিএ সেটিংস সংরক্ষণ করতে ব্যর্থ হয়েছে", "secretSourcePrivateEndpoints": "অনুমোদিত ব্যক্তিগত গোপন উৎস হোস্ট", "secretSourcePrivateEndpointsDesc": "ব্যক্তিগত হোস্ট, যাদের সাথে গোপন সূত্র (1Password Connect) যোগাযোগ করতে পারে। এগুলিকে কমা দিয়ে আলাদা করুন।", "updateSecretSourceEndpointsFailed": "গোপন উৎস এন্ডপয়েন্ট অনুমতি তালিকা আপডেট করতে ব্যর্থ হয়েছে", - "stepCaPrivateEndpoints": "ব্যক্তিগত স্টেপ সিএ হোস্টদের অনুমতি দেওয়া হয়েছে", - "stepCaPrivateEndpointsDesc": "স্টেপ সিএ সার্টিফিকেট ফ্লো যেসব প্রাইভেট হোস্টের সাথে যোগাযোগ করতে পারে, সেগুলো হলো: স্বয়ং সিএ এবং, যদি অভ্যন্তরীণ হয়, আপনার আইডেন্টিটি প্রোভাইডার। এদেরকে কমা দিয়ে আলাদা করুন।", - "updateStepCaEndpointsFailed": "Step CA এন্ডপয়েন্ট অ্যালাওলিস্ট আপডেট করতে ব্যর্থ হয়েছে", "notificationPrivateEndpoints": "ব্যক্তিগত বিজ্ঞপ্তি হোস্ট অনুমোদিত", "notificationPrivateEndpointsDesc": "সুনির্দিষ্ট ব্যক্তিগত হোস্টগুলো, যাদের সাথে নোটিফিকেশন চ্যানেলগুলো যোগাযোগ করতে পারে। এগুলোকে কমা দিয়ে আলাদা করুন।", "updateNotificationEndpointsFailed": "নোটিফিকেশন এন্ডপয়েন্ট অ্যালাওলিস্ট আপডেট করতে ব্যর্থ হয়েছে" diff --git a/src/ui/locales/translated/ca_ES.json b/src/ui/locales/translated/ca_ES.json index 888a4c3f2..a7a8dfa10 100644 --- a/src/ui/locales/translated/ca_ES.json +++ b/src/ui/locales/translated/ca_ES.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Contrasenya desada, escriu per canviar-la", "replaceKey": "Substitueix la clau", "docsLink": "Veure documents", - "stepcaLabel": "Pas CA", - "stepcaDesc": "Inicieu la sessió a través del vostre proveïdor d'identitat i deixeu que la vostra Step CA emeti un certificat SSH de curta durada per a aquest amfitrió. L'URL de la CA, l'empremta digital arrel i el proveïdor OIDC els estableix un administrador a Configuració de l'administrador.", "agentLabel": "Agent SSH", "agentDesc": "Autentiqueu-vos mitjançant un agent SSH que s'executi al servidor Termix (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). L'agent ha d'estar executant-se a la màquina on s'allotja Termix.", "agentAuthenticationDesc": "Feu servir claus d'un agent SSH per autenticar aquesta connexió. Deixeu la ruta del sòcol buida per utilitzar SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "Desa", "saving": "Desant..." }, - "stepCa": "Pas CA", - "stepCaDesc": "Emet certificats SSH de curta durada des d'una CA Smallstep. Introdueix l'URL de la CA, la seva empremta digital arrel (tal com es mostra a l'arrencada de la CA Step) i el nom del proveïdor OIDC; a continuació, tria \"Step CA\" com a tipus d'autenticació de l'amfitrió. Deixa els tres buits per desactivar-los.", - "stepCaFingerprint": "Empremta digital arrel (SHA-256)", - "stepCaProvisioner": "Nom del proveïdor OIDC", - "stepCaSaved": "S'ha desat la configuració de l'Administració de Control del Pas", - "stepCaSaveFailed": "No s'ha pogut desar la configuració de Step CA.", "secretSourcePrivateEndpoints": "Amfitrions d'origen secret privat permesos", "secretSourcePrivateEndpointsDesc": "Amfitrions privats amb els quals es poden posar en contacte fonts secretes (1Password Connect). Separeu-los amb comes.", "updateSecretSourceEndpointsFailed": "No s'ha pogut actualitzar la llista de permesos del punt final de la font secreta", - "stepCaPrivateEndpoints": "Amfitrions privats permesos de Step CA", - "stepCaPrivateEndpointsDesc": "Amfitrions privats amb els quals el flux de certificats de la CA de Step pot contactar: la mateixa CA i, si és intern, el vostre proveïdor d'identitat. Separeu-los amb comes.", - "updateStepCaEndpointsFailed": "No s'ha pogut actualitzar la llista permesa del punt final de Step CA.", "notificationPrivateEndpoints": "Amfitrions de notificacions privades permesos", "notificationPrivateEndpointsDesc": "Amfitrions privats exactes amb els quals els canals de notificació poden contactar. Separeu-los amb comes.", "updateNotificationEndpointsFailed": "No s'ha pogut actualitzar la llista de permesos del punt final de notificació" diff --git a/src/ui/locales/translated/cs_CZ.json b/src/ui/locales/translated/cs_CZ.json index 4e9813966..d281c86f9 100644 --- a/src/ui/locales/translated/cs_CZ.json +++ b/src/ui/locales/translated/cs_CZ.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Heslo uloženo, zadejte heslo pro změnu.", "replaceKey": "Vyměňte klíč", "docsLink": "Zobrazit dokumenty", - "stepcaLabel": "Krok CA", - "stepcaDesc": "Přihlaste se prostřednictvím svého poskytovatele identity a nechte svou certifikační autoritu Step CA vydat krátkodobý SSH certifikát pro tohoto hostitele. URL certifikační autority, otisk kořenového serveru a zřizovatel OIDC nastavuje administrátor v Nastavení správce.", "agentLabel": "SSH agent", "agentDesc": "Ověřte se pomocí SSH agenta spuštěného na serveru Termix (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). Agent musí běžet na počítači, kde je hostován Termix.", "agentAuthenticationDesc": "K ověření tohoto připojení použijte klíče od SSH agenta. Pro použití SSH_AUTH_SOCK nechte cestu k socketu prázdnou.", @@ -2121,18 +2119,9 @@ "save": "Uložit", "saving": "Ukládání..." }, - "stepCa": "Krok CA", - "stepCaDesc": "Vydávejte krátkodobé SSH certifikáty z certifikační autority smallstep. Zadejte URL adresu certifikační autority, její kořenový otisk prstu (jak je znázorněno v kroku ca bootstrap) a název poskytovatele OIDC; poté jako typ ověřování hostitele vyberte „Step CA“. Pro deaktivaci ponechte všechny tři pole prázdné.", - "stepCaFingerprint": "Kořenový otisk (SHA-256)", - "stepCaProvisioner": "Název zřizovatele OIDC", - "stepCaSaved": "Nastavení kroku CA uloženo", - "stepCaSaveFailed": "Nepodařilo se uložit nastavení kroku CA", "secretSourcePrivateEndpoints": "Povolené hostitele soukromých tajných zdrojů", "secretSourcePrivateEndpointsDesc": "Soukromé hostitele, které mohou kontaktovat tajné zdroje (1Password Connect). Oddělte je čárkami.", "updateSecretSourceEndpointsFailed": "Nepodařilo se aktualizovat seznam povolených koncových bodů tajného zdroje.", - "stepCaPrivateEndpoints": "Povolení soukromí hostitelé Step CA", - "stepCaPrivateEndpointsDesc": "Soukromí hostitelé, které může tok certifikátů Step CA kontaktovat: samotná CA a v případě interní certifikace také váš poskytovatel identity. Oddělte je čárkami.", - "updateStepCaEndpointsFailed": "Nepodařilo se aktualizovat seznam povolených koncových bodů Step CA.", "notificationPrivateEndpoints": "Povolení hostitelé soukromých oznámení", "notificationPrivateEndpointsDesc": "Přesné soukromé hostitele, které mohou notifikační kanály kontaktovat. Oddělte je čárkami.", "updateNotificationEndpointsFailed": "Nepodařilo se aktualizovat seznam povolených koncových bodů oznámení" diff --git a/src/ui/locales/translated/da_DK.json b/src/ui/locales/translated/da_DK.json index af0e5d74e..3864d9f41 100644 --- a/src/ui/locales/translated/da_DK.json +++ b/src/ui/locales/translated/da_DK.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Adgangskode gemt, skriv for at ændre", "replaceKey": "Udskift nøgle", "docsLink": "Se dokumenter", - "stepcaLabel": "Trin CA", - "stepcaDesc": "Log ind via din identitetsudbyder, og lad din Step CA udstede et kortvarigt SSH-certifikat til denne vært. CA-URL'en, root-fingeraftrykket og OIDC-provisioneren indstilles af en administrator under administratorindstillinger.", "agentLabel": "SSH-agent", "agentDesc": "Godkend ved hjælp af en SSH-agent, der kører på Termix-serveren (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). Agenten skal køre på den maskine, hvor Termix hostes.", "agentAuthenticationDesc": "Brug nøgler fra en SSH-agent til at godkende denne forbindelse. Lad socket-stien være tom for at bruge SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "Spare", "saving": "Gemmer..." }, - "stepCa": "Trin CA", - "stepCaDesc": "Udsted kortlivede SSH-certifikater fra en smallstep CA. Indtast CA-URL'en, dens rodfingeraftryk (som vist i step ca bootstrap) og navnet på OIDC-provisioneren; vælg derefter \"Step CA\" som en værts godkendelsestype. Lad alle tre være tomme for at deaktivere.", - "stepCaFingerprint": "Rodfingeraftryk (SHA-256)", - "stepCaProvisioner": "OIDC-provisioneringsnavn", - "stepCaSaved": "Trin CA-indstillinger gemt", - "stepCaSaveFailed": "Kunne ikke gemme trin CA-indstillinger", "secretSourcePrivateEndpoints": "Tilladte private hemmelige kildeværter", "secretSourcePrivateEndpointsDesc": "Private værter som hemmelige kilder (1Password Connect) kan kontakte. Adskil dem med kommaer.", "updateSecretSourceEndpointsFailed": "Kunne ikke opdatere tilladelseslisten for det hemmelige kildeslutpunkt", - "stepCaPrivateEndpoints": "Tilladte private Step CA-værter", - "stepCaPrivateEndpointsDesc": "Private værter, som Step CA-certifikatflowet kan kontakte: selve CA'en og, hvis den er intern, din identitetsudbyder. Adskil dem med kommaer.", - "updateStepCaEndpointsFailed": "Kunne ikke opdatere Step CA-slutpunktstilladelseslisten", "notificationPrivateEndpoints": "Tilladte private notifikationsværter", "notificationPrivateEndpointsDesc": "Præcise private værter, som notifikationskanaler kan kontakte. Adskil dem med kommaer.", "updateNotificationEndpointsFailed": "Kunne ikke opdatere tilladelseslisten for notifikationsslutpunktet" diff --git a/src/ui/locales/translated/de_DE.json b/src/ui/locales/translated/de_DE.json index 9d83f760f..c32988adf 100644 --- a/src/ui/locales/translated/de_DE.json +++ b/src/ui/locales/translated/de_DE.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Passphrase gespeichert, zum Ändern eingeben", "replaceKey": "Schlüssel ersetzen", "docsLink": "Dokumentation anzeigen", - "stepcaLabel": "Schritt CA", - "stepcaDesc": "Melden Sie sich über Ihren Identitätsanbieter an und lassen Sie Ihre Step-Zertifizierungsstelle ein kurzlebiges SSH-Zertifikat für diesen Host ausstellen. Die URL der Zertifizierungsstelle, der Root-Fingerabdruck und der OIDC-Provisioner werden von einem Administrator unter „Admin-Einstellungen“ festgelegt.", "agentLabel": "SSH-Agent", "agentDesc": "Authentifizieren Sie sich mit einem SSH-Agenten, der auf dem Termix-Server läuft (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). Der Agent muss auf dem Rechner laufen, auf dem Termix gehostet wird.", "agentAuthenticationDesc": "Verwenden Sie Schlüssel eines SSH-Agenten, um diese Verbindung zu authentifizieren. Lassen Sie den Socket-Pfad leer, um SSH_AUTH_SOCK zu verwenden.", @@ -2121,18 +2119,9 @@ "save": "Speichern", "saving": "Wird gespeichert …" }, - "stepCa": "Schritt CA", - "stepCaDesc": "Stellen Sie kurzlebige SSH-Zertifikate von einer Smallstep-CA aus. Geben Sie die CA-URL, ihren Root-Fingerabdruck (wie von step ca bootstrap angezeigt) und den Namen des OIDC-Provisioners ein. Wählen Sie anschließend „Step CA“ als Authentifizierungstyp für den Host. Lassen Sie alle drei Felder leer, um die Authentifizierung zu deaktivieren.", - "stepCaFingerprint": "Root-Fingerabdruck (SHA-256)", - "stepCaProvisioner": "OIDC-Provisionername", - "stepCaSaved": "Schritt CA-Einstellungen gespeichert", - "stepCaSaveFailed": "Speichern der Step CA-Einstellungen fehlgeschlagen", "secretSourcePrivateEndpoints": "Zulässige private geheime Quellhosts", "secretSourcePrivateEndpointsDesc": "Private Hosts, die von geheimen Quellen (1Password Connect) kontaktiert werden können. Trennen Sie diese durch Kommas.", "updateSecretSourceEndpointsFailed": "Die Aktualisierung der Zulassungsliste für geheime Quellendpunkte ist fehlgeschlagen.", - "stepCaPrivateEndpoints": "Zulässige private Step CA-Hosts", - "stepCaPrivateEndpointsDesc": "Private Hosts, mit denen der Step CA-Zertifikatsfluss in Kontakt treten kann, kontaktieren möglicherweise die Zertifizierungsstelle selbst und, falls intern, Ihren Identitätsanbieter. Trennen Sie diese durch Kommas.", - "updateStepCaEndpointsFailed": "Die Aktualisierung der Step CA-Endpunkt-Zulassungsliste ist fehlgeschlagen.", "notificationPrivateEndpoints": "Zulässige Hosts für private Benachrichtigungen", "notificationPrivateEndpointsDesc": "Genaue private Hosts, die Benachrichtigungskanäle kontaktieren dürfen. Durch Kommas trennen.", "updateNotificationEndpointsFailed": "Aktualisierung der Zulassungsliste für Benachrichtigungsendpunkte fehlgeschlagen" diff --git a/src/ui/locales/translated/el_GR.json b/src/ui/locales/translated/el_GR.json index 281d7ceb4..7e5672961 100644 --- a/src/ui/locales/translated/el_GR.json +++ b/src/ui/locales/translated/el_GR.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Η φράση πρόσβασης αποθηκεύτηκε, πληκτρολογήστε για αλλαγή", "replaceKey": "Αντικατάσταση κλειδιού", "docsLink": "Προβολή εγγράφων", - "stepcaLabel": "Βήμα CA", - "stepcaDesc": "Συνδεθείτε μέσω του παρόχου ταυτότητας και αφήστε την Step CA σας να εκδώσει ένα βραχύβιο πιστοποιητικό SSH για αυτόν τον κεντρικό υπολογιστή. Η διεύθυνση URL CA, το δακτυλικό αποτύπωμα root και ο πάροχος OIDC ορίζονται από έναν διαχειριστή στις Ρυθμίσεις διαχειριστή.", "agentLabel": "Πράκτορας SSH", "agentDesc": "Πραγματοποιήστε έλεγχο ταυτότητας χρησιμοποιώντας έναν παράγοντα SSH που εκτελείται στον διακομιστή Termix (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). Ο παράγοντας πρέπει να εκτελείται στον υπολογιστή όπου φιλοξενείται το Termix.", "agentAuthenticationDesc": "Χρησιμοποιήστε κλειδιά από έναν παράγοντα SSH για να επαληθεύσετε αυτήν τη σύνδεση. Αφήστε τη διαδρομή υποδοχής κενή για να χρησιμοποιήσετε το SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "Εκτός", "saving": "Οικονομία..." }, - "stepCa": "Βήμα CA", - "stepCaDesc": "Έκδοση βραχύβιων πιστοποιητικών SSH από μια αρχή έκδοσης πιστοποιητικών smallstep. Εισαγάγετε τη διεύθυνση URL της αρχής έκδοσης πιστοποιητικών, το ριζικό της αποτύπωμα (όπως φαίνεται από το bootstrap του step ca) και το όνομα του παροχέα OIDC. Στη συνέχεια, επιλέξτε \"Step CA\" ως τύπο ελέγχου ταυτότητας κεντρικού υπολογιστή. Αφήστε και τα τρία κενά για απενεργοποίηση.", - "stepCaFingerprint": "Δακτυλικό αποτύπωμα ρίζας (SHA-256)", - "stepCaProvisioner": "Όνομα παρόχου OIDC", - "stepCaSaved": "Οι ρυθμίσεις CA του βήματος αποθηκεύτηκαν", - "stepCaSaveFailed": "Αποτυχία αποθήκευσης ρυθμίσεων Step CA", "secretSourcePrivateEndpoints": "Επιτρεπόμενοι ιδιωτικοί μυστικοί κεντρικοί υπολογιστές πηγών", "secretSourcePrivateEndpointsDesc": "Ιδιωτικοί κεντρικοί υπολογιστές με τους οποίους ενδέχεται να επικοινωνήσουν μυστικές πηγές (1Password Connect). Διαχωρίστε τους με κόμματα.", "updateSecretSourceEndpointsFailed": "Αποτυχία ενημέρωσης της λίστας επιτρεπόμενων τελικών σημείων μυστικής πηγής", - "stepCaPrivateEndpoints": "Επιτρεπόμενοι ιδιωτικοί κεντρικοί υπολογιστές Step CA", - "stepCaPrivateEndpointsDesc": "Οι ιδιωτικοί κεντρικοί υπολογιστές της ροής πιστοποιητικών Step CA ενδέχεται να επικοινωνήσουν με: την ίδια την CA και, εάν είναι εσωτερική, τον πάροχο ταυτότητάς σας. Διαχωρίστε τους με κόμματα.", - "updateStepCaEndpointsFailed": "Αποτυχία ενημέρωσης της λίστας επιτρεπόμενων τελικών σημείων Step CA", "notificationPrivateEndpoints": "Επιτρεπόμενοι ιδιωτικοί κεντρικοί υπολογιστές ειδοποιήσεων", "notificationPrivateEndpointsDesc": "Ακριβώς ορίστε τους ιδιωτικούς κεντρικούς υπολογιστές με τους οποίους ενδέχεται να επικοινωνήσουν τα κανάλια ειδοποιήσεων. Διαχωρίστε τους με κόμματα.", "updateNotificationEndpointsFailed": "Αποτυχία ενημέρωσης της λίστας επιτρεπόμενων τελικών σημείων ειδοποιήσεων" diff --git a/src/ui/locales/translated/es_ES.json b/src/ui/locales/translated/es_ES.json index 0ab0a1a00..ab8f19ee8 100644 --- a/src/ui/locales/translated/es_ES.json +++ b/src/ui/locales/translated/es_ES.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Frase de contraseña guardada, escriba para cambiarla", "replaceKey": "Reemplazar clave", "docsLink": "Ver documentación", - "stepcaLabel": "Paso CA", - "stepcaDesc": "Inicie sesión a través de su proveedor de identidad y permita que su CA de Step emita un certificado SSH de corta duración para este host. La URL de la CA, la huella digital raíz y el proveedor OIDC se configuran mediante un administrador en la sección de Configuración de administración.", "agentLabel": "Agente SSH", "agentDesc": "Autentíquese usando un agente SSH que se ejecuta en el servidor de Termix (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). El agente debe estar ejecutándose en la máquina donde está alojado Termix.", "agentAuthenticationDesc": "Utilice las claves de un agente SSH para autenticar esta conexión. Deje la ruta del socket vacía para usar SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "Guardar", "saving": "Guardando..." }, - "stepCa": "Paso CA", - "stepCaDesc": "Emita certificados SSH de corta duración desde una CA de paso pequeño. Introduzca la URL de la CA, su huella digital raíz (tal como la muestra step ca bootstrap) y el nombre del proveedor OIDC; a continuación, seleccione \"Step CA\" como tipo de autenticación del host. Deje los tres campos vacíos para desactivarlo.", - "stepCaFingerprint": "Huella digital de la raíz (SHA-256)", - "stepCaProvisioner": "nombre del proveedor OIDC", - "stepCaSaved": "Configuración de CA guardada", - "stepCaSaveFailed": "No se pudieron guardar los ajustes de Step CA.", "secretSourcePrivateEndpoints": "Hosts de fuentes secretas privadas permitidas", "secretSourcePrivateEndpointsDesc": "Hosts privados con los que fuentes secretas (1Password Connect) pueden contactar. Sepárelos con comas.", "updateSecretSourceEndpointsFailed": "No se pudo actualizar la lista de permitidos del punto final de origen secreto.", - "stepCaPrivateEndpoints": "Hosts de CA de paso privado permitidos", - "stepCaPrivateEndpointsDesc": "Los hosts privados con los que puede contactar el flujo de certificados de Step CA son: la propia CA y, si es interna, su proveedor de identidad. Sepárelos con comas.", - "updateStepCaEndpointsFailed": "No se pudo actualizar la lista de puntos finales permitidos de Step CA.", "notificationPrivateEndpoints": "Hosts de notificación privada permitidos", "notificationPrivateEndpointsDesc": "Hosts privados exactos con los que los canales de notificación pueden contactar. Sepárelos con comas.", "updateNotificationEndpointsFailed": "No se pudo actualizar la lista de permitidos del punto final de notificación." diff --git a/src/ui/locales/translated/fi_FI.json b/src/ui/locales/translated/fi_FI.json index e493a8e64..a05bcb8cb 100644 --- a/src/ui/locales/translated/fi_FI.json +++ b/src/ui/locales/translated/fi_FI.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Salasana tallennettu, kirjoita muuttaaksesi", "replaceKey": "Vaihda avain", "docsLink": "Näytä dokumentit", - "stepcaLabel": "Vaihe CA", - "stepcaDesc": "Kirjaudu sisään identiteetintarjoajasi kautta ja anna vaiheen varmentajan myöntää lyhytaikainen SSH-varmenne tälle isännälle. Varmentajan URL-osoitteen, pääkäyttäjän sormenjäljen ja OIDC-varmenteen määrittäjän määrittää järjestelmänvalvojan asetuksissa.", "agentLabel": "SSH-agentti", "agentDesc": "Todenna itsesi käyttämällä Termix-palvelimella käynnissä olevaa SSH-agenttia (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). Agentin on oltava käynnissä koneella, jolla Termix sijaitsee.", "agentAuthenticationDesc": "Käytä SSH-agentin avaimia tämän yhteyden todentamiseen. Jätä soketin polku tyhjäksi käyttääksesi SSH_AUTH_SOCK-ominaisuutta.", @@ -2121,18 +2119,9 @@ "save": "Tallentaa", "saving": "Tallennetaan..." }, - "stepCa": "Vaihe CA", - "stepCaDesc": "Myönnä lyhytaikaisia SSH-varmenteita smallstep-sertifikaatin myöntäjältä. Syötä sertifikaatin myöntäjän URL-osoite, sen juuritunniste (kuten step ca bootstrapissa näytetään) ja OIDC-palveluntarjoajan nimi. Valitse sitten isännän todennustyypiksi \"Step CA\". Jätä kaikki kolme tyhjiksi poistaaksesi sen käytöstä.", - "stepCaFingerprint": "Juurisormenjälki (SHA-256)", - "stepCaProvisioner": "OIDC-palveluntarjoajan nimi", - "stepCaSaved": "Vaiheen CA-asetukset tallennettu", - "stepCaSaveFailed": "Vaiheen CA-asetusten tallentaminen epäonnistui", "secretSourcePrivateEndpoints": "Sallitut yksityiset salaiset lähdepalvelimet", "secretSourcePrivateEndpointsDesc": "Yksityiset palvelimet, joihin salaiset lähteet (1Password Connect) voivat ottaa yhteyttä. Erota ne pilkuilla.", "updateSecretSourceEndpointsFailed": "Salaisen lähteen päätepisteen sallittujen luettelon päivittäminen epäonnistui.", - "stepCaPrivateEndpoints": "Sallitut yksityiset Step CA -isännät", - "stepCaPrivateEndpointsDesc": "Yksityiset isännöitsijät, joilla Step CA -varmennetyönkulku voi ottaa yhteyttä: itse CA:han ja, jos kyseessä on sisäinen varmenne, identiteetintarjoajaasi. Erota ne pilkuilla.", - "updateStepCaEndpointsFailed": "Vaiheen CA-päätepisteiden sallittujen luettelon päivittäminen epäonnistui.", "notificationPrivateEndpoints": "Sallitut yksityisten ilmoitusten isännät", "notificationPrivateEndpointsDesc": "Tarkat yksityiset isännät, joihin ilmoituskanavat voivat ottaa yhteyttä. Erota ne pilkuilla.", "updateNotificationEndpointsFailed": "Ilmoitusten päätepisteiden sallittujen luettelon päivittäminen epäonnistui." diff --git a/src/ui/locales/translated/fr_FR.json b/src/ui/locales/translated/fr_FR.json index b0be6ebe0..5d489d97f 100644 --- a/src/ui/locales/translated/fr_FR.json +++ b/src/ui/locales/translated/fr_FR.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Phrase secrète enregistrée, saisissez pour modifier", "replaceKey": "Remplacer la clé", "docsLink": "Voir la documentation", - "stepcaLabel": "Étape CA", - "stepcaDesc": "Connectez-vous via votre fournisseur d'identité et laissez votre autorité de certification Step CA émettre un certificat SSH temporaire pour cet hôte. L'URL de l'autorité de certification, l'empreinte racine et le provisionneur OIDC sont configurés par un administrateur dans les paramètres d'administration.", "agentLabel": "Agent SSH", "agentDesc": "Authentifiez-vous en utilisant un agent SSH exécuté sur le serveur Termix (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). L'agent doit être en cours d'exécution sur la machine où Termix est hébergé.", "agentAuthenticationDesc": "Utilisez les clés d'un agent SSH pour authentifier cette connexion. Laissez le chemin du socket vide pour utiliser SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "Enregistrer", "saving": "Enregistrement..." }, - "stepCa": "Étape CA", - "stepCaDesc": "Émettez des certificats SSH à durée de vie limitée auprès d'une autorité de certification Smallstep. Saisissez l'URL de l'autorité de certification, son empreinte racine (telle qu'affichée par l'outil d'amorçage de l'autorité de certification Smallstep) et le nom du provisionneur OIDC ; puis sélectionnez « Autorité de certification Smallstep » comme type d'authentification de l'hôte. Laissez ces trois champs vides pour désactiver l'authentification.", - "stepCaFingerprint": "Empreinte racine (SHA-256)", - "stepCaProvisioner": "Nom du fournisseur OIDC", - "stepCaSaved": "Paramètres de l'étape CA enregistrés", - "stepCaSaveFailed": "Échec de l'enregistrement des paramètres de l'étape CA", "secretSourcePrivateEndpoints": "Hôtes de sources de secrets privés autorisés", "secretSourcePrivateEndpointsDesc": "Hôtes privés que les sources secrètes (1Password Connect) peuvent contacter. Séparez-les par des virgules.", "updateSecretSourceEndpointsFailed": "Échec de la mise à jour de la liste blanche des points de terminaison sources secrets", - "stepCaPrivateEndpoints": "Hôtes d'autorité de certification privés autorisés", - "stepCaPrivateEndpointsDesc": "Les hôtes privés pour lesquels le flux de certificats Step CA peut être sollicité peuvent contacter : l’autorité de certification elle-même et, si elle est interne, votre fournisseur d’identité. Séparez-les par des virgules.", - "updateStepCaEndpointsFailed": "Échec de la mise à jour de la liste blanche du point de terminaison Step CA", "notificationPrivateEndpoints": "Hôtes de notifications privées autorisés", "notificationPrivateEndpointsDesc": "Indiquez précisément les hôtes privés que les canaux de notification peuvent contacter. Séparez-les par des virgules.", "updateNotificationEndpointsFailed": "Échec de la mise à jour de la liste blanche du point de terminaison de notification" diff --git a/src/ui/locales/translated/he_IL.json b/src/ui/locales/translated/he_IL.json index 3aeaf0c93..6d9a49bef 100644 --- a/src/ui/locales/translated/he_IL.json +++ b/src/ui/locales/translated/he_IL.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "סיסמת הסיסמה נשמרה, הקלד כדי לשנות", "replaceKey": "החלפת מפתח", "docsLink": "הצג מסמכים", - "stepcaLabel": "שלב CA", - "stepcaDesc": "היכנס דרך ספק הזהויות שלך ותן ל-Step CA שלך להנפיק אישור SSH קצר מועד עבור מארח זה. כתובת ה-URL של CA, טביעת האצבע של הבסיס וספק ה-OIDC מוגדרים על ידי מנהל מערכת תחת הגדרות מנהל מערכת.", "agentLabel": "סוכן SSH", "agentDesc": "בצע אימות באמצעות סוכן SSH הפועל על שרת Termix (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). הסוכן חייב לפעול על המכונה שבה מתארח Termix.", "agentAuthenticationDesc": "השתמש במפתחות מסוכן SSH כדי לאמת חיבור זה. השאר את נתיב ה-socket ריק כדי להשתמש ב-SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "לְהַצִיל", "saving": "חִסָכוֹן..." }, - "stepCa": "שלב CA", - "stepCaDesc": "הנפק אישורי SSH קצרי מועד מ-smallstep CA. הזן את כתובת ה-URL של CA, טביעת האצבע הבסיסית שלה (כפי שמוצג על ידי step ca bootstrap) ואת שם ספק ה-OIDC; לאחר מכן בחר \"Step CA\" כסוג האימות של המארח. השאר את שלושתם ריקים כדי להשבית.", - "stepCaFingerprint": "טביעת אצבע של שורש (SHA-256)", - "stepCaProvisioner": "שם ספק OIDC", - "stepCaSaved": "הגדרות שלב CA נשמרו", - "stepCaSaveFailed": "נכשלה שמירת הגדרות שלב CA", "secretSourcePrivateEndpoints": "מארחי מקור סודי פרטיים מורשים", "secretSourcePrivateEndpointsDesc": "מארחים פרטיים שמקורות סודיים (1Password Connect) עשויים ליצור קשר איתם. יש להפריד ביניהם באמצעות פסיקים.", "updateSecretSourceEndpointsFailed": "נכשל עדכון רשימת ההיתרים של נקודת הקצה של המקור הסודי", - "stepCaPrivateEndpoints": "מארחים פרטיים של Step CA מורשים", - "stepCaPrivateEndpointsDesc": "מארחים פרטיים, זרימת האישורים של Step CA עשויה ליצור קשר עם: רשות האישורים עצמה, ואם היא פנימית, עם ספק הזהויות שלך. הפרידו ביניהם באמצעות פסיקים.", - "updateStepCaEndpointsFailed": "נכשל עדכון רשימת ההיתרים של נקודת הקצה של Step CA", "notificationPrivateEndpoints": "מארחי התראות פרטיים מורשים", "notificationPrivateEndpointsDesc": "מארחים פרטיים מדויקים שערוצים של התראות עשויים ליצור איתם קשר. הפרד ביניהם באמצעות פסיקים.", "updateNotificationEndpointsFailed": "נכשל עדכון רשימת ההיתרים של נקודת הקצה של ההתראות" diff --git a/src/ui/locales/translated/hi_IN.json b/src/ui/locales/translated/hi_IN.json index d8842ce71..35c8f1673 100644 --- a/src/ui/locales/translated/hi_IN.json +++ b/src/ui/locales/translated/hi_IN.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "पासफ़्रेज़ सहेजा गया, बदलने के लिए टाइप करें", "replaceKey": "कुंजी बदलें", "docsLink": "दस्तावेज़ देखें", - "stepcaLabel": "स्टेप सीए", - "stepcaDesc": "अपने आइडेंटिटी प्रोवाइडर के माध्यम से साइन इन करें और अपने स्टेप सीए को इस होस्ट के लिए एक अल्पकालिक एसएसएच प्रमाणपत्र जारी करने दें। सीए यूआरएल, रूट फिंगरप्रिंट और ओआईडीसी प्रोविजनर को एडमिनिस्ट्रेटर द्वारा एडमिन सेटिंग्स के अंतर्गत सेट किया जाता है।", "agentLabel": "SSH एजेंट", "agentDesc": "Termix सर्वर पर चल रहे SSH एजेंट (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent) का उपयोग करके प्रमाणित करें। एजेंट उस मशीन पर चल रहा होना चाहिए जहां Termix होस्ट किया गया है।", "agentAuthenticationDesc": "इस कनेक्शन को प्रमाणित करने के लिए SSH एजेंट से कुंजियों का उपयोग करें। SSH_AUTH_SOCK का उपयोग करने के लिए सॉकेट पथ को खाली छोड़ दें।", @@ -2121,18 +2119,9 @@ "save": "सहेजें", "saving": "सहेज रहा है..." }, - "stepCa": "स्टेप सीए", - "stepCaDesc": "एक छोटे स्टेप सीए से अल्पकालिक एसएसएच प्रमाणपत्र जारी करें। सीए यूआरएल, उसका रूट फिंगरप्रिंट (जैसा कि स्टेप सीए बूटस्ट्रैप द्वारा दिखाया गया है) और ओआईडीसी प्रोविजनर का नाम दर्ज करें; फिर होस्ट के प्रमाणीकरण प्रकार के रूप में \"स्टेप सीए\" चुनें। इसे अक्षम करने के लिए तीनों को खाली छोड़ दें।", - "stepCaFingerprint": "रूट फिंगरप्रिंट (SHA-256)", - "stepCaProvisioner": "OIDC प्रोविज़नर नाम", - "stepCaSaved": "चरण CA सेटिंग्स सहेजी गईं", - "stepCaSaveFailed": "स्टेप CA सेटिंग्स को सहेजने में विफल", "secretSourcePrivateEndpoints": "निजी गुप्त स्रोत होस्टों को अनुमति दी गई", "secretSourcePrivateEndpointsDesc": "वे निजी होस्ट जिनसे गुप्त स्रोत (1Password Connect) संपर्क कर सकते हैं। इन्हें अल्पविराम से अलग करें।", "updateSecretSourceEndpointsFailed": "गुप्त स्रोत एंडपॉइंट अनुमति सूची को अपडेट करने में विफल।", - "stepCaPrivateEndpoints": "निजी स्टेप सीए होस्ट की अनुमति है", - "stepCaPrivateEndpointsDesc": "निजी होस्ट, स्टेप सीए प्रमाणपत्र प्रवाह के दौरान निम्नलिखित से संपर्क कर सकते हैं: सीए स्वयं और, यदि आंतरिक है, तो आपका पहचान प्रदाता। इन्हें अल्पविराम से अलग करें।", - "updateStepCaEndpointsFailed": "स्टेप सीए एंडपॉइंट अनुमति सूची को अपडेट करने में विफलता", "notificationPrivateEndpoints": "निजी अधिसूचना होस्टों को अनुमति दी गई", "notificationPrivateEndpointsDesc": "वे सटीक निजी होस्ट जिनके साथ नोटिफिकेशन चैनल संपर्क कर सकते हैं। इन्हें अल्पविराम से अलग करें।", "updateNotificationEndpointsFailed": "नोटिफिकेशन एंडपॉइंट की अनुमति सूची को अपडेट करने में विफलता" diff --git a/src/ui/locales/translated/hu_HU.json b/src/ui/locales/translated/hu_HU.json index c5d8f26c9..d84d07051 100644 --- a/src/ui/locales/translated/hu_HU.json +++ b/src/ui/locales/translated/hu_HU.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Jelszó mentve, gépelje be a módosításhoz", "replaceKey": "Kulcs cseréje", "docsLink": "Dokumentumok megtekintése", - "stepcaLabel": "CA lépés", - "stepcaDesc": "Jelentkezzen be az identitásszolgáltatóján keresztül, és hagyja, hogy a lépéses hitelesítésszolgáltatója rövid életű SSH-tanúsítványt állítson ki ehhez a gazdagéphez. A hitelesítésszolgáltató URL-címét, a root ujjlenyomatát és az OIDC-kiszolgálót a rendszergazda állítja be az Adminisztrátori beállítások alatt.", "agentLabel": "SSH-ügynök", "agentDesc": "Hitelesítést végezzen egy, a Termix szerveren futó SSH ügynökkel (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). Az ügynöknek azon a gépen kell futnia, amelyen a Termix fut.", "agentAuthenticationDesc": "SSH ügynöktől származó kulcsok használata a kapcsolat hitelesítéséhez. Az SSH_AUTH_SOCK használatához hagyja üresen a socket elérési útját.", @@ -2121,18 +2119,9 @@ "save": "Megtakarítás", "saving": "Megtakarítás..." }, - "stepCa": "CA lépés", - "stepCaDesc": "Rövid életű SSH tanúsítványok kibocsátása smallstep CA-tól. Adja meg a CA URL-címét, a gyökér ujjlenyomatát (ahogy a step ca bootstrap mutatja) és az OIDC szolgáltató nevét; majd válassza a „Step CA” lehetőséget a gazdagép hitelesítési típusaként. A letiltáshoz hagyja mindhármat üresen.", - "stepCaFingerprint": "Gyökér ujjlenyomat (SHA-256)", - "stepCaProvisioner": "OIDC-szolgáltató neve", - "stepCaSaved": "Lépés CA beállításai mentve", - "stepCaSaveFailed": "Nem sikerült menteni a Step CA beállításait", "secretSourcePrivateEndpoints": "Engedélyezett privát titkos forráshosztok", "secretSourcePrivateEndpointsDesc": "Privát tárhelyszolgáltatók, amelyekkel titkos források (1Password Connect) kapcsolatba léphetnek. Vesszővel válassza el őket.", "updateSecretSourceEndpointsFailed": "Nem sikerült frissíteni a titkos forrásvégpontok engedélyezőlistáját.", - "stepCaPrivateEndpoints": "Engedélyezett privát Step CA-hosztok", - "stepCaPrivateEndpointsDesc": "A Step CA tanúsítványfolyamat által érintett privát hosztok a következőkkel vehetik fel a kapcsolatot: maga a CA, és ha belső, akkor az identitásszolgáltató. Vesszővel válassza el őket.", - "updateStepCaEndpointsFailed": "Nem sikerült frissíteni a Step CA végpontok engedélyezőlistáját.", "notificationPrivateEndpoints": "Engedélyezett privát értesítési hosztok", "notificationPrivateEndpointsDesc": "Pontosan azok a privát hosztok, amelyekkel az értesítési csatornák kapcsolatba léphetnek. Vesszővel válassza el őket.", "updateNotificationEndpointsFailed": "Nem sikerült frissíteni az értesítési végpontok engedélyezőlistáját" diff --git a/src/ui/locales/translated/id_ID.json b/src/ui/locales/translated/id_ID.json index 641a1973c..0206606eb 100644 --- a/src/ui/locales/translated/id_ID.json +++ b/src/ui/locales/translated/id_ID.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Kata sandi tersimpan, ketik untuk mengubahnya", "replaceKey": "Ganti kunci", "docsLink": "Lihat dokumen", - "stepcaLabel": "Langkah CA", - "stepcaDesc": "Masuk melalui penyedia identitas Anda dan biarkan Step CA Anda menerbitkan sertifikat SSH berumur pendek untuk host ini. URL CA, sidik jari root, dan penyedia OIDC diatur oleh administrator di bawah Pengaturan Admin.", "agentLabel": "Agen SSH", "agentDesc": "Lakukan otentikasi menggunakan agen SSH yang berjalan di server Termix (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). Agen tersebut harus berjalan di mesin tempat Termix dihosting.", "agentAuthenticationDesc": "Gunakan kunci dari agen SSH untuk mengautentikasi koneksi ini. Biarkan jalur soket kosong untuk menggunakan SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "Menyimpan", "saving": "Penghematan..." }, - "stepCa": "Langkah CA", - "stepCaDesc": "Terbitkan sertifikat SSH berumur pendek dari CA smallstep. Masukkan URL CA, sidik jari root-nya (seperti yang ditunjukkan oleh step ca bootstrap) dan nama penyedia OIDC; kemudian pilih \"Step CA\" sebagai tipe otentikasi host. Biarkan ketiganya kosong untuk menonaktifkan.", - "stepCaFingerprint": "Sidik jari akar (SHA-256)", - "stepCaProvisioner": "Nama penyedia OIDC", - "stepCaSaved": "Pengaturan CA langkah tersimpan", - "stepCaSaveFailed": "Gagal menyimpan pengaturan Langkah CA.", "secretSourcePrivateEndpoints": "Host sumber rahasia pribadi yang diizinkan", "secretSourcePrivateEndpointsDesc": "Host pribadi yang dapat dihubungi oleh sumber rahasia (1Password Connect). Pisahkan dengan koma.", "updateSecretSourceEndpointsFailed": "Gagal memperbarui daftar izin titik akhir sumber rahasia.", - "stepCaPrivateEndpoints": "Host CA Langkah pribadi yang diizinkan", - "stepCaPrivateEndpointsDesc": "Host privat yang dapat dihubungi oleh alur sertifikat Step CA adalah: CA itu sendiri dan, jika internal, penyedia identitas Anda. Pisahkan mereka dengan koma.", - "updateStepCaEndpointsFailed": "Gagal memperbarui daftar endpoint Step CA yang diizinkan.", "notificationPrivateEndpoints": "Host pemberitahuan pribadi yang diizinkan", "notificationPrivateEndpointsDesc": "Host privat yang tepat yang mungkin dihubungi oleh saluran pemberitahuan. Pisahkan dengan koma.", "updateNotificationEndpointsFailed": "Gagal memperbarui daftar izin titik akhir notifikasi." diff --git a/src/ui/locales/translated/it_IT.json b/src/ui/locales/translated/it_IT.json index 188a2b6d6..462d65593 100644 --- a/src/ui/locales/translated/it_IT.json +++ b/src/ui/locales/translated/it_IT.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Passphrase salvata, digita per modificare", "replaceKey": "Sostituisci chiave", "docsLink": "Visualizza documentazione", - "stepcaLabel": "Passo CA", - "stepcaDesc": "Accedi tramite il tuo provider di identità e lascia che la tua CA Step emetta un certificato SSH temporaneo per questo host. L'URL della CA, l'impronta digitale della radice e il provisioner OIDC vengono impostati da un amministratore nelle Impostazioni di amministrazione.", "agentLabel": "Agente SSH", "agentDesc": "Autenticati usando un agente SSH in esecuzione sul server Termix (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). L'agente deve essere in esecuzione sulla macchina dove è ospitato Termix.", "agentAuthenticationDesc": "Utilizza le chiavi di un agente SSH per autenticare questa connessione. Lascia vuoto il percorso del socket per utilizzare SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "Salva", "saving": "Salvataggio in corso..." }, - "stepCa": "Passo CA", - "stepCaDesc": "Emetti certificati SSH di breve durata da una CA di smallstep. Inserisci l'URL della CA, la sua impronta digitale radice (come mostrato da step ca bootstrap) e il nome del provisioner OIDC; quindi scegli \"Step CA\" come tipo di autenticazione dell'host. Lascia tutti e tre i campi vuoti per disabilitare.", - "stepCaFingerprint": "Impronta digitale radice (SHA-256)", - "stepCaProvisioner": "Nome del fornitore OIDC", - "stepCaSaved": "Impostazioni di Step CA salvate", - "stepCaSaveFailed": "Impossibile salvare le impostazioni di Step CA", "secretSourcePrivateEndpoints": "Host di fonti di segreti privati consentiti", "secretSourcePrivateEndpointsDesc": "Host privati che le fonti segrete (1Password Connect) potrebbero contattare. Separarli con delle virgole.", "updateSecretSourceEndpointsFailed": "Impossibile aggiornare l'elenco consentito dell'endpoint della sorgente segreta.", - "stepCaPrivateEndpoints": "Host Step CA privati consentiti", - "stepCaPrivateEndpointsDesc": "Gli host privati che utilizzano il flusso di certificati Step CA potrebbero contattare: la CA stessa e, se interna, il provider di identità. Separarli con delle virgole.", - "updateStepCaEndpointsFailed": "Impossibile aggiornare l'elenco di elementi consentiti dell'endpoint Step CA.", "notificationPrivateEndpoints": "Host di notifica privata consentiti", "notificationPrivateEndpointsDesc": "Indizi precisi degli host privati che i canali di notifica possono contattare. Separarli con delle virgole.", "updateNotificationEndpointsFailed": "Impossibile aggiornare la lista di elementi consentiti dell'endpoint di notifica." diff --git a/src/ui/locales/translated/ja_JP.json b/src/ui/locales/translated/ja_JP.json index 9a8e8431d..d3d43eb7a 100644 --- a/src/ui/locales/translated/ja_JP.json +++ b/src/ui/locales/translated/ja_JP.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "パスフレーズが保存されました。変更するには入力してください", "replaceKey": "鍵を置き換え", "docsLink": "ドキュメントを表示", - "stepcaLabel": "ステップCA", - "stepcaDesc": "IDプロバイダー経由でサインインし、Step CAにこのホスト用の有効期限の短いSSH証明書を発行させてください。CAのURL、ルートフィンガープリント、およびOIDCプロビジョナーは、管理者が「管理設定」で設定します。", "agentLabel": "SSHエージェント", "agentDesc": "Termixサーバー上で動作しているSSHエージェント(Bitwarden、1Password、gpg-agent、KeeAgent、ssh-agent)を使用して認証します。エージェントはTermixがホストされているマシン上で実行されている必要があります。", "agentAuthenticationDesc": "この接続を認証するには、SSHエージェントのキーを使用します。SSH_AUTH_SOCKを使用する場合は、ソケットパスを空欄にしてください。", @@ -2121,18 +2119,9 @@ "save": "保存", "saving": "保存中..." }, - "stepCa": "ステップCA", - "stepCaDesc": "smallstep CA から有効期限の短い SSH 証明書を発行します。CA の URL、ルートフィンガープリント (step ca bootstrap で表示されるもの)、および OIDC プロビジョナーの名前を入力し、ホストの認証タイプとして「Step CA」を選択します。3 つの項目すべてを空欄にすると無効になります。", - "stepCaFingerprint": "ルートフィンガープリント(SHA-256)", - "stepCaProvisioner": "OIDCプロビジョナー名", - "stepCaSaved": "ステップCAの設定が保存されました", - "stepCaSaveFailed": "ステップCAの設定を保存できませんでした", "secretSourcePrivateEndpoints": "許可されたプライベートシークレットソースホスト", "secretSourcePrivateEndpointsDesc": "秘密情報源(1Password Connectなど)が連絡を取る可能性のあるプライベートホスト。カンマで区切ってください。", "updateSecretSourceEndpointsFailed": "シークレットソースエンドポイントの許可リストの更新に失敗しました", - "stepCaPrivateEndpoints": "許可されたプライベートStep CAホスト", - "stepCaPrivateEndpointsDesc": "プライベートホストの場合、Step CA 証明書フローは、認証局自体、および内部の場合は ID プロバイダーと連絡を取ることができます。これらはカンマで区切ってください。", - "updateStepCaEndpointsFailed": "Step CAエンドポイントの許可リストの更新に失敗しました", "notificationPrivateEndpoints": "許可されたプライベート通知ホスト", "notificationPrivateEndpointsDesc": "通知チャネルが接続する可能性のあるプライベートホストを正確に指定します。カンマで区切ってください。", "updateNotificationEndpointsFailed": "通知エンドポイントの許可リストの更新に失敗しました" diff --git a/src/ui/locales/translated/ko_KR.json b/src/ui/locales/translated/ko_KR.json index 772356fd4..3b2e4db01 100644 --- a/src/ui/locales/translated/ko_KR.json +++ b/src/ui/locales/translated/ko_KR.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "암호가 저장되었습니다. 변경하려면 입력하세요.", "replaceKey": "키 교체", "docsLink": "문서 보기", - "stepcaLabel": "스텝 CA", - "stepcaDesc": "ID 공급자를 통해 로그인하고 Step CA에서 이 호스트에 대한 단기 SSH 인증서를 발급받으세요. CA URL, 루트 지문 및 OIDC 프로비저너는 관리자가 관리자 설정에서 설정합니다.", "agentLabel": "SSH 에이전트", "agentDesc": "Termix 서버에서 실행 중인 SSH 에이전트(Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent)를 사용하여 인증합니다. 에이전트는 Termix가 호스팅된 시스템에서 실행 중이어야 합니다.", "agentAuthenticationDesc": "SSH 에이전트의 키를 사용하여 이 연결을 인증하십시오. SSH_AUTH_SOCK을 사용하려면 소켓 경로를 비워 두십시오.", @@ -2121,18 +2119,9 @@ "save": "저장", "saving": "저장 중..." }, - "stepCa": "스텝 CA", - "stepCaDesc": "SmallStep CA에서 단기 SSH 인증서를 발급합니다. CA URL, 루트 지문(step ca bootstrap 명령으로 확인 가능) 및 OIDC 프로비저너 이름을 입력하고 호스트 인증 유형으로 \"Step CA\"를 선택합니다. 세 항목을 모두 비워두면 해당 기능을 비활성화할 수 있습니다.", - "stepCaFingerprint": "루트 지문(SHA-256)", - "stepCaProvisioner": "OIDC 프로비저너 이름", - "stepCaSaved": "단계 CA 설정이 저장되었습니다", - "stepCaSaveFailed": "Step CA 설정을 저장하는 데 실패했습니다.", "secretSourcePrivateEndpoints": "허용된 개인 비밀 소스 호스트", "secretSourcePrivateEndpointsDesc": "비밀 소스(1Password Connect)가 연결할 수 있는 비공개 호스트를 나열하세요. 쉼표로 구분하세요.", "updateSecretSourceEndpointsFailed": "비밀 소스 엔드포인트 허용 목록 업데이트에 실패했습니다.", - "stepCaPrivateEndpoints": "허용된 개인 Step CA 호스트", - "stepCaPrivateEndpointsDesc": "Step CA 인증서 흐름이 접촉할 수 있는 개인 호스트는 CA 자체와 (내부 호스트인 경우) ID 공급자입니다. 이들을 쉼표로 구분하세요.", - "updateStepCaEndpointsFailed": "Step CA 엔드포인트 허용 목록 업데이트에 실패했습니다.", "notificationPrivateEndpoints": "허용된 개인 알림 호스트", "notificationPrivateEndpointsDesc": "알림 채널이 연락할 수 있는 정확한 비공개 호스트를 입력하세요. 쉼표로 구분하세요.", "updateNotificationEndpointsFailed": "알림 엔드포인트 허용 목록 업데이트에 실패했습니다." diff --git a/src/ui/locales/translated/nl_NL.json b/src/ui/locales/translated/nl_NL.json index 955b338ce..2997a793d 100644 --- a/src/ui/locales/translated/nl_NL.json +++ b/src/ui/locales/translated/nl_NL.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Wachtwoordzin opgeslagen, typ om te wijzigen", "replaceKey": "Vervang de sleutel", "docsLink": "Documenten bekijken", - "stepcaLabel": "Stap CA", - "stepcaDesc": "Meld u aan via uw identiteitsprovider en laat uw Step CA een kortstondig SSH-certificaat voor deze host uitgeven. De CA-URL, root-fingerprint en OIDC-provisioner worden door een beheerder ingesteld onder Beheerinstellingen.", "agentLabel": "SSH-agent", "agentDesc": "Authenticeer met behulp van een SSH-agent die op de Termix-server draait (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). De agent moet draaien op de machine waarop Termix is gehost.", "agentAuthenticationDesc": "Gebruik sleutels van een SSH-agent om deze verbinding te authenticeren. Laat het socketpad leeg om SSH_AUTH_SOCK te gebruiken.", @@ -2121,18 +2119,9 @@ "save": "Redden", "saving": "Besparing..." }, - "stepCa": "Stap CA", - "stepCaDesc": "Geef kortstondige SSH-certificaten uit via een smallstep CA. Voer de URL van de CA, de root-fingerprint (zoals weergegeven door step ca bootstrap) en de naam van de OIDC-provisioner in; kies vervolgens \"Step CA\" als authenticatietype voor de host. Laat alle drie de velden leeg om deze functie uit te schakelen.", - "stepCaFingerprint": "Root-vingerafdruk (SHA-256)", - "stepCaProvisioner": "Naam van de OIDC-leverancier", - "stepCaSaved": "Stap CA-instellingen opgeslagen", - "stepCaSaveFailed": "Het opslaan van de Step CA-instellingen is mislukt.", "secretSourcePrivateEndpoints": "Toegestane hosts voor privégeheime bronnen", "secretSourcePrivateEndpointsDesc": "Privéhosts die mogelijk door geheime bronnen (1Password Connect) worden benaderd. Scheid ze met komma's.", "updateSecretSourceEndpointsFailed": "Het bijwerken van de whitelist voor geheime bron-eindpunten is mislukt.", - "stepCaPrivateEndpoints": "Toegestane privé Step CA-hosts", - "stepCaPrivateEndpointsDesc": "De private hosts waarmee de Step CA-certificaatstroom contact kan opnemen, zijn: de certificeringsinstantie zelf en, indien intern, uw identiteitsprovider. Scheid deze met komma's.", - "updateStepCaEndpointsFailed": "Het bijwerken van de Step CA-eindpunt-whitelist is mislukt.", "notificationPrivateEndpoints": "Toegestane hosts voor privénotificaties", "notificationPrivateEndpointsDesc": "De exacte privéhosts die via notificatiekanalen gecontacteerd mogen worden. Scheid ze met komma's.", "updateNotificationEndpointsFailed": "Het bijwerken van de lijst met toegestane notificatie-eindpunten is mislukt." diff --git a/src/ui/locales/translated/no_NO.json b/src/ui/locales/translated/no_NO.json index c5f33f1e8..c7b909a67 100644 --- a/src/ui/locales/translated/no_NO.json +++ b/src/ui/locales/translated/no_NO.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Passordfrase lagret, skriv inn for å endre", "replaceKey": "Bytt ut nøkkelen", "docsLink": "Vis dokumenter", - "stepcaLabel": "Trinn CA", - "stepcaDesc": "Logg på via identitetsleverandøren din, og la Step CA utstede et kortvarig SSH-sertifikat for denne verten. CA-URL-en, rotfingeravtrykket og OIDC-provisioneren angis av en administrator under Administratorinnstillinger.", "agentLabel": "SSH-agent", "agentDesc": "Autentiser ved hjelp av en SSH-agent som kjører på Termix-serveren (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). Agenten må kjøre på maskinen der Termix er driftet.", "agentAuthenticationDesc": "Bruk nøkler fra en SSH-agent for å autentisere denne tilkoblingen. La socket-banen stå tom for å bruke SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "Spare", "saving": "Lagrer..." }, - "stepCa": "Trinn CA", - "stepCaDesc": "Utsted kortlivede SSH-sertifikater fra en smallstep CA. Skriv inn CA-URL-en, rotfingeravtrykket (som vist i step ca bootstrap) og navnet på OIDC-provisioneren; velg deretter \"Step CA\" som verts autentiseringstype. La alle tre stå tomme for å deaktivere.", - "stepCaFingerprint": "Rotfingeravtrykk (SHA-256)", - "stepCaProvisioner": "OIDC-provisionernavn", - "stepCaSaved": "Innstillinger for trinn CA er lagret", - "stepCaSaveFailed": "Kunne ikke lagre innstillingene for trinn CA", "secretSourcePrivateEndpoints": "Tillatte private hemmelige kildeverter", "secretSourcePrivateEndpointsDesc": "Private verter som hemmelige kilder (1Password Connect) kan kontakte. Skill dem med komma.", "updateSecretSourceEndpointsFailed": "Kunne ikke oppdatere tillatelseslisten for hemmelig kildeendepunkt", - "stepCaPrivateEndpoints": "Tillatte private Step CA-verter", - "stepCaPrivateEndpointsDesc": "Private verter som Step CA-sertifikatflyten kan kontakte: selve CA-en og, hvis intern, identitetsleverandøren din. Skill dem med komma.", - "updateStepCaEndpointsFailed": "Kunne ikke oppdatere tillatelseslisten for Step CA-endepunktet", "notificationPrivateEndpoints": "Tillatte private varslingsverter", "notificationPrivateEndpointsDesc": "Nøyaktige private verter som varslingskanaler kan kontakte. Skill dem med komma.", "updateNotificationEndpointsFailed": "Kunne ikke oppdatere tillatelseslisten for varslingsendepunktet" diff --git a/src/ui/locales/translated/pl_PL.json b/src/ui/locales/translated/pl_PL.json index 457301f34..8b1542670 100644 --- a/src/ui/locales/translated/pl_PL.json +++ b/src/ui/locales/translated/pl_PL.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Hasło klucza zapisane, wpisz, aby zmienić", "replaceKey": "Zastąp klucz", "docsLink": "Zobacz dokumentację", - "stepcaLabel": "Krok CA", - "stepcaDesc": "Zaloguj się za pomocą swojego dostawcy tożsamości i pozwól, aby urząd certyfikacji Step CA wydał tymczasowy certyfikat SSH dla tego hosta. Adres URL urzędu certyfikacji, odcisk palca głównego i dostawca OIDC są ustawiane przez administratora w Ustawieniach administratora.", "agentLabel": "Agent SSH", "agentDesc": "Uwierzytelnij się za pomocą agenta SSH uruchomionego na serwerze Termix (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). Agent musi być uruchomiony na komputerze, na którym hostowany jest Termix.", "agentAuthenticationDesc": "Użyj kluczy agenta SSH do uwierzytelnienia tego połączenia. Pozostaw ścieżkę do gniazda pustą, aby użyć SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "Zapisz", "saving": "Zapisywanie..." }, - "stepCa": "Krok CA", - "stepCaDesc": "Wydaj krótkotrwałe certyfikaty SSH z urzędu certyfikacji smallstep. Wprowadź adres URL urzędu certyfikacji, jego odcisk główny (jak pokazano w pliku bootstrap urzędu certyfikacji step) oraz nazwę dostawcy OIDC; następnie wybierz „Step CA” jako typ uwierzytelniania hosta. Pozostaw wszystkie trzy pola puste, aby wyłączyć uwierzytelnianie.", - "stepCaFingerprint": "Odcisk palca głównego (SHA-256)", - "stepCaProvisioner": "Nazwa dostawcy OIDC", - "stepCaSaved": "Ustawienia kroku CA zapisane", - "stepCaSaveFailed": "Nie udało się zapisać ustawień kroku CA", "secretSourcePrivateEndpoints": "Dozwolone prywatne, tajne źródła hostów", "secretSourcePrivateEndpointsDesc": "Prywatne serwery, z którymi mogą kontaktować się tajne źródła (1Password Connect). Rozdziel je przecinkami.", "updateSecretSourceEndpointsFailed": "Nie udało się zaktualizować listy dozwolonych punktów końcowych źródła tajnego", - "stepCaPrivateEndpoints": "Dozwolone prywatne hosty Step CA", - "stepCaPrivateEndpointsDesc": "Prywatni hosty, z których przepływ certyfikatów Step CA może się kontaktować: z samym CA oraz, jeśli jest wewnętrzny, z Twoim dostawcą tożsamości. Rozdziel je przecinkami.", - "updateStepCaEndpointsFailed": "Nie udało się zaktualizować listy dozwolonych punktów końcowych urzędu certyfikacji kroku", "notificationPrivateEndpoints": "Dozwolone hosty powiadomień prywatnych", "notificationPrivateEndpointsDesc": "Dokładne nazwy prywatnych hostów, z którymi mogą kontaktować się kanały powiadomień. Oddziel je przecinkami.", "updateNotificationEndpointsFailed": "Nie udało się zaktualizować listy dozwolonych punktów końcowych powiadomień" diff --git a/src/ui/locales/translated/pt_BR.json b/src/ui/locales/translated/pt_BR.json index 889b8ef7f..2691c87e1 100644 --- a/src/ui/locales/translated/pt_BR.json +++ b/src/ui/locales/translated/pt_BR.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Senha salva, digite para alterar", "replaceKey": "Substituir chave", "docsLink": "Ver documentos", - "stepcaLabel": "Etapa CA", - "stepcaDesc": "Faça login através do seu provedor de identidade e permita que a sua Autoridade Certificadora (CA) Step emita um certificado SSH de curta duração para este host. O URL da CA, a impressão digital da raiz e o provisionador OIDC são definidos por um administrador em Configurações de Administrador.", "agentLabel": "Agente SSH", "agentDesc": "Autentique-se usando um agente SSH em execução no servidor Termix (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). O agente deve estar em execução na máquina onde o Termix está hospedado.", "agentAuthenticationDesc": "Use as chaves de um agente SSH para autenticar esta conexão. Deixe o caminho do socket vazio para usar SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "Salvar", "saving": "Salvando..." }, - "stepCa": "Etapa CA", - "stepCaDesc": "Emita certificados SSH de curta duração a partir de uma CA Smallstep. Insira o URL da CA, sua impressão digital raiz (conforme mostrado pelo comando `step ca bootstrap`) e o nome do provisionador OIDC; em seguida, escolha \"Step CA\" como o tipo de autenticação do host. Deixe os três campos em branco para desativar.", - "stepCaFingerprint": "Impressão digital raiz (SHA-256)", - "stepCaProvisioner": "nome do provisionador OIDC", - "stepCaSaved": "Etapa Configurações CA salvas", - "stepCaSaveFailed": "Falha ao salvar as configurações da Etapa CA", "secretSourcePrivateEndpoints": "Hosts de fontes secretas privadas permitidas", "secretSourcePrivateEndpointsDesc": "Hosts privados que fontes secretas (1Password Connect) podem contatar. Separe-os com vírgulas.", "updateSecretSourceEndpointsFailed": "Falha ao atualizar a lista de permissões do endpoint de origem secreta.", - "stepCaPrivateEndpoints": "Hosts CA privados permitidos", - "stepCaPrivateEndpointsDesc": "Para hosts privados, o fluxo de certificados da Step CA pode contatar: a própria CA e, se interna, seu provedor de identidade. Separe-os com vírgulas.", - "updateStepCaEndpointsFailed": "Falha ao atualizar a lista de permissões do endpoint da Autoridade Certificadora (CA) da Etapa.", "notificationPrivateEndpoints": "Hosts de notificação privada permitidos", "notificationPrivateEndpointsDesc": "Nomeie os hosts privados exatos que os canais de notificação podem contatar. Separe-os com vírgulas.", "updateNotificationEndpointsFailed": "Falha ao atualizar a lista de permissões do endpoint de notificação." diff --git a/src/ui/locales/translated/pt_PT.json b/src/ui/locales/translated/pt_PT.json index 752aa6b4f..04d61addd 100644 --- a/src/ui/locales/translated/pt_PT.json +++ b/src/ui/locales/translated/pt_PT.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Frase-passe guardada, escreva para alterar", "replaceKey": "Substituir chave", "docsLink": "Ver documentação", - "stepcaLabel": "Etapa CA", - "stepcaDesc": "Faça login através do seu fornecedor de identidade e permita que a sua Autoridade Certificadora (CA) Step emita um certificado SSH de curta duração para este host. O URL da AC, a impressão digital da raiz e o provisionador OIDC são definidos por um administrador em Definições de Administrador.", "agentLabel": "Agente SSH", "agentDesc": "Autentique-se usando um agente SSH em execução no servidor Termix (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). O agente deve estar em execução na máquina onde o Termix está alojado.", "agentAuthenticationDesc": "Utilize as chaves de um agente SSH para autenticar esta ligação. Deixe o caminho do socket vazio para utilizar SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "Guardar", "saving": "A guardar..." }, - "stepCa": "Etapa CA", - "stepCaDesc": "Emita certificados SSH de curta duração a partir de uma CA Smallstep. Introduza o URL da AC, a sua impressão digital raiz (conforme mostrado pelo comando `step ca bootstrap`) e o nome do provisionador OIDC; em seguida, escolha \"Step CA\" como tipo de autenticação do host. Deixe os três campos em branco para desativar.", - "stepCaFingerprint": "Impressão digital de raiz (SHA-256)", - "stepCaProvisioner": "nome do provisionador OIDC", - "stepCaSaved": "Passo Configurações CA guardadas", - "stepCaSaveFailed": "Falha ao guardar as definições da Etapa CA", "secretSourcePrivateEndpoints": "Hosts de fontes secretas privadas permitidas", "secretSourcePrivateEndpointsDesc": "Hosts privados que as fontes secretas (1Password Connect) podem contactar. Separe-os com vírgulas.", "updateSecretSourceEndpointsFailed": "Falha ao atualizar a lista de permissões do endpoint de origem secreta.", - "stepCaPrivateEndpoints": "Hosts CA privados permitidos", - "stepCaPrivateEndpointsDesc": "Para hosts privados, o fluxo de certificados da Step CA pode contactar: a própria CA e, se for interna, o seu fornecedor de identidade. Separe-os com vírgulas.", - "updateStepCaEndpointsFailed": "Falha ao atualizar a lista de permissões do endpoint da Autoridade Certificadora (CA) da Etapa.", "notificationPrivateEndpoints": "Hosts de notificação privada permitidos", "notificationPrivateEndpointsDesc": "Nomeie os hosts privados exatos que os canais de notificação podem contactar. Separe-os com vírgulas.", "updateNotificationEndpointsFailed": "Falha ao atualizar a lista de permissões do endpoint de notificação." diff --git a/src/ui/locales/translated/ro_RO.json b/src/ui/locales/translated/ro_RO.json index 8dca93aa5..84d575c8a 100644 --- a/src/ui/locales/translated/ro_RO.json +++ b/src/ui/locales/translated/ro_RO.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Parolă salvată, tastați pentru a o modifica", "replaceKey": "Înlocuiți cheia", "docsLink": "Vizualizați documentele", - "stepcaLabel": "Pasul CA", - "stepcaDesc": "Conectați-vă prin intermediul furnizorului de identitate și permiteți autorității de certificare Step CA să emită un certificat SSH de scurtă durată pentru această gazdă. Adresa URL a autorității de certificare, amprenta rădăcină și furnizorul OIDC sunt setate de un administrator în Setări administrator.", "agentLabel": "Agent SSH", "agentDesc": "Autentificați-vă folosind un agent SSH care rulează pe serverul Termix (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). Agentul trebuie să ruleze pe mașina pe care este găzduit Termix.", "agentAuthenticationDesc": "Folosește chei de la un agent SSH pentru a autentifica această conexiune. Lasă calea socketului goală pentru a utiliza SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "Salva", "saving": "Economisire..." }, - "stepCa": "Pasul CA", - "stepCaDesc": "Emiteți certificate SSH de scurtă durată de viață de la o CA Smallstep. Introduceți adresa URL a CA, amprenta sa rădăcină (așa cum se arată în bootstrap-ul CA Step) și numele furnizorului OIDC; apoi alegeți „Step CA” ca tip de autentificare al gazdei. Lăsați toate trei golite pentru a dezactiva.", - "stepCaFingerprint": "Amprentă digitală rădăcină (SHA-256)", - "stepCaProvisioner": "Numele furnizorului OIDC", - "stepCaSaved": "Setările Step CA au fost salvate", - "stepCaSaveFailed": "Setările Step CA nu au putut fi salvate.", "secretSourcePrivateEndpoints": "Gazde sursă secrete private permise", "secretSourcePrivateEndpointsDesc": "Gazde private pe care sursele secrete (1Password Connect) le pot contacta. Separați-le prin virgule.", "updateSecretSourceEndpointsFailed": "Nu s-a putut actualiza lista de permise pentru endpoint-ul sursă secretă", - "stepCaPrivateEndpoints": "Gazde private permise de Step CA", - "stepCaPrivateEndpointsDesc": "Gazde private pe care fluxul de certificate Step CA le poate contacta: CA-ul în sine și, dacă este intern, furnizorul dvs. de identitate. Separați-le prin virgule.", - "updateStepCaEndpointsFailed": "Nu s-a putut actualiza lista de permise pentru endpoint-ul Step CA.", "notificationPrivateEndpoints": "Gazde de notificări private permise", "notificationPrivateEndpointsDesc": "Gazde private exacte pe care canalele de notificare le pot contacta. Separați-le prin virgule.", "updateNotificationEndpointsFailed": "Nu s-a putut actualiza lista de permise pentru punctul final de notificare" diff --git a/src/ui/locales/translated/ru_RU.json b/src/ui/locales/translated/ru_RU.json index 4a44fed6d..0bec6dc37 100644 --- a/src/ui/locales/translated/ru_RU.json +++ b/src/ui/locales/translated/ru_RU.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Парольная фраза сохранена, введите для изменения", "replaceKey": "Заменить ключ", "docsLink": "Документация", - "stepcaLabel": "Шаг CA", - "stepcaDesc": "Войдите в систему через своего поставщика идентификации и позвольте вашему центру сертификации Step CA выдать краткосрочный SSH-сертификат для этого хоста. URL-адрес центра сертификации, отпечаток корневого сертификата и средство подготовки OIDC задаются администратором в разделе «Настройки администратора».", "agentLabel": "SSH-агент", "agentDesc": "Аутентификация с помощью SSH-агента, запущенного на сервере Termix (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). Агент должен работать на машине, где размещён Termix.", "agentAuthenticationDesc": "Для аутентификации этого соединения используйте ключи от SSH-агента. Оставьте поле пути к сокету пустым, чтобы использовать SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "Сохранить", "saving": "Сохранение…" }, - "stepCa": "Шаг CA", - "stepCaDesc": "Выдача краткосрочных SSH-сертификатов от центра сертификации smallstep. Введите URL-адрес центра сертификации, его корневой отпечаток (как показано командой step ca bootstrap) и имя поставщика OIDC; затем выберите «Step CA» в качестве типа аутентификации хоста. Оставьте все три поля пустыми, чтобы отключить эту функцию.", - "stepCaFingerprint": "Отпечаток корневого ключа (SHA-256)", - "stepCaProvisioner": "имя поставщика OIDC", - "stepCaSaved": "Настройки шага CA сохранены.", - "stepCaSaveFailed": "Не удалось сохранить настройки шага CA.", "secretSourcePrivateEndpoints": "Разрешены хосты источников частных секретов", "secretSourcePrivateEndpointsDesc": "Частные хосты, с которыми могут связываться секретные источники (1Password Connect). Разделяйте их запятыми.", "updateSecretSourceEndpointsFailed": "Не удалось обновить список разрешенных конечных точек источника секретных данных.", - "stepCaPrivateEndpoints": "Разрешены частные хосты Step CA", - "stepCaPrivateEndpointsDesc": "Частные хосты, для которых выполняется процедура сертификации Step CA, могут связаться с: самим центром сертификации и, если он внутренний, с вашим поставщиком идентификации. Разделяйте их запятыми.", - "updateStepCaEndpointsFailed": "Не удалось обновить список разрешенных конечных точек Step CA.", "notificationPrivateEndpoints": "Разрешены частные хосты для уведомлений", "notificationPrivateEndpointsDesc": "Точные частные хосты, с которыми могут связываться каналы уведомлений. Разделяйте их запятыми.", "updateNotificationEndpointsFailed": "Не удалось обновить список разрешенных конечных точек уведомлений." diff --git a/src/ui/locales/translated/sr_SP.json b/src/ui/locales/translated/sr_SP.json index 5f117bdcb..0b52a4f6c 100644 --- a/src/ui/locales/translated/sr_SP.json +++ b/src/ui/locales/translated/sr_SP.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Лозинка је сачувана, унесите текст да бисте је променили", "replaceKey": "Замените кључ", "docsLink": "Прикажи документе", - "stepcaLabel": "Корак CA", - "stepcaDesc": "Пријавите се преко свог добављача идентитета и дозволите свом Step CA да изда краткотрајни SSH сертификат за овај хост. CA URL, root отисак прста и OIDC provisioner подешава администратор у подешавањима администратора.", "agentLabel": "SSH агент", "agentDesc": "Аутентификујте се помоћу SSH агента који ради на Termix серверу (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). Агент мора бити покренут на машини на којој се налази Termix.", "agentAuthenticationDesc": "Користите кључеве од SSH агента за аутентификацију ове везе. Оставите путању сокета празну да бисте користили SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "Сачувај", "saving": "Чување..." }, - "stepCa": "Корак CA", - "stepCaDesc": "Издајте краткотрајне SSH сертификате из smallstep CA. Унесите CA URL, његов коренски отисак прста (као што је приказано у step ca bootstrap) и име OIDC провизора; затим изаберите „Step CA“ као тип аутентификације хоста. Оставите сва три празна да бисте онемогућили.", - "stepCaFingerprint": "Отисак коренског идентификатора (SHA-256)", - "stepCaProvisioner": "Име OIDC провизора", - "stepCaSaved": "Подешавања CA корака су сачувана", - "stepCaSaveFailed": "Чување подешавања корака CA није успело", "secretSourcePrivateEndpoints": "Дозвољени приватни тајни изворни хостови", "secretSourcePrivateEndpointsDesc": "Приватни хостови које тајни извори (1Password Connect) могу контактирати. Одвојите их зарезима.", "updateSecretSourceEndpointsFailed": "Ажурирање листе дозвољених крајњих тачака тајног извора није успело", - "stepCaPrivateEndpoints": "Дозвољени приватни хостови Step CA", - "stepCaPrivateEndpointsDesc": "Приватни хостови које ток сертификата Step CA може контактирати: сам CA и, ако је интерно, ваш добављач идентитета. Раздвојите их зарезима.", - "updateStepCaEndpointsFailed": "Није успело ажурирање листе дозвољених крајњих тачака Step CA", "notificationPrivateEndpoints": "Дозвољени хостови за приватна обавештења", "notificationPrivateEndpointsDesc": "Тачни приватни хостови које канали за обавештења могу да контактирају. Одвојите их зарезима.", "updateNotificationEndpointsFailed": "Ажурирање листе дозвољених крајњих тачака обавештења није успело" diff --git a/src/ui/locales/translated/sv_SE.json b/src/ui/locales/translated/sv_SE.json index 3ad273a46..d37a13bcd 100644 --- a/src/ui/locales/translated/sv_SE.json +++ b/src/ui/locales/translated/sv_SE.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Lösenfrasen sparad, skriv för att ändra", "replaceKey": "Byt nyckel", "docsLink": "Visa dokument", - "stepcaLabel": "Steg CA", - "stepcaDesc": "Logga in via din identitetsleverantör och låt din Step CA utfärda ett kortlivat SSH-certifikat för den här värden. CA-URL:en, rotfingeravtrycket och OIDC-provisionern ställs in av en administratör under administratörsinställningar.", "agentLabel": "SSH-agent", "agentDesc": "Autentisera med en SSH-agent som körs på Termix-servern (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). Agenten måste köras på den maskin där Termix finns.", "agentAuthenticationDesc": "Använd nycklar från en SSH-agent för att autentisera den här anslutningen. Lämna socket-sökvägen tom om du vill använda SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "Spara", "saving": "Sparande..." }, - "stepCa": "Steg CA", - "stepCaDesc": "Utfärda kortlivade SSH-certifikat från en smallstep-CA. Ange CA-URL:en, dess rotfingeravtryck (som visas av steg-CA-bootstrap) och namnet på OIDC-provisioneraren; välj sedan \"Step CA\" som en värds autentiseringstyp. Lämna alla tre tomma för att inaktivera.", - "stepCaFingerprint": "Rotfingeravtryck (SHA-256)", - "stepCaProvisioner": "OIDC-provisioneringsnamn", - "stepCaSaved": "Steg CA-inställningar sparade", - "stepCaSaveFailed": "Kunde inte spara inställningarna för steg CA", "secretSourcePrivateEndpoints": "Tillåtna privata hemliga källvärdar", "secretSourcePrivateEndpointsDesc": "Privata värdar som hemliga källor (1Password Connect) kan kontakta. Separera dem med kommatecken.", "updateSecretSourceEndpointsFailed": "Misslyckades med att uppdatera tillåtelselistan för slutpunkten för hemlig källa", - "stepCaPrivateEndpoints": "Tillåtna privata Step CA-värdar", - "stepCaPrivateEndpointsDesc": "Privata värdar som Step CA-certifikatflödet kan kontakta: själva CA:n och, om den är intern, din identitetsleverantör. Separera dem med kommatecken.", - "updateStepCaEndpointsFailed": "Misslyckades med att uppdatera tillåtelselistan för Step CA-slutpunkten", "notificationPrivateEndpoints": "Tillåtna privata aviseringsvärdar", "notificationPrivateEndpointsDesc": "Exakta privata värdar som aviseringskanaler kan kontakta. Separera dem med kommatecken.", "updateNotificationEndpointsFailed": "Misslyckades med att uppdatera tillåtelselistan för aviseringsslutpunkten" diff --git a/src/ui/locales/translated/th_TH.json b/src/ui/locales/translated/th_TH.json index 17ed912e1..1838d2ee2 100644 --- a/src/ui/locales/translated/th_TH.json +++ b/src/ui/locales/translated/th_TH.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "บันทึกรหัสผ่านแล้ว พิมพ์เพื่อเปลี่ยน", "replaceKey": "เปลี่ยนกุญแจ", "docsLink": "ดูเอกสาร", - "stepcaLabel": "ขั้นตอน CA", - "stepcaDesc": "ลงชื่อเข้าใช้ผ่านผู้ให้บริการยืนยันตัวตนของคุณ และให้ Step CA ออกใบรับรอง SSH ที่มีอายุสั้นสำหรับโฮสต์นี้ URL ของ CA, ลายนิ้วมือรูท และตัวจัดเตรียม OIDC จะถูกตั้งค่าโดยผู้ดูแลระบบภายใต้การตั้งค่าผู้ดูแลระบบ", "agentLabel": "เอเจนต์ SSH", "agentDesc": "ยืนยันตัวตนโดยใช้เอเจนต์ SSH ที่ทำงานอยู่บนเซิร์ฟเวอร์ Termix (เช่น Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent) เอเจนต์จะต้องทำงานอยู่บนเครื่องที่ติดตั้ง Termix อยู่", "agentAuthenticationDesc": "ใช้คีย์จากเอเจนต์ SSH เพื่อตรวจสอบสิทธิ์การเชื่อมต่อนี้ หากต้องการใช้ SSH_AUTH_SOCK ให้เว้นช่องเส้นทางซ็อกเก็ตว่างไว้", @@ -2121,18 +2119,9 @@ "save": "บันทึก", "saving": "ประหยัด..." }, - "stepCa": "ขั้นตอน CA", - "stepCaDesc": "ออกใบรับรอง SSH ที่มีอายุสั้นจาก CA ของ smallstep ป้อน URL ของ CA ลายนิ้วมือรูท (ตามที่แสดงโดย step ca bootstrap) และชื่อของตัวจัดเตรียม OIDC จากนั้นเลือก \"Step CA\" เป็นประเภทการตรวจสอบสิทธิ์ของโฮสต์ เว้นว่างทั้งสามช่องเพื่อปิดใช้งาน", - "stepCaFingerprint": "ลายนิ้วมือราก (SHA-256)", - "stepCaProvisioner": "ชื่อผู้จัดเตรียม OIDC", - "stepCaSaved": "บันทึกการตั้งค่าขั้นตอน CA แล้ว", - "stepCaSaveFailed": "ไม่สามารถบันทึกการตั้งค่าขั้นตอน CA ได้", "secretSourcePrivateEndpoints": "อนุญาตให้โฮสต์แหล่งข้อมูลลับส่วนตัว", "secretSourcePrivateEndpointsDesc": "โฮสต์ส่วนตัวที่แหล่งข้อมูลลับ (1Password Connect) อาจติดต่อได้ คั่นด้วยเครื่องหมายจุลภาค", "updateSecretSourceEndpointsFailed": "ไม่สามารถอัปเดตรายการอนุญาตปลายทางแหล่งข้อมูลลับได้", - "stepCaPrivateEndpoints": "อนุญาตให้โฮสต์ Step CA ส่วนตัว", - "stepCaPrivateEndpointsDesc": "สำหรับโฮสต์ส่วนตัว กระบวนการขอใบรับรอง Step CA อาจติดต่อกับ: CA เอง และหากเป็นโฮสต์ภายใน ก็อาจติดต่อกับผู้ให้บริการยืนยันตัวตนของคุณด้วย คั่นด้วยเครื่องหมายจุลภาค", - "updateStepCaEndpointsFailed": "ไม่สามารถอัปเดตรายการอนุญาตปลายทาง Step CA ได้", "notificationPrivateEndpoints": "อนุญาตให้โฮสต์แจ้งเตือนส่วนตัว", "notificationPrivateEndpointsDesc": "ระบุโฮสต์ส่วนตัวที่ช่องทางการแจ้งเตือนสามารถติดต่อได้ คั่นด้วยเครื่องหมายจุลภาค", "updateNotificationEndpointsFailed": "ไม่สามารถอัปเดตรายการอนุญาตปลายทางการแจ้งเตือนได้" diff --git a/src/ui/locales/translated/tr_TR.json b/src/ui/locales/translated/tr_TR.json index c83547177..a8683eb43 100644 --- a/src/ui/locales/translated/tr_TR.json +++ b/src/ui/locales/translated/tr_TR.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Parola kaydedildi, değiştirmek için yazın", "replaceKey": "Anahtarı değiştir", "docsLink": "Dokümanları görüntüle", - "stepcaLabel": "Adım CA", - "stepcaDesc": "Kimlik sağlayıcınız aracılığıyla oturum açın ve Step CA'nızın bu sunucu için kısa süreli bir SSH sertifikası düzenlemesine izin verin. CA URL'si, kök parmak izi ve OIDC sağlayıcısı, yönetici tarafından Yönetici Ayarları altında belirlenir.", "agentLabel": "SSH Ajanı", "agentDesc": "Termix sunucusunda çalışan bir SSH ajanı kullanarak kimlik doğrulayın (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). Ajan, Termix'in barındırıldığı makinede çalışıyor olmalıdır.", "agentAuthenticationDesc": "Bu bağlantıyı doğrulamak için bir SSH aracısından gelen anahtarları kullanın. SSH_AUTH_SOCK kullanmak için soket yolunu boş bırakın.", @@ -2121,18 +2119,9 @@ "save": "Kaydet", "saving": "Kaydediliyor..." }, - "stepCa": "Adım CA", - "stepCaDesc": "Smallstep CA'dan kısa süreli SSH sertifikaları verin. CA URL'sini, kök parmak izini (step ca bootstrap tarafından gösterildiği gibi) ve OIDC sağlayıcısının adını girin; ardından ana bilgisayarın kimlik doğrulama türü olarak \"Step CA\"yı seçin. Devre dışı bırakmak için üçünü de boş bırakın.", - "stepCaFingerprint": "Kök parmak izi (SHA-256)", - "stepCaProvisioner": "OIDC tedarikçi adı", - "stepCaSaved": "Adım CA ayarları kaydedildi", - "stepCaSaveFailed": "Step CA ayarlarını kaydetme başarısız oldu.", "secretSourcePrivateEndpoints": "İzin verilen özel gizli kaynak sunucuları", "secretSourcePrivateEndpointsDesc": "Gizli kaynakların (1Password Connect) iletişim kurabileceği özel sunucular. Bunları virgülle ayırın.", "updateSecretSourceEndpointsFailed": "Gizli kaynak uç nokta izin listesini güncelleme başarısız oldu.", - "stepCaPrivateEndpoints": "Özel Step CA sunucularına izin verildi.", - "stepCaPrivateEndpointsDesc": "Özel sunucular, Step CA sertifika akışı kapsamında şu kuruluşlarla iletişime geçebilir: CA'nın kendisi ve (dahili ise) kimlik sağlayıcınız. Bunları virgülle ayırın.", - "updateStepCaEndpointsFailed": "Step CA uç nokta izin listesini güncelleme başarısız oldu.", "notificationPrivateEndpoints": "İzin verilen özel bildirim sunucuları", "notificationPrivateEndpointsDesc": "Bildirim kanallarının iletişim kurabileceği tam özel sunucuları belirtin. Bunları virgülle ayırın.", "updateNotificationEndpointsFailed": "Bildirim uç noktası izin listesini güncelleme başarısız oldu." diff --git a/src/ui/locales/translated/uk_UA.json b/src/ui/locales/translated/uk_UA.json index 0b89dc780..ec62a06c9 100644 --- a/src/ui/locales/translated/uk_UA.json +++ b/src/ui/locales/translated/uk_UA.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Пароль збережено, введіть, щоб змінити", "replaceKey": "Замінити ключ", "docsLink": "Переглянути документи", - "stepcaLabel": "Крок CA", - "stepcaDesc": "Увійдіть через свого постачальника ідентифікаційних даних і дозвольте вашому центру сертифікації Step CA видати короткочасний SSH-сертифікат для цього хоста. URL-адресу центру сертифікації, кореневий відбиток і OIDC-провайдер встановлює адміністратор у розділі «Налаштування адміністратора».", "agentLabel": "SSH-агент", "agentDesc": "Автентифікуйтеся за допомогою SSH-агента, що працює на сервері Termix (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). Агент має бути запущений на машині, де розміщено Termix.", "agentAuthenticationDesc": "Використовуйте ключі від SSH-агента для автентифікації цього з'єднання. Залиште шлях сокета порожнім, щоб використовувати SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "Зберегти", "saving": "Збереження..." }, - "stepCa": "Крок CA", - "stepCaDesc": "Видавати короткочасні SSH-сертифікати від центру сертифікації smallstep. Введіть URL-адресу центру сертифікації, його кореневий відбиток (як показано в step ca bootstrap) та ім'я постачальника OIDC; потім виберіть \"Step CA\" як тип автентифікації хоста. Залиште всі три поля порожніми, щоб вимкнути.", - "stepCaFingerprint": "Кореневий відбиток (SHA-256)", - "stepCaProvisioner": "Ім'я постачальника OIDC", - "stepCaSaved": "Налаштування кроку CA збережено", - "stepCaSaveFailed": "Не вдалося зберегти налаштування кроку CA", "secretSourcePrivateEndpoints": "Дозволені приватні секретні хости джерел", "secretSourcePrivateEndpointsDesc": "Приватні хости, з якими можуть зв’язуватися секретні джерела (1Password Connect). Розділіть їх комами.", "updateSecretSourceEndpointsFailed": "Не вдалося оновити список дозволених кінцевих точок секретного джерела", - "stepCaPrivateEndpoints": "Дозволені приватні хости Step CA", - "stepCaPrivateEndpointsDesc": "Приватні хости, з якими може зв’язатися потік сертифікатів Step CA: сам CA та, якщо внутрішній, ваш постачальник ідентифікаційних даних. Розділяйте їх комами.", - "updateStepCaEndpointsFailed": "Не вдалося оновити список дозволених кінцевих точок Step CA", "notificationPrivateEndpoints": "Дозволені хости приватних сповіщень", "notificationPrivateEndpointsDesc": "Точні приватні хости, з якими можуть зв’язуватися канали сповіщень. Розділіть їх комами.", "updateNotificationEndpointsFailed": "Не вдалося оновити список дозволених кінцевих точок сповіщень" diff --git a/src/ui/locales/translated/vi_VN.json b/src/ui/locales/translated/vi_VN.json index b03c2403e..7f365598c 100644 --- a/src/ui/locales/translated/vi_VN.json +++ b/src/ui/locales/translated/vi_VN.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "Đã lưu cụm mật khẩu, nhập để thay đổi", "replaceKey": "Thay khóa", "docsLink": "Xem tài liệu", - "stepcaLabel": "Bước CA", - "stepcaDesc": "Đăng nhập thông qua nhà cung cấp danh tính của bạn và cho phép Step CA cấp chứng chỉ SSH có thời hạn ngắn cho máy chủ này. URL CA, dấu vân tay gốc và trình cung cấp OIDC được quản trị viên thiết lập trong Cài đặt quản trị.", "agentLabel": "SSH Agent", "agentDesc": "Xác thực bằng SSH agent chạy trên máy chủ Termix (Bitwarden, 1Password, gpg-agent, KeeAgent, ssh-agent). Agent phải đang chạy trên máy chạy Termix.", "agentAuthenticationDesc": "Sử dụng khóa từ SSH agent để xác thực kết nối này. Để trống đường dẫn socket để sử dụng SSH_AUTH_SOCK.", @@ -2121,18 +2119,9 @@ "save": "Lưu", "saving": "Đang lưu..." }, - "stepCa": "Bước CA", - "stepCaDesc": "Cấp chứng chỉ SSH có thời hạn ngắn từ CA smallstep. Nhập URL của CA, dấu vân tay gốc của nó (như hiển thị bởi step ca bootstrap) và tên của trình cung cấp OIDC; sau đó chọn \"Step CA\" làm loại xác thực của máy chủ. Để trống cả ba trường để vô hiệu hóa.", - "stepCaFingerprint": "Dấu vân tay gốc (SHA-256)", - "stepCaProvisioner": "Tên trình cung cấp OIDC", - "stepCaSaved": "Cài đặt bước CA đã được lưu", - "stepCaSaveFailed": "Không thể lưu cài đặt Bước CA.", "secretSourcePrivateEndpoints": "Các máy chủ nguồn bí mật riêng tư được cho phép", "secretSourcePrivateEndpointsDesc": "Các máy chủ riêng tư mà các nguồn bí mật (1Password Connect) có thể liên hệ. Phân tách chúng bằng dấu phẩy.", "updateSecretSourceEndpointsFailed": "Không thể cập nhật danh sách cho phép điểm cuối nguồn bí mật.", - "stepCaPrivateEndpoints": "Cho phép máy chủ Step CA riêng tư", - "stepCaPrivateEndpointsDesc": "Các máy chủ riêng mà quy trình chứng chỉ CA từng bước có thể liên hệ: chính CA và, nếu là máy chủ nội bộ, nhà cung cấp danh tính của bạn. Phân tách chúng bằng dấu phẩy.", - "updateStepCaEndpointsFailed": "Không thể cập nhật danh sách cho phép điểm cuối Step CA.", "notificationPrivateEndpoints": "Cho phép máy chủ thông báo riêng tư", "notificationPrivateEndpointsDesc": "Liệt kê chính xác các máy chủ riêng mà kênh thông báo có thể liên hệ. Phân tách chúng bằng dấu phẩy.", "updateNotificationEndpointsFailed": "Không thể cập nhật danh sách cho phép điểm cuối thông báo." diff --git a/src/ui/locales/translated/zh_CN.json b/src/ui/locales/translated/zh_CN.json index 00ddd5647..780d678af 100644 --- a/src/ui/locales/translated/zh_CN.json +++ b/src/ui/locales/translated/zh_CN.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "密码短语已保存,输入以更改", "replaceKey": "替换密钥", "docsLink": "查看文档", - "stepcaLabel": "步骤 CA", - "stepcaDesc": "通过您的身份提供商登录,并让您的 Step CA 为此主机颁发一个短期 SSH 证书。CA URL、根指纹和 OIDC 配置器由管理员在“管理设置”中设置。", "agentLabel": "SSH 代理", "agentDesc": "使用运行在 Termix 服务器上的 SSH 代理进行认证(Bitwarden、1Password、gpg-agent、KeeAgent、ssh-agent)。代理必须在托管 Termix 的机器上运行。", "agentAuthenticationDesc": "使用 SSH 代理中的密钥来验证此连接。将套接字路径留空以使用 SSH_AUTH_SOCK。", @@ -2121,18 +2119,9 @@ "save": "保存", "saving": "保存中..." }, - "stepCa": "步骤 CA", - "stepCaDesc": "从小型 CA 颁发短期 SSH 证书。输入 CA URL、其根指纹(如 `step ca bootstrap` 命令所示)和 OIDC 配置器的名称;然后选择“Step CA”作为主机的身份验证类型。如果三项都留空,则禁用此功能。", - "stepCaFingerprint": "根指纹(SHA-256)", - "stepCaProvisioner": "OIDC配置器名称", - "stepCaSaved": "步骤 CA 设置已保存", - "stepCaSaveFailed": "保存步骤 CA 设置失败", "secretSourcePrivateEndpoints": "允许的私有秘密来源主机", "secretSourcePrivateEndpointsDesc": "秘密来源(例如 1Password Connect)可能联系的私有主机。请用逗号分隔。", "updateSecretSourceEndpointsFailed": "更新秘密源端点允许列表失败", - "stepCaPrivateEndpoints": "允许的私有步骤 CA 主机", - "stepCaPrivateEndpointsDesc": "私有主机在执行步骤 CA 证书流程时可能会联系:CA 本身,以及(如果是内部)您的身份提供商。请用逗号分隔它们。", - "updateStepCaEndpointsFailed": "更新 Step CA 端点允许列表失败", "notificationPrivateEndpoints": "允许的私人通知主机", "notificationPrivateEndpointsDesc": "通知渠道可以联系的具体私有主机。请用逗号分隔。", "updateNotificationEndpointsFailed": "更新通知端点允许列表失败" diff --git a/src/ui/locales/translated/zh_TW.json b/src/ui/locales/translated/zh_TW.json index 9888220b5..5de1dd566 100644 --- a/src/ui/locales/translated/zh_TW.json +++ b/src/ui/locales/translated/zh_TW.json @@ -771,8 +771,6 @@ "keyPassphraseSaved": "密碼片語已儲存,輸入以變更", "replaceKey": "取代金鑰", "docsLink": "檢視文件", - "stepcaLabel": "步驟 CA", - "stepcaDesc": "透過您的身分提供者登錄,並讓您的 Step CA 為此主機頒發短期 SSH 憑證。 CA URL、根指紋和 OIDC 配置器由管理員在「管理設定」中設定。", "agentLabel": "SSH 代理", "agentDesc": "使用在 Termix 伺服器上執行的 SSH 代理進行驗證(Bitwarden、1Password、gpg-agent、KeeAgent、ssh-agent)。代理必須在託管 Termix 的機器上執行。", "agentAuthenticationDesc": "使用 SSH 代理程式中的金鑰來驗證此連線。將套接字路徑留空以使用 SSH_AUTH_SOCK。", @@ -2121,18 +2119,9 @@ "save": "儲存", "saving": "儲存中..." }, - "stepCa": "步驟 CA", - "stepCaDesc": "從小型 CA 頒發短期 SSH 憑證。輸入 CA URL、其根指紋(如 `step ca bootstrap` 指令所示)和 OIDC 設定器的名稱;然後選擇「Step CA」作為主機的驗證類型。如果三項都留空,則停用此功能。", - "stepCaFingerprint": "根指紋(SHA-256)", - "stepCaProvisioner": "OIDC配置器名稱", - "stepCaSaved": "步驟 CA 設定已儲存", - "stepCaSaveFailed": "儲存步驟 CA 設定失敗", "secretSourcePrivateEndpoints": "允許的私有秘密來源主機", "secretSourcePrivateEndpointsDesc": "秘密來源(例如 1Password Connect)可能聯絡的私有主機。請用逗號分隔。", "updateSecretSourceEndpointsFailed": "更新秘密來源端點允許清單失敗", - "stepCaPrivateEndpoints": "允許的私有步驟 CA 主機", - "stepCaPrivateEndpointsDesc": "私有主機在執行步驟 CA 憑證流程時可能會聯絡:CA 本身,以及(如果是內部)您的身分提供者。請用逗號分隔它們。", - "updateStepCaEndpointsFailed": "更新 Step CA 端點允許清單失敗", "notificationPrivateEndpoints": "允許的私人通知主機", "notificationPrivateEndpointsDesc": "通知管道可以聯繫的具體私有主機。請用逗號分隔。", "updateNotificationEndpointsFailed": "更新通知端點允許清單失敗" diff --git a/src/ui/plugin-host/auth-registry.ts b/src/ui/plugin-host/auth-registry.ts index 558c2e29d..4280d0678 100644 --- a/src/ui/plugin-host/auth-registry.ts +++ b/src/ui/plugin-host/auth-registry.ts @@ -12,8 +12,6 @@ import { createRegistry } from "@/lib/registry"; * type is picked. Login methods render on the login screen for methods the * server reports as enabled; second factors render in the step after a first * login. Either kind may bring an enrolment section for Settings > Security. - * Core's own (OIDC, LDAP) register from src/ui/auth/legacy-auth-ui.tsx until - * they move into plugins. */ export interface SshAuthEditorDef { /** The authType value stored on the host. */ diff --git a/src/ui/sidebar/AdminSettingsPanel.tsx b/src/ui/sidebar/AdminSettingsPanel.tsx index ddf6c7008..613ff35c5 100644 --- a/src/ui/sidebar/AdminSettingsPanel.tsx +++ b/src/ui/sidebar/AdminSettingsPanel.tsx @@ -3,8 +3,6 @@ import { useTranslation } from "react-i18next"; import { useBranding } from "@/contexts/BrandingContext"; import { getNotificationPrivateEndpoints, - getStepCaPrivateEndpoints, - setStepCaPrivateEndpoints as setStepCaPrivateEndpointsApi, setNotificationPrivateEndpoints as setNotificationPrivateEndpointsApi, } from "@/api/private-endpoints-api"; import { @@ -73,7 +71,6 @@ import { type AdminUser, } from "./AdminManagementSections"; import { toast } from "sonner"; -import { getStepCaSettings, updateStepCaSettings } from "@/api/settings-api"; import { getDatabaseTransferUrl } from "@/lib/database-transfer-url"; import { AdminDatabaseSection, @@ -129,25 +126,6 @@ export function AdminSettingsPanel({ const [analyticsLocked, setAnalyticsLocked] = useState(false); const [oidcSilentLoginDefaultLocked, setOidcSilentLoginDefaultLocked] = useState(false); - const [stepCaPrivateEndpoints, setStepCaPrivateEndpoints] = useState< - string[] - >([]); - const [stepCaSettings, setStepCaSettings] = useState({ - caUrl: "", - fingerprint: "", - provisioner: "", - }); - useEffect(() => { - getStepCaSettings() - .then((s) => - setStepCaSettings({ - caUrl: s.caUrl, - fingerprint: s.fingerprint, - provisioner: s.provisioner, - }), - ) - .catch(() => {}); - }, []); const [notificationPrivateEndpoints, setNotificationPrivateEndpoints] = useState([]); const [hostDefaults, setHostDefaults] = useState({}); @@ -333,7 +311,6 @@ export function AdminSettingsPanel({ oidcSilent, analytics, notificationEndpoints, - stepCaEndpoints, branding, ] = await Promise.allSettled([ getRegistrationAllowed(), @@ -347,7 +324,6 @@ export function AdminSettingsPanel({ getOidcSilentLoginDefault(), getAnalyticsEnabled(), getNotificationPrivateEndpoints(), - getStepCaPrivateEndpoints(), getBranding(), ]); @@ -379,9 +355,6 @@ export function AdminSettingsPanel({ setAnalyticsEnabled(analytics.value.enabled); setAnalyticsLocked(analytics.value.locked ?? false); } - if (stepCaEndpoints.status === "fulfilled") { - setStepCaPrivateEndpoints(stepCaEndpoints.value); - } if (notificationEndpoints.status === "fulfilled") { setNotificationPrivateEndpoints(notificationEndpoints.value); } @@ -499,28 +472,6 @@ export function AdminSettingsPanel({ } } - async function handleSaveStepCaSettings() { - try { - await updateStepCaSettings(stepCaSettings); - toast.success(t("admin.stepCaSaved")); - } catch (error) { - toast.error( - error instanceof Error ? error.message : t("admin.stepCaSaveFailed"), - ); - } - } - - async function handleSaveStepCaPrivateEndpoints(hosts: string[]) { - const previous = stepCaPrivateEndpoints; - setStepCaPrivateEndpoints(hosts); - try { - setStepCaPrivateEndpoints(await setStepCaPrivateEndpointsApi(hosts)); - } catch { - setStepCaPrivateEndpoints(previous); - toast.error(t("admin.updateStepCaEndpointsFailed")); - } - } - async function handleSaveNotificationPrivateEndpoints(hosts: string[]) { const previous = notificationPrivateEndpoints; setNotificationPrivateEndpoints(hosts); @@ -938,11 +889,6 @@ export function AdminSettingsPanel({ analyticsLocked={analyticsLocked} handleToggleAnalytics={handleToggleAnalytics} notificationPrivateEndpoints={notificationPrivateEndpoints} - stepCaPrivateEndpoints={stepCaPrivateEndpoints} - onSaveStepCaPrivateEndpoints={handleSaveStepCaPrivateEndpoints} - stepCaSettings={stepCaSettings} - setStepCaSettings={setStepCaSettings} - handleSaveStepCaSettings={handleSaveStepCaSettings} onSaveNotificationPrivateEndpoints={ handleSaveNotificationPrivateEndpoints } diff --git a/src/ui/sidebar/AdminSettingsSections.tsx b/src/ui/sidebar/AdminSettingsSections.tsx index f46db09ab..d85f4309b 100644 --- a/src/ui/sidebar/AdminSettingsSections.tsx +++ b/src/ui/sidebar/AdminSettingsSections.tsx @@ -18,13 +18,6 @@ type GeneralSettingsSectionProps = { handleToggleAnalytics: () => void; notificationPrivateEndpoints: string[]; onSaveNotificationPrivateEndpoints: (hosts: string[]) => void; - stepCaPrivateEndpoints: string[]; - onSaveStepCaPrivateEndpoints: (hosts: string[]) => void; - stepCaSettings: { caUrl: string; fingerprint: string; provisioner: string }; - setStepCaSettings: Dispatch< - SetStateAction<{ caUrl: string; fingerprint: string; provisioner: string }> - >; - handleSaveStepCaSettings: () => void; allowRegistration: boolean; handleToggleRegistration: () => void; allowPasswordLogin: boolean; @@ -57,11 +50,6 @@ export function AdminGeneralSettingsSection({ handleToggleAnalytics, notificationPrivateEndpoints, onSaveNotificationPrivateEndpoints, - stepCaPrivateEndpoints, - onSaveStepCaPrivateEndpoints, - stepCaSettings, - setStepCaSettings, - handleSaveStepCaSettings, allowRegistration, handleToggleRegistration, allowPasswordLogin, @@ -130,73 +118,6 @@ export function AdminGeneralSettingsSection({ } />
-
- - {t("admin.stepCaPrivateEndpoints")} - - - {t("admin.stepCaPrivateEndpointsDesc")} - - - onSaveStepCaPrivateEndpoints( - event.target.value - .split(",") - .map((entry) => entry.trim()) - .filter(Boolean), - ) - } - /> -
- -
- - {t("admin.stepCa")} - - - {t("admin.stepCaDesc")} - - - setStepCaSettings((p) => ({ ...p, caUrl: e.target.value })) - } - /> - - setStepCaSettings((p) => ({ ...p, fingerprint: e.target.value })) - } - /> -
- - setStepCaSettings((p) => ({ - ...p, - provisioner: e.target.value, - })) - } - /> - -
-