
EdgeSSH
A browser SSH terminal and SFTP file manager on your own Cloudflare account
EdgeSSH is a self-hosted Termius alternative built on Cloudflare (D1, Durable Objects, Workers). Free tier eligible within limits. Inspect the source and license in the linked repository.
Source & license
Upstream license: Apache-2.0
License TL;DR
You can use, change and sell it, including in closed-source products. When sharing copies, include the license, keep required notices and mark changed files. It includes a contributor patent grant with conditions, but no trademark permission or warranty.
Explain Apache 2.0 in plain English →Summary of the main license. Separate packages and assets can have different terms.
Inspect repository ↗Read this project’s actual license ↗Repository owner
See the upstream repository for the original creator and contributors.
Maintain this project? Maintainer verification →Cloudflare hosting
Free tier eligible within limits
Designed for one administrator on the Workers Free plan: Workers, D1 and SQLite-backed Durable Objects all have free allowances. Long or many concurrent SSH sessions consume Durable Object duration and requests, and heavy use can exceed the daily free limits.
Hosting requirements
- Login is GitHub OAuth for one named account, or Cloudflare Access; Access is optional, not required.
- Each SSH session is a Durable Object holding a WebSocket and an outbound TCP socket; idle-but-open terminals still count towards Durable Object duration.
- The target servers, their network and any SSH keys remain the operator’s responsibility; EdgeSSH holds encrypted copies of the credentials it uses.
- Source and configuration review establishes a deployment path and conditional costs; this candidate was not executed or load-tested.
- Official Cloudflare pricing pages saved 31 Oct 2026 (Markdown editions of developers.cloudflare.com pricing documentation).
Review findings & limitations
- Login is GitHub OAuth for one named account, or Cloudflare Access; Access is optional, not required.
- Each SSH session is a Durable Object holding a WebSocket and an outbound TCP socket; idle-but-open terminals still count towards Durable Object duration.
- The target servers, their network and any SSH keys remain the operator’s responsibility; EdgeSSH holds encrypted copies of the credentials it uses.
- Competitor links describe the stated workflow overlap, not feature parity or a verified price saving.
- Source review at the pinned commit; deployment, workload CPU and application security have not been independently tested.
Sources checked 31/10/2026
Repository snapshot: 6c84546. Hosting eligibility reflects the deployment documentation and listed assumptions.
- termius ↗
**Cloudflare-native · Self-hosted · Single-admin · Open source**
- workers ↗
name = "edgessh" main = "src/worker.ts" compatibility_date = "2026-07-01" workers_dev = true preview_urls = false # 模板默认使用用户自己的 workers.dev 地址,避免绑定原作者的域名。 # Actions 可通过 CUSTOM_DOMAIN 配置自定义域名。 [build] command = "npm run build:web" [assets] directory = "./dist" binding = "ASSETS" not_found_handling = "single-page-application" run_worker_first = true [vars] CONNECT_TI
- d1 ↗
ngs]] name = "SSH_SESSIONS" class_name = "SSHSessionDO" [[migrations]] tag = "v1" new_sqlite_classes = ["SSHSessionDO"] [[d1_databases]] binding = "DB" database_name = "edgessh-accounts" # 部署脚本在目标账户中查找或创建数据库,不把账户专属 ID 留在模板里。 migrations_dir = "migrations"
- durable-objects ↗
TS" not_found_handling = "single-page-application" run_worker_first = true [vars] CONNECT_TIMEOUT_MS = "10000" [[durable_objects.bindings]] name = "SSH_SESSIONS" class_name = "SSHSessionDO" [[migrations]] tag = "v1" new_sqlite_classes = ["SSHSessionDO"] [[d1_databases]] binding = "DB" database_name = "edgessh-accounts" # 部署脚本在目标账户中查找或创建数据库,不把账户专属 ID 留在模板里。 migrations_dir = "migrations"
- free-tier-eligible ↗
| **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation |
- free-tier-eligible ↗
| Rows read | 5 million / day | First 25 billion / month included + $0.001 / million rows |
- free-tier-eligible ↗
| SQL Stored data <sup>5</sup> | 5 GB (total) | 5 GB-month, + $0.20/ GB-month |
- Apache-2.0 ↗
Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial owne
- architecture ↗
每一个 SSH 会话都由独立的 Durable Object 承载
- architecture ↗
主机资料与连接凭据会先用 **AES-256-GCM** 加密,再写入 D1。
- architecture ↗
name = "edgessh" main = "src/worker.ts" compatibility_date = "2026-07-01" workers_dev = true preview_urls = false # 模板默认使用用户自己的 workers.dev 地址,避免绑定原作者的域名。 # Actions 可通过 CUSTOM_DOMAIN 配置自定义域名。 [build] command = "npm run build:web" [assets] directory = "./dist" binding = "ASSETS" not_found_handling = "single-page-application" run_worker_first = true [vars] CONNECT_TI
- architecture ↗
ngs]] name = "SSH_SESSIONS" class_name = "SSHSessionDO" [[migrations]] tag = "v1" new_sqlite_classes = ["SSHSessionDO"] [[d1_databases]] binding = "DB" database_name = "edgessh-accounts" # 部署脚本在目标账户中查找或创建数据库,不把账户专属 ID 留在模板里。 migrations_dir = "migrations"
- architecture ↗
TS" not_found_handling = "single-page-application" run_worker_first = true [vars] CONNECT_TIMEOUT_MS = "10000" [[durable_objects.bindings]] name = "SSH_SESSIONS" class_name = "SSHSessionDO" [[migrations]] tag = "v1" new_sqlite_classes = ["SSHSessionDO"] [[d1_databases]] binding = "DB" database_name = "edgessh-accounts" # 部署脚本在目标账户中查找或创建数据库,不把账户专属 ID 留在模板里。 migrations_dir = "migrations"
Upstream screenshot · aozorae/EdgeSSH repository contributors ↗. Depicts the upstream project. We have not deployed and tested a fresh installation here.
What it can replace
Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.
Browser SSH sessions, SFTP file transfer and a saved, encrypted host library for one administrator; the servers themselves, team sharing and native desktop/mobile clients are not included
See supporting source ↗How it works
The shape of EdgeSSH on Cloudflare, and how it stacks up against the rented tools it replaces.
Architecture
Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.
View upstream source ↗Configuration and workflow sources
Reviewed commit 6c845463b347. This configuration evidence comes from reading source files. Execution checks, when available, appear in the project's runtime review.
Deployment configuration · 2 files
Cloudflare Workers · compatibility 2026-07-01
edgessh · default
Entrypoint: src/worker.ts
Build: npm run build:web
Static assets: ./dist · single-page-application · Worker first: true
DB→ D1SSH_SESSIONS→ Durable Objects · class SSHSessionDOASSETS→ Static assets
Cloudflare Workers · compatibility 2026-07-01
edgessh-preview · default
Entrypoint: src/preview-worker.ts
SSH_SESSIONS→ Durable Objects · class SSHSessionDO · Worker edgessh
Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.
Runtime source · handlers, binding usage and workflow steps
Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.
- L129 · fetch handler exported · references ASSETS · calls url.pathname.startsWith, isProductionHttp, corsResponse, jsonError, httpsRedirect, request.headers.get, includes, corsPreflightResponse, secureResponse, Response.json, authRoute, url.hostname.endsWith, locateHost, json, currentAccount, trustedForwardRoute, authProvider, hostsRoute, snippetsRoute, keychainRoute, forwardRulesRoute, forwardingRoute, sessionTicket, sshUpgrade, sftpUpgrade, processUpgrade, env.ASSETS.fetch, apiFailure
- L17 · clientAddress calls (conditional paths may differ): request.headers.get, test, value.toLowerCase
- L22 · sessionTicket calls (conditional paths may differ): request.headers.get, jsonError, Number, Number.isFinite, request.text, encode, JSON.parse, Array.isArray, Object.keys, env.SSH_SESSIONS.newUniqueId, env.SSH_SESSIONS.get, stub.fetch, clientAddress, response.json, secureResponse, Response.json, id.toString
- L46 · sshUpgrade calls (conditional paths may differ): request.headers.get, jsonError, hasValidWebSocketOrigin, url.searchParams.get, env.SSH_SESSIONS.idFromString, headers.set, headers.delete, crypto.randomUUID, sftpAttachUrl.searchParams.set, id.toString, processAttachUrl.searchParams.set, clientAddress, fetch, env.SSH_SESSIONS.get
- L78 · sftpUpgrade calls (conditional paths may differ): request.headers.get, jsonError, hasValidWebSocketOrigin, url.searchParams.get, env.SSH_SESSIONS.idFromString, headers.set, headers.delete, fetch, env.SSH_SESSIONS.get
- L103 · processUpgrade calls (conditional paths may differ): request.headers.get, jsonError, hasValidWebSocketOrigin, url.searchParams.get, env.SSH_SESSIONS.idFromString, headers.set, headers.delete, fetch, env.SSH_SESSIONS.get
Environment references: env.SSH_SESSIONS · env.ASSETS
- L10 · fetch handler exported · references PREVIEW_ORIGIN, SSH_SESSIONS · calls previewError, request.headers.has, crypto.randomUUID, request.headers.get, readBoundedText, parseCapability, JSON.parse, fetch, env.SSH_SESSIONS.get, env.SSH_SESSIONS.idFromString, response.json, Response.json, url.pathname.startsWith, readPreviewCookie, headers.forEach, headerNames.push, name.startsWith, headers.delete, headers.set
Environment references: env.PREVIEW_ORIGIN · env.SSH_SESSIONS
- L103 · normalizeTerminalSize calls (conditional paths may differ): Number.isFinite, Math.floor
- L111 · isValidHost calls (conditional paths may differ): host.startsWith, host.endsWith, host.slice, bare.includes, test, IPV4_RE.test, every, bare.split, Number, part.startsWith, HOST_RE.test
- L127 · parseConnectMessage calls (conditional paths may differ): Array.isArray, isValidHost, raw.host.trim, Number.isInteger, test, some, Object.keys, allowedFields.has, includes, raw.command.includes, normalizeTerminalSize, privateKey.includes, replace
- L1 · secureResponse calls (conditional paths may differ): headers.set, response.headers.get
- L14 · corsResponse calls (conditional paths may differ): headers.set
- L31 · corsPreflightResponse calls (conditional paths may differ): secureResponse
- L38 · jsonError calls (conditional paths may differ): secureResponse, Response.json
- L42 · isProductionHttp calls (conditional paths may differ): request.headers.has
- L46 · hasValidWebSocketOrigin calls (conditional paths may differ): request.headers.get
- L50 · httpsRedirect calls (conditional paths may differ): secureResponse, url.toString
- L12 · currentAccount calls (conditional paths may differ): authProvider, workspaceState, githubAccount, accessAccount
- L22 · accessAccount calls (conditional paths may differ): test, accessToken, resolvers.get, createRemoteJWKSet, resolvers.set, jwtVerify
Environment references: env.ACCESS_AUD · env.ACCESS_TEAM_DOMAIN
- L8 · authRoute calls (conditional paths may differ): includes, authProvider, json, currentAccount, workspaceState, revokeWorkspaceSessions, clearGithubCookies, githubLogin, githubCallback
- L32 · metadata calls (conditional paths may differ): Boolean
- L37 · refreshLocation calls (conditional paths may differ): locateHost, Date.now
- L48 · systemInfo calls (conditional paths may differ): includes, String, toLowerCase, text
- L60 · validate calls (conditional paths may differ): parseConnectMessage, text, includes, encoding.toLowerCase, trim
- L99 · hostsRoute calls (conditional paths may differ): exec, all, bind, env.DB.prepare, Promise.all, rows.results.map, decryptHost, Date.now, slice, records.filter, stale.map, refreshLocation, encryptHost, env.DB.batch, json, records.map, metadata, first, run, systemInfo
Environment references: env.DB · env.ENCRYPTION_KEY
- L8 · validateSnippet calls (conditional paths may differ): body.name.trim, body.command.trim, test, body.command.replace
- L19 · initializeLibrary calls (conditional paths may differ): first, bind, env.DB.prepare, Date.now, Promise.all, DEFAULT_SNIPPETS.map, crypto.randomUUID, encryptHost, env.DB.batch
- L37 · snippetsRoute calls (conditional paths may differ): exec, initializeLibrary, all, bind, env.DB.prepare, Promise.all, rows.results.map, decryptHost, json, run, validateSnippet, readJSON, crypto.randomUUID, encryptHost, Date.now
Environment references: env.DB · env.ENCRYPTION_KEY
- L10 · readKeychainKey calls (conditional paths may differ): first, bind, env.DB.prepare, decryptHost
- L23 · keychainRoute calls (conditional paths may differ): readJSON, Object.keys, generatePrivateKey, json, SSHAuth.publicKeyInfo, exec, all, bind, env.DB.prepare, Promise.all, rows.results.map, metadata, decryptHost, readKeychainKey, hosts.results.map, references.some, run, body.name.trim, test, privateKey.trim
Environment references: env.DB · env.ENCRYPTION_KEY · env.SSH_SESSIONS
- L14 · validateForwardRule calls (conditional paths may differ): name.trim, test, Number.isInteger
- L27 · forwardRulesRoute calls (conditional paths may differ): exec, all, bind, env.DB.prepare, Promise.all, rows.results.map, decryptHost, json, run, validateForwardRule, readJSON, first, crypto.randomUUID, encryptHost, Date.now
Environment references: env.DB · env.ENCRYPTION_KEY
- L8 · json calls (conditional paths may differ): responseHeaders.set, secureResponse, Response.json
- L14 · readJSON calls (conditional paths may differ): request.headers.get, request.body.getReader, reader.read, reader.cancel, chunks.push, bytes.set, JSON.parse, decode, Array.isArray
- L37 · apiFailure calls (conditional paths may differ): jsonError
- L13 · createLocation calls (conditional paths may differ): Number, Number.isFinite, Math.abs, toUpperCase, String, slice, test
- L25 · locateAddress calls (conditional paths may differ): fetch, encodeURIComponent, AbortSignal.timeout, response.json, createLocation
- L56 · locateHost calls (conditional paths may differ): isPrivateAddress, slice, map, filter, resolvePublicAddresses, address.toLowerCase, locateAddress
- L8 · forwardingRoute calls (conditional paths may differ): url.searchParams.get, test, jsonError, fetch, env.SSH_SESSIONS.get, env.SSH_SESSIONS.idFromString, previewOrigin, Response.json, request.headers.get, readBoundedText, JSON.parse, Number.isInteger, Number, JSON.stringify, headers.set
- L61 · trustedForwardRoute calls (conditional paths may differ): exec, jsonError, request.headers.has, headers.set, headers.delete, fetch, env.SSH_SESSIONS.get, env.SSH_SESSIONS.idFromString
Environment references: env.SSH_SESSIONS · env.PREVIEW_ORIGIN · env.APP_ORIGIN
- L9 · cookieSite calls (conditional paths may differ): join, slice, host.split, host.endsWith
- L13 · previewOrigin calls (conditional paths may differ): value.includes, preview.hostname.endsWith, app.hostname.endsWith, join, slice, preview.hostname.split, app.hostname.split, preview.hostname.includes, cookieSite
- L29 · previewError calls (conditional paths may differ): Response.json
- L37 · readPreviewCookie calls (conditional paths may differ): filter, map, split, headers.get, part.trim, part.startsWith, slice
- L43 · parseCapability calls (conditional paths may differ): exec
- L2 · readBoundedText calls (conditional paths may differ): request.body.getReader, reader.read, decoder.decode, catch, reader.cancel, reader.releaseLock
- L11 · toBase64Url calls (conditional paths may differ): String.fromCharCode, replace, btoa
- L17 · fromBase64Url calls (conditional paths may differ): test, replace, value.replace, repeat, atob, toBase64Url, Uint8Array.from, char.charCodeAt
- L25 · createTicket calls (conditional paths may differ): Date.now, crypto.randomUUID, toBase64Url, encoder.encode, JSON.stringify, crypto.subtle.importKey, crypto.subtle.sign
- L34 · verifyTicket calls (conditional paths may differ): ticket.split, crypto.subtle.importKey, crypto.subtle.verify, fromBase64Url, encoder.encode, JSON.parse, decoder.decode, Number.isFinite, Date.now, test
- L58 · isIPv4 calls (conditional paths may differ): test, every, host.split, Number, part.startsWith
- L63 · normalizeIPv6 calls (conditional paths may differ): toLowerCase, hostname.replace, host.toLowerCase
- L71 · isPrivateAddress calls (conditional paths may differ): replace, host.toLowerCase, value.endsWith, isIPv4, map, value.split, value.includes, normalizeIPv6, test, ipv6.startsWith, ipv6.slice, isPrivateAddress, mapped.split, halves.every, Number.parseInt
- L114 · toSocketHostname calls (conditional paths may differ): address.includes, address.startsWith
- L121 · resolveDnsType calls (conditional paths may differ): fetch, encodeURIComponent, AbortSignal.timeout, responseUrl.pathname.startsWith, response.headers.get, response.json, isIPv4, addresses.push, answer.data.includes, normalizeIPv6, normalized.includes
- L150 · resolvePublicAddresses calls (conditional paths may differ): host.includes, isIPv4, Promise.allSettled, types.map, resolveDnsType, addresses.push
- L165 · assertPublicTarget calls (conditional paths may differ): isPrivateAddress, resolvePublicAddresses, addresses.map, address.toLowerCase
- L1 · accessToken calls (conditional paths may differ): request.headers.get, part.trim, part.startsWith
- L23 · cookie calls (conditional paths may differ): request.headers.get, part.trim, part.startsWith
- L33 · signingKey calls (conditional paths may differ): crypto.subtle.importKey, encryptionKeyBytes, crypto.subtle.deriveBits, encoder.encode
- L47 · sign calls (conditional paths may differ): sign, setExpirationTime, setIssuedAt, setAudience, setIssuer, setProtectedHeader, audience, signingKey
- L53 · verify calls (conditional paths may differ): jwtVerify, signingKey, audience
- L60 · redirect calls (conditional paths may differ): headers.append, secureResponse
- L66 · githubLogin calls (conditional paths may differ): config, base64url.encode, crypto.getRandomValues, crypto.subtle.digest, encoder.encode, toString, sign, redirect, url.toString, setCookie
- L82 · githubCallback calls (conditional paths may differ): config, cookie, url.searchParams.get, url.searchParams.has, verify, fetcher, AbortSignal.timeout, tokenResponse.json, userResponse.json, Number.isSafeInteger, String, workspaceState, sign, redirect, setCookie, apiFailure, response.headers.append
- L133 · githubAccount calls (conditional paths may differ): config, cookie, verify
- L147 · clearGithubCookies calls (conditional paths may differ): response.headers.append, setCookie
Environment references: env.APP_ORIGIN · env.GH_CLIENT_ID · env.GH_CLIENT_SECRET · env.GH_ADMIN_ID · env.ENCRYPTION_KEY
- L18 · workspaceState calls (conditional paths may differ): first, env.DB.prepare
- L33 · revokeWorkspaceSessions calls (conditional paths may differ): run, bind, env.DB.prepare, Math.floor, Date.now
Environment references: env.DB
- L3 · base64 calls (conditional paths may differ): btoa, String.fromCharCode
- L7 · unbase64 calls (conditional paths may differ): Uint8Array.from, atob, c.charCodeAt
- L11 · digest calls (conditional paths may differ): base64, crypto.subtle.digest, encoder.encode
- L15 · encryptionKeyBytes calls (conditional paths may differ): unbase64
- L21 · encryptionKey calls (conditional paths may differ): crypto.subtle.importKey, encryptionKeyBytes
- L25 · encryptHost calls (conditional paths may differ): crypto.getRandomValues, crypto.subtle.encrypt, encoder.encode, encryptionKey, JSON.stringify, String.fromCharCode, base64, btoa
- L37 · decryptHost calls (conditional paths may differ): value.split, crypto.subtle.decrypt, unbase64, encoder.encode, encryptionKey, JSON.parse, decode
- L15 · osReleaseValue calls (conditional paths may differ): raw.trim, includes, value.at, value.slice, unquoted.replace
- L22 · clean calls (conditional paths may differ): slice, trim, replace
- L26 · parseSystemProbe calls (conditional paths may differ): output.split, line.startsWith, clean, line.slice, exec, values.set, osReleaseValue, kernel.toLowerCase, test, values.has, toLowerCase, values.get
- L3 · generatePrivateKey calls (conditional paths may differ): crypto.subtle.generateKey, crypto.subtle.exportKey, Uint8Array.from, atob, replace, char.charCodeAt, concat, encodeString, crypto.getRandomValues, encode, encodeUint32, seed.fill, btoa, String.fromCharCode, join, base64.match
- L5 · cookiePrefix calls (conditional paths may differ): pop, base.split
- L8 · mountURL calls (conditional paths may differ): value.startsWith, path.startsWith, test, includes, Number, url.pathname.startsWith
- L27 · mountCSS calls (conditional paths may differ): css.replace, mountURL
- L32 · mountSrcset calls (conditional paths may differ): test, value.slice, token.replace, mountURL, token.slice
- L54 · upstreamCookies calls (conditional paths may differ): cookiePrefix, join, map, filter, value.split, part.trim, part.startsWith, part.includes, part.slice
- L60 · mountCookie calls (conditional paths may differ): value.split, indexOf, trim, slice, pathname.slice, pathname.lastIndexOf, parts.slice, part.trim, test, trimmed.slice, Number, Number.isFinite, Math.floor, Date.parse, attrs.push, path.startsWith, Math.max, Math.min, Date.now, cookiePrefix
- L4 · isSSH2Identification calls (conditional paths may differ): value.startsWith
- L49 · parseKEXInit calls (conditional paths may differ): decode, data.subarray, name.includes, some, name.split, test, lists.push, split
- L94 · negotiate calls (conditional paths may differ): serverList.includes, clientList.join, serverList.join
- L100 · parseServerSigAlgs calls (conditional paths may differ): decode, payload.subarray, filter, value.split, test
- L141 · filterExtInfo calls (conditional paths may differ): list.filter, a.startsWith
- L3 · buildMacData calls (conditional paths may differ): writeUint32, data.set
- L29 · writeBytes calls (conditional paths may differ): target.set
- L8 · concat calls (conditional paths may differ): result.set
- L39 · encodeUint32 calls (conditional paths may differ): writeUint32
- L45 · encodeString calls (conditional paths may differ): textEncoder.encode, writeUint32, result.set
- L55 · toSSHMPInt calls (conditional paths may differ): bytes.subarray, writeUint32, result.set
- L72 · x25519SecretToSSHMPInt calls (conditional paths may differ): toSSHMPInt
- L57 · finiteNumber calls (conditional paths may differ): Number, replace, value.replace, Number.isFinite
- L64 · parseDecimal calls (conditional paths may differ): NUMBER_TOKEN_RE.test, test, Number, token.replace
- L72 · clampPercent calls (conditional paths may differ): Math.min, Math.max
- L76 · parseLoad calls (conditional paths may differ): line.match, match, map, tokens.slice, values.every
- L90 · parseCPU calls (conditional paths may differ): test, line.match, parseDecimal, clampPercent
- L109 · unitMultiplier calls (conditional paths may differ): unit.startsWith
- L119 · parseSizeToken calls (conditional paths may differ): token.match, parseDecimal, toLowerCase, indexOf, Math.round
- L129 · parseUsage calls (conditional paths may differ): test, parseMacOSPhysMem, parseLabelledUsage, parseFreeBSDMemUsage
- L153 · parseLabelledUsage calls (conditional paths may differ): line.match, unitMultiplier, parseDecimal, amount, Math.round, Math.min, clampPercent
- L183 · parseFreeBSDMemUsage calls (conditional paths may differ): tokenRegex.exec, parseSizeToken, toLowerCase, memLabels.has, entries.push, entries.find, Math.round, entries.reduce, Math.max, clampPercent
- L211 · parseMacOSPhysMem calls (conditional paths may differ): line.match, parseSizeToken, clampPercent
- L231 · headerIndex calls (conditional paths may differ): names.map, n.toUpperCase, headers.findIndex, upperNames.includes, header.toUpperCase
- L236 · parseMemoryValue calls (conditional paths may differ): test, value.match, finiteNumber, indexOf, Math.round
- L249 · parseNonNegativeInteger calls (conditional paths may differ): test, Number, Number.isSafeInteger
- L265 · parseNetwork calls (conditional paths may differ): replace, raw.replace, cleaned.indexOf, trim, cleaned.slice, section.split, line.trim, entry.split, parseNonNegativeInteger, samples.push
- L290 · parseProcesses calls (conditional paths may differ): lines.findIndex, test, split, trim, headerIndex, filter, Math.max, lines.slice, line.trim, Number, Number.isSafeInteger, processes.push, parseMemoryValue, finiteNumber, join, fields.slice
- L336 · parseTopSnapshot calls (conditional paths may differ): Date.now, replace, raw.replace, cleaned.split, parseProcesses, parseNetwork, parseCPU, parseLoad, parseUsage
- L1 · retainTrailingMarkerPrefix calls (conditional paths may differ): Math.min, Math.max, buffer.endsWith, marker.slice, buffer.slice
- L27 · sha512 calls (conditional paths may differ): crypto.subtle.digest
- L498 · bcrypt_hash calls (conditional paths may differ): state.expandstate, state.expand0state, stream2word, state.enc
- L524 · bcrypt_pbkdf calls (conditional paths may differ): Math.floor, sha2pass.set, sha512, pass.subarray, sha2salt.set, bcrypt_hash, Math.min
- L586 · bcryptPbkdf calls (conditional paths may differ): Number.isInteger, encode, bcrypt_pbkdf
- L45 · checkInt calls (conditional paths may differ): parseInt
- L49 · checkInts calls (conditional paths may differ): checkInt
- L61 · coerceArray calls (conditional paths may differ): arg.slice, Array.prototype.slice.call, Array.isArray, checkInts, checkInt
- L98 · copyArray calls (conditional paths may differ): sourceArray.slice, Array.prototype.slice.call, targetArray.set
- L160 · toBytes calls (conditional paths may differ): result.push, parseInt, text.substr
- L172 · fromBytes calls (conditional paths may differ): result.push, result.join
- L216 · convertToInt32 calls (conditional paths may differ): result.push
- L754 · pkcs7pad calls (conditional paths may differ): coerceArray, createArray, copyArray
- L765 · pkcs7strip calls (conditional paths may differ): coerceArray, createArray, copyArray
- L22 · concat calls (conditional paths may differ): arrays.reduce, result.set
- L32 · encodeString calls (conditional paths may differ): encode, setUint32, concat
- L39 · readUint32 calls (conditional paths may differ): getUint32
- L44 · readString calls (conditional paths may differ): readUint32, decode, data.subarray
- L58 · buildPasswordAuthRequest calls (conditional paths may differ): concat, encodeString
- L69 · buildKeyboardInteractiveAuthRequest calls (conditional paths may differ): concat, encodeString
- L80 · parseKeyboardInteractiveChallenge calls (conditional paths may differ): readString, readUint32, prompts.push
- L103 · passwordResponsesForChallenge calls (conditional paths may differ): isSupportedPasswordPrompt
- L115 · buildKeyboardInteractiveResponse calls (conditional paths may differ): setUint32, concat, responses.map
- L125 · isSupportedPasswordPrompt calls (conditional paths may differ): challenge.name.trim, challenge.instruction.trim, test, prompt.trim, exec, toLowerCase, identity.host.toLowerCase
- L12 · stripHopHeaders calls (conditional paths may differ): split, headers.get, name.trim, headers.delete
- L17 · isInternalCookie calls (conditional paths may differ): name.toLowerCase, PREVIEW_COOKIE.toLowerCase, test
- L22 · upstreamHeaders calls (conditional paths may differ): stripHopHeaders, headers.forEach, headerNames.push, test, includes, headers.delete, headers.set, upstreamCookies, headers.get, trim, join, filter, split, part.indexOf, isInternalCookie, part.slice, referer.replace
- L52 · rewriteLocalURL calls (conditional paths may differ): mountURL
- L56 · previewHeaders calls (conditional paths may differ): source.getSetCookie, stripHopHeaders, headers.delete, trim, cookie.slice, cookie.indexOf, isInternalCookie, mountCookie, headers.append, cookie.split, split, filter, parts.slice, test, join, headers.get, headers.set, mountURL
- L145 · sendRequest calls (conditional paths may differ): upstreamHeaders, headers.get, test, Number, headers.set, headers.forEach, channel.write, encoder.encode, request.body.getReader, reader.read, value.length.toString, catch, reader.cancel, reader.releaseLock
- L173 · proxyHTTP calls (conditional paths may differ): request.headers.get, path.startsWith, test, channel.close, channel.readable.getReader, channel.abort, request.signal.addEventListener, request.signal.removeEventListener, catch, reader.cancel, reader.releaseLock, sendRequest, sending.catch, parser.headers, previewHeaders, includes, finish, source.get, transfer.toLowerCase, Number.isSafeInteger
- L4 · pathRuntime calls (conditional paths may differ): JSON.stringify, cookiePrefix
- L145 · uint32 calls (conditional paths may differ): Number.isInteger
- L152 · positiveInteger calls (conditional paths may differ): Number.isSafeInteger
- L157 · toUint64 calls (conditional paths may differ): Number.isSafeInteger, BigInt, test
- L172 · displayUint64 calls (conditional paths may differ): BigInt, Number, value.toString
- L176 · encodeUint32 calls (conditional paths may differ): setUint32, uint32
- L182 · encodeUint64 calls (conditional paths may differ): toUint64, view.setUint32, Number
- L191 · encodeString calls (conditional paths may differ): encoder.encode, setUint32, result.set
- L200 · join calls (conditional paths may differ): Number.isSafeInteger, result.set
- L215 · encodeAttrs calls (conditional paths may differ): encodeUint32, values.push, encodeUint64, encodeString, join
- L254 · parseAttrs calls (conditional paths may differ): reader.readUint32, flags.toString, displayUint64, reader.readUint64, Math.floor, extended.push, reader.readText, slice, reader.readStringBytes
- L294 · parseStatus calls (conditional paths may differ): reader.readUint32, reader.readText, reader.assertEnd
- L164 · formatSFTPPermissions calls (conditional paths may differ): getSFTPFileKind, bits.join
Build and deployment pipeline · 3 GitHub Actions workflows
Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.
Triggers: workflow_dispatch
deploy · no job dependencies declared
Uses: ./.github/workflows/deploy.yml
Triggers: push, workflow_dispatch, workflow_call
Deploy to Cloudflare · no job dependencies declared
- Check out repository
actions/checkout@v4 - Set up Node.js
actions/setup-node@v4 - Install dependencies
npm ci - Validate deployment settings
npm run deploy:validate - Check project
npm run check - Check file manager in desktop and mobile browsers
npx playwright install --with-deps chromium npm run test:browser -- --workers=2 - Configure selected login, provision D1, migrate and deploy Worker
npm run deploy
Triggers: schedule, workflow_dispatch
Check official release markers · no job dependencies declared
- Check out user repository
actions/checkout@v4 - Find marked commit and update main
git remote add official "https://github.com/${OFFICIAL_REPOSITORY}.git" git fetch --no-tags official "${OFFICIAL_BRANCH}" current_sha="$(git rev-parse refs/remotes/origin/main)" official_sha="$(git rev-parse "refs/remotes/official/${OFFICIAL_BRANCH}")" target_sha="" # rev-list 按拓扑从新到旧扫描,只接受完整 Git trailer,防止正文中的同名文字误触发更新。 while read -r sha; do marker="$(git show -s --format='%(trailers:key=EdgeSSH-Auto-Update,valueonly,unfold)' "${sha}")" if [[ "${marker}" == "true" ]]; then target_sha="${sha}" …
Deploy marked commit · after update
Condition: needs.update.outputs.updated == 'true'
Uses: ./.github/workflows/deploy.yml
build:web: vite build --config vite.config.tsdeploy:validate: node --experimental-strip-types scripts/deploy.ts --validate-onlydeploy: node --experimental-strip-types scripts/deploy.ts
Repository README
View original on GitHub ↗Full upstream document by @aozorae · README.md · snapshot 6c84546
EdgeSSH · 开源 WebSSH 工作台
浏览器里的 SSH 终端与 SFTP 文件管理,运行在你自己的 Cloudflare 上。
把主机管理、在线终端、文件传输、进程监控与远程 HTTP 服务预览,收进一个个人服务器管理工作台,无需额外维护 SSH 中转服务器。
An open-source, self-hosted WebSSH client on Cloudflare Workers — browser-based SSH terminal, SFTP file manager, and server monitoring.
Cloudflare-native · Self-hosted · Single-admin · Open source
快速部署 · 功能特性 · 安全设计 · 本地开发 · 文档站 →

全部演示数据使用文档保留地址与虚构信息,不对应任何真实服务器。
你的服务器 · 你的凭据 · 你的 Cloudflare · 你的 SSH 工作台
快速导航 · 功能、部署与技术说明
CLI / Agent 调用
本地 Agent 可以通过 CLI 使用现有主机、SSH 和文件功能,使用 Cloudflare Access 浏览器登录并复用令牌。需要先部署本次代码并安装 Node.js 22.12+ 与 cloudflared。
npm --prefix cli ci
npm install --global ./cli
edgessh login --url https://ssh.example.com
edgessh hosts
edgessh exec "主机名" --json -- 'uname -a'
edgessh get "主机名" /var/log/app.log ./app.log
支持交互终端、目录查看、上传下载、创建目录、重命名和删除。命令执行与文件操作按需连接,完成即断开,设有超时和输出上限。安装、指纹确认、Agent 用法和资源消耗说明见 CLI 文档。
为什么是 EdgeSSH?
SSH 客户端不少,WebSSH 也算不上新鲜。
EdgeSSH 想解决的是另一件事:
能不能不再维护一套 SSH 中转服务器,却依然拥有一个随时能从浏览器打开的个人 SSH 工作台?
EdgeSSH 把 WebSSH 的连接层、主机管理与工作台界面,一起部署在 Cloudflare 上。
浏览器只负责交互;应用逻辑与认证交给 Cloudflare Worker;每一个 SSH 会话都由独立的 Durable Object 承载,再通过 Cloudflare TCP Sockets 连到你的服务器。
☁️ Cloudflare Native跑在 Cloudflare Workers 上。 不用再单独维护一台 WebSSH 中转 VPS。 | 🧰 SSH Workspace不只是一个 Terminal。 主机、文件、进程、代码片段和远程服务,都在同一个工作台里。 | 🔐 Self-hosted部署在你自己的 Cloudflare 账户里。 数据库、Worker 和配置,全部由你掌控。 | 👤 Single Admin面向个人服务器管理场景。 没有多租户、没有成员系统,也没有复杂的权限模型。 |
它不只是塞进浏览器里的一个 Terminal。
它是部署在 Cloudflare 上的个人 SSH 工作台。
功能特性
一个地方,管好你所有的服务器
EdgeSSH 把散落各处的服务器连接信息,收进统一的主机库。
在这里你可以:
- 保存、编辑服务器信息
- 搜索与整理主机
- 记录系统类型与服务器所在地
- 从 Dashboard 直接跳进 SSH
- 用密码、上传的 SSH 私钥或密钥链中的已有密钥认证
- 在不同设备间访问同一份工作区
主机资料与连接凭据会先用 AES-256-GCM 加密,再写入 D1。
密钥链
- 直接粘贴或上传未设口令的 OpenSSH Ed25519 / RSA / ECDSA 私钥;也可点击「生成」将 Ed25519 公私钥填入输入框,确认后保存
- 公钥可填写并校验,也可从私钥自动提取;支持复制公钥和按需下载私钥
- 私钥加密保存在 D1,列表不返回私钥;主机只保存密钥引用,连接时由 Worker 解密
- 同一密钥可用于多台主机,以及终端、文件管理和端口转发;仍被主机引用时不可删除
下载的私钥文件未设口令,请妥善保管。密钥链不会自动把公钥安装到目标服务器。
🖥️ SSH 在线终端 · Terminal
打开浏览器,就是进了服务器。

基于 xterm.js 的交互式终端,支持:
- SSH 2.0
- PTY
- 窗口尺寸同步
- Keepalive
- 全屏终端
- UTF-8 / GB18030 / Big5
- 密码认证
- keyboard-interactive
- OpenSSH 私钥认证

第一次连接某台服务器时,EdgeSSH 会先展示它的 SHA-256 Host Key Fingerprint。
只有你确认无误后,才会继续发送 SSH 凭据。
📁 SFTP 文件管理 · File Manager
不必为了改一个文件,再多开一个工具。

EdgeSSH 内置了一个基于 SFTP 的文件管理器:
- 浏览服务器目录
- 上传与下载
- 新建文件夹
- 重命名
- 删除文件
- 键盘导航
- 文件类型图标
- 在线编辑文本文件(代码高亮、Markdown 预览、自动保存)
- 从文件管理器直接跳回 SSH Terminal
选中 js/ts/css/html/json/md 等文本文件即可在弹窗里编辑:CodeMirror 6 与语言包按需加载,Markdown 预览依赖仅在预览时加载。 小文件停止输入 3 秒后自动保存,自动写入至少间隔 10 秒;达到 128K 字符时改为 15 秒防抖、30 秒最小间隔。 Ctrl/Cmd+S 和关闭前的保存不受此间隔限制。保存仍传输整个文件,但不会为每次自动保存重新读取整个目录。 文本按扩展名白名单 + 单文件 2 MiB 限制,读取使用现有 SFTP 连接的 64 KiB 二进制分块,超限与二进制文件仍走下载。 保存前和 rename 前都核对远端 mtime;未能取得新版本时暂停自动保存,不会静默跳过检查后覆盖。 mtime 仅为秒级尽力检测,并非文件锁或原子比较写入,请避免多人同时编辑同一文件。 文件管理器按主机记住上次目录(只存在浏览器本地)。
Terminal 与 File Manager 共用同一个 SSH 会话,切换之间不必重新连接。
📊 服务器进程监控 · Server Monitor

在 SSH 会话里就能直接看到:
CPU · Load · Memory · Swap · Processes · OS · Architecture
不需要跳出工作台,也不需要在服务器上额外装一个 Agent。
🔌 HTTP 端口转发 · Port Forwarding
服务器上有些服务只监听:
127.0.0.1:3000
在 EdgeSSH 里,同样可以打开它。

EdgeSSH 能借助已有的 SSH 会话,访问服务器本地的 HTTP 服务,并在浏览器里给出一个临时预览。
适合这些场景:
开发服务器 · 本地 Dashboard · 管理面板 · 调试接口
如果要打开的是一个不太信任的网页,可以改用独立的 Preview Worker,把预览页面和 EdgeSSH 管理界面隔离开来。
Port Forwarding 不是一个通用反向代理。WebSocket、HTTPS 上游以及部分复杂 Web 应用,目前还有一些限制。
📝 命令片段库 · Code Snippets

把常用命令存下来:
docker ps
df -h
free -h
journalctl -xe
需要的时候:
搜索 → 填入 Terminal → 检查一遍 → 执行
代码片段通过 D1 同步,而不是只存在某一个浏览器里。
🌍 服务器,也可以从地球上看
EdgeSSH 会根据服务器的公网 IP,估算出大致地理位置,并画在 Dashboard 的地球上。
点一下主机,就能从地图直接跳进对应的服务器。
地球组件用的是轻量的 Canvas 球面投影,没有引入 Three.js 这类较重的 3D Runtime。
地理位置仅用于可视化展示,不代表服务器的实时在线状态。
一套工作台,多种服务器管理方式
EdgeSSH
│
┌───────────────────┼───────────────────┐
│ │ │
Hosts Workspace Tools
│ │ │
Credentials Terminal File Manager
Groups SFTP Monitoring
Locations Sessions Snippets
│ │ Port Forward
└───────────────────┼───────────────────┘
│
Your Servers
不管你手上是:
VPS · Cloud VM · NAS · Home Server · Development Machine
只要它能通过公网 SSH 访问,就可以加入 EdgeSSH。
Cloudflare-native
EdgeSSH 不需要你另外部署一套 WebSSH Backend。
Browser
│
│ HTTPS / WebSocket
▼
Cloudflare Worker
│
├── Authentication
├── Host Management
├── Encrypted Storage ──────→ D1
├── Static App / API
│
└── Session Ticket
│
▼
Durable Object
│
│ Cloudflare TCP Socket
▼
SSH Server
SSH 握手、认证、密钥交换以及 Channel 通信,都在 Worker 一侧完成。
浏览器实际连接的是你的 EdgeSSH Worker,而不是直接跟目标服务器建立 TCP 连接。
用到的 Cloudflare 能力
Workers
跑起 EdgeSSH 的应用、API 与 SSH 协议逻辑。
Durable Objects
给每个 SSH 会话一份独立的生命周期与 WebSocket 状态。
D1
存放加密后的主机资料、管理员信息和代码片段。
TCP Sockets
从 Cloudflare 网络出发,建立到 SSH Server 的 TCP 连接。
认证
EdgeSSH 是一个 Single-admin Workspace。
它不是 SaaS 多租户系统,没有成员、组织,也没有角色权限。
目前支持两种登录方式:
GitHub OAuth适合想快速部署的用户。 用指定的 GitHub 账号,作为唯一的管理员身份。 不需要 Cloudflare Zero Trust。 | Cloudflare Access适合已经在用 Cloudflare Zero Trust 的用户。 可以用邮箱 OTP,也可以接入已有的 Identity Provider。 |
两种方式二选一即可。
切换认证 Provider 不会新建一个用户工作区,也不会重新生成一份主机资料库。
安全设计
服务器管理工具天生要经手高权限凭据,所以 EdgeSSH 一直在尽量收窄默认的攻击面。
加密存储
主机密码、私钥以及相关敏感资料,都会用 AES-256-GCM 加密后再存进 D1。
浏览器不会长期保存 SSH 凭据。
Host Key Verification
第一次连接服务器,必须确认 SSH Host Key Fingerprint。
指纹一旦发生变化,会再次要求你确认。

Single-admin
EdgeSSH 不开放注册,也没有多租户的数据隔离逻辑。
整个实例只属于一个管理员工作区。
Session Isolation
每个 SSH 会话都由 Durable Object 单独管理。
会话入口使用临时票据,辅助连接也会做身份检查。
SSRF Protection
连接目标必须解析到允许访问的公网地址,并且会在建立连接前检查解析结果。
信任边界
EdgeSSH 不是一个端到端加密的 SSH Gateway。
Worker 本身就是真正的 SSH Client,建立连接时必然要处理 SSH 凭据和会话数据。
所以:
请只把 EdgeSSH 部署在你自己信任、自己掌控的 Cloudflare 账户里。
同时建议给服务器上的 SSH 用户,配置刚好够用的最小权限。
快速部署
最推荐的方式是:
01 · Fork
把 EdgeSSH Fork 到你自己的 GitHub 账号下。
02 · 选择认证方式
GitHub OAuth:
AUTH_PROVIDER=github # Variable
GH_CLIENT_ID=<Client ID> # Variable
GH_CLIENT_SECRET=<Secret> # Secret
GH_ADMIN=<GitHub 用户名> # Variable
首次部署请按 GitHub OAuth 五步完整教程操作:创建的是 OAuth App,Client ID 放 Variable,Client Secret 放 Secret,回调必须是正式入口加 /auth/callback。无需 Zero Trust、Access 权限或额外 GitHub PAT。
旧版点击登录报“服务暂时不可用”:请同步含 2026-10-07 Base64 解码修复的代码,再对最新 main 运行 Deploy。保留原 Worker、D1 和 ENCRYPTION_KEY;不是靠扩大 Token 权限或换密钥解决。新版 Deploy 会自动检查 GitHub 登录入口与工作区状态。
或者 Cloudflare Access:
AUTH_PROVIDER=cloudflare # Variable,默认值
ADMIN_EMAIL=<管理员邮箱> # Variable
从旧版升级时,请将 GITHUB_CLIENT_ID、GITHUB_CLIENT_SECRET、GITHUB_ADMIN、GITHUB_ADMIN_ID
分别迁移为 GH_CLIENT_ID、GH_CLIENT_SECRET、GH_ADMIN、GH_ADMIN_ID。GitHub 保留 GITHUB_
前缀,旧名称无法作为 Actions Variable 或 Secret 使用。
03 · 添加 Cloudflare Token
无论选择哪种认证方式,都要在 GitHub Actions Secrets 里加入 CLOUDFLARE_API_TOKEN。
04 · Run workflow
运行 Deploy Workflow。
剩下的交给 EdgeSSH 自动完成:
Worker → Durable Object → D1 → Migration → Secrets → Deployment
不用你手动去复制:
D1 Database ID
Access AUD
Team Domain
Random Encryption Key
查看完整部署指南 →
认证配置、API Token 权限、自定义域名、Preview Worker、升级与迁移,都在文档站里有详细说明。
当前支持
| 能力 | 支持情况 |
|---|---|
| SSH | SSH 2.0 / Shell / PTY / Resize / Keepalive |
| SFTP | SFTP v3 |
| Password | ✅ |
| keyboard-interactive | ✅ 单密码提示 |
| Ed25519 | ✅ |
| RSA | ✅ |
| ECDSA | ✅ P-256 / P-384 / P-521 |
| File Manager | ✅ |
| Process Monitor | ✅ |
| OS Detection | ✅ |
| Code Snippets | ✅ |
| HTTP Port Forward | ✅ |
| Multiple Admins | ❌ |
| SCP | ❌ |
| ProxyJump | ❌ |
| SSH Agent | ❌ |
| Encrypted Private Keys | 暂不支持 |
单一管理员工作区,最多可以保存 200 台主机。
SFTP 单文件上传上限为 2 GiB(网页与 CLI 均按 64 KiB 分块、最多 512 KiB 在途),下载上限仍为 64 MiB。上传不支持断点续传;大文件传输期间请保持连接。
端口转发安全
EdgeSSH 的 Port Forward 提供两种模式。
Trusted
直接走 EdgeSSH 主 Worker。
适用于:
你自己开发、自己部署、自己信得过的网页。
这种模式和 EdgeSSH 管理界面共享浏览器 Origin,所以不建议用来打开不可信的站点。
Isolated
通过独立的 Preview Worker 提供页面。
适用于:
没法完全信任的远程 Web 应用。
Preview Worker 和 EdgeSSH 主界面运行在不同的 Origin 上,能减少被代理页面反过来调用管理 API 的风险。
完整的安全模型、生命周期、Cookie 处理和当前的代理兼容范围,请看:
本地开发
Requirements:
Node.js >= 22.12
npm
Cloudflare Wrangler
Clone:
git clone https://github.com/aozorae/EdgeSSH.git
cd EdgeSSH
npm ci
cp .env.example .dev.vars
npx wrangler d1 migrations apply DB --local
npm run dev
前端开发:
npm run dev:web
常用命令
| Command | Description |
|---|---|
npm run dev |
Build frontend and start local Worker |
npm run dev:web |
Start Vite frontend |
npm run build:web |
Build frontend |
npm run typecheck |
TypeScript check |
npm test |
Run tests |
npm run test:browser |
Browser regression tests |
npm run showcase |
Regenerate privacy-safe README screenshots with local Chrome |
npm run check |
Full validation |
npm run deploy |
Deploy to Cloudflare |
项目结构
EdgeSSH/
├── .github/workflows/ GitHub Actions deployment
├── frontend/ Web workspace
├── src/
│ ├── accounts/ Authentication & encrypted storage
│ ├── backend/ Sessions, SFTP & system detection
│ ├── ssh/ SSH protocol implementation
│ └── worker.ts Worker entry
├── migrations/ D1 migrations
├── tests/ Tests
├── DEPLOYMENT.md Deployment documentation
└── wrangler.toml Cloudflare configuration
上游项目
EdgeSSH 基于:
Worker Web SSH / CF-Workers-WebSSH
持续开发而来。
在原有的 Cloudflare WebSSH 能力之上,EdgeSSH 又加上了:
Host Management · Encrypted Storage · File Manager · Monitoring · GitHub OAuth · Port Forwarding · Code Snippets · Globe Visualization
感谢原作者 CM / cmliu 打下的这份开源基础。
特别致谢
特别感谢 Linux.do 社区 对 EdgeSSH 推广的支持,让 EdgeSSH 有机会在社区内发帖分享与交流。
Contributing
欢迎提交:
Issues · Bug Reports · Pull Requests · Documentation Improvements
提交代码前,请先跑一遍:
npm run check
涉及 SSH、SFTP 或 Port Forward 的改动,请用你自己拥有授权的服务器来测试。
提交 Issue 时,有一件事拜托一定注意:
请不要上传密码、SSH 私钥、Access Token,或是包含敏感信息的日志。
Acknowledgements
- cmliu/CF-Workers-WebSSH — EdgeSSH 的直接上游
- huashengdun/webssh — WebSSH 与前端 API 参考
- newbietan/CloudSSH — Cloudflare Workers SSH 实现参考
- crazypeace/huashengdun-webssh — WebSSH 二次开发参考
License
EdgeSSH is licensed under the Apache License 2.0.
使用、修改与分发时,请保留适用的许可证与版权声明。
EdgeSSH · 你的个人 WebSSH 工作台
SSH infrastructure doesn't need another server to manage it.
Your servers · Your credentials · Your Cloudflare · Your workspace
Frequently asked about EdgeSSH
What is EdgeSSH?+
EdgeSSH is a self-hosted Termius alternative built on the Cloudflare developer platform. A browser SSH terminal and SFTP file manager on your own Cloudflare account
What does EdgeSSH replace?+
EdgeSSH is listed as an alternative to Termius. Compare the features and tradeoffs before migrating.
What Cloudflare primitives does EdgeSSH use?+
EdgeSSH is built on D1, Durable Objects, Workers.
How much does EdgeSSH cost to run?+
Designed for one administrator on the Workers Free plan: Workers, D1 and SQLite-backed Durable Objects all have free allowances. Long or many concurrent SSH sessions consume Durable Object duration and requests, and heavy use can exceed the daily free limits. Login is GitHub OAuth for one named account, or Cloudflare Access; Access is optional, not required. Each SSH session is a Durable Object holding a WebSocket and an outbound TCP socket; idle-but-open terminals still count towards Durable Object duration. The target servers, their network and any SSH keys remain the operator’s responsibility; EdgeSSH holds encrypted copies of the credentials it uses. Source and configuration review establishes a deployment path and conditional costs; this candidate was not executed or load-tested. Official Cloudflare pricing pages saved 31 Oct 2026 (Markdown editions of developers.cloudflare.com pricing documentation). Check current Cloudflare pricing before deploying.
Is EdgeSSH open source?+
The upstream repository declares the Apache-2.0 license. Read its terms at https://raw.githubusercontent.com/aozorae/EdgeSSH/6c845463b347b49dbe4cb2ba9e838052b715347d/LICENSE. Source code and contributor credit are available at https://github.com/aozorae/EdgeSSH.
Community rating
No ratings yet. Tried this project? Share your experience.
One rating per verified account. You can change or remove yours. Accounts are email verified; use of the software is self-reported.

Discussion · 0
sign in to comment →