Skip to content

maciekaz/edgex

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Edgex

Serverless script orchestration platform built on Cloudflare Workers. Deploy PowerShell and Bash scripts to any number of remote machines — no VPN, no open ports, no agent service required.


Table of contents


Features

Script management

  • PowerShell & Bash support — upload, version, and manage scripts for both Windows and Linux/macOS targets
  • In-browser editor — syntax-highlighted script editor directly in the dashboard, no external tooling needed
  • Automatic versioning — every update bumps the patch version (1.0.0 → 1.0.1); full version history visible in the UI
  • R2-backed storage — scripts are stored durably in Cloudflare R2 with zero egress fees

Deployment & orchestration

  • One-liner bootstrapper — a single PowerShell or Bash command enrolls a machine and executes the script; nothing is left behind
  • Multi-machine deployments — deploy to any number of machines simultaneously; track each one individually
  • Time-limited or permanent deployments — deployments can expire after a configurable number of hours or run indefinitely (expires_hours: null)
  • Re-run protection — configurable per-platform limit on how many times a single machine can re-run the same deployment; enforced atomically (no race conditions)
  • Live status tracking — per-machine status (running, completed, failed, expired) updated in real time as the script executes
  • Structured feedback — agents report back info, success, warning, and error log entries with optional step names; all stored in D1 and visible in the dashboard
  • Cancel at any level — cancel an entire deployment or a single machine target with one click

Security

  • Zero Trust auth — admin dashboard and all admin API routes protected by Cloudflare Access (JWKS + Web Crypto JWT verification in-process)
  • TOFU enrollment — Trust On First Use: the first machine to enroll with a given machine_id gets a unique cryptographic token; subsequent enrollments are idempotent; hostname conflicts are rejected
  • Cryptographic Kill Switch — SHA-256 checksum of the script is pinned at deployment creation; a single-bit difference in the stored file instantly expires the deployment and blocks all machines from downloading it
  • Token isolation — the master_token (shared per deployment) is used only for enrollment; all subsequent agent calls use a per-machine agent_token that cannot be used to enroll new machines
  • Wrapper snapshot isolation — the PS1/Bash wrapper template active at deployment creation is snapshotted in D1; later admin edits to the wrapper do not affect running deployments

AI assistant

  • Script generation — describe what a script should do in plain language; Edgex generates production-ready PowerShell or Bash using @cf/qwen/qwen2.5-coder-32b-instruct running on Workers AI (on-platform, no external API)
  • Refactoring — paste existing code and ask for improvements; the model rewrites it while preserving intent
  • Security audit — one-click audit flags potential vulnerabilities, unsafe patterns, and privilege escalation risks in your scripts
  • Explain mode — get a plain-English breakdown of what any script does, line by line

Dashboard & UX

  • Enterprise glassmorphism dashboard — dark background, frosted-glass cards, served at /dashboard/
  • Live geo map — every enrolled machine appears on an interactive Leaflet map (CartoDB Dark Matter tiles) with hub-and-spoke animated polylines showing real-time connectivity
  • Geo metadatacolo, city, country, latitude, longitude captured automatically from each agent request via Cloudflare edge metadata; no GeoIP library needed
  • Bilingual UI — full English / Polish localisation (EN/PL), switchable at runtime without a page reload
  • Customisable wrapper templates — edit the PS1 and Bash wrapper boilerplate (helper functions injected around every script) directly from the Settings panel; changes are logged with timestamps

Why Edgex?

Traditional RMM tools require either an always-on agent service (attack surface), an open inbound port (firewall nightmare), or a VPN tunnel (ops overhead). Edgex takes a different approach:

  • Pull-based execution — machines call out to the platform, never the other way around. No inbound ports needed.
  • Ephemeral scripts — scripts are not stored on the machine. They are downloaded, executed in memory, and discarded.
  • Zero persistent agent — the bootstrapper is a one-liner. No service to install, no registry entries, no scheduled tasks left behind.
  • Per-machine identity — every machine gets a unique cryptographic token at enrollment. A leaked token compromises one machine, not the entire deployment.
  • Cryptographic integrity — the platform pins a SHA-256 checksum of the script at deployment creation time. If the stored script is tampered with at any point, the deployment is killed instantly before any machine receives the compromised payload.
  • Bilingual UI — the admin dashboard is fully localised in English and Polish (EN/PL), switchable at runtime without a page reload.

Why Cloudflare Workers?

Property Benefit
Edge network (300+ PoPs) Scripts are served from the closest datacenter to the target machine. Latency is minimal regardless of geography.
No cold starts Workers run in V8 isolates, not containers. Startup time is ~1 ms, not seconds.
No infrastructure to manage No EC2, no load balancer, no TLS termination, no autoscaling groups.
D1 (SQLite at the edge) Relational database with full SQL, zero ops.
R2 (object storage) Script files stored durably with zero egress fees.
Workers AI On-platform LLM for script generation, refactoring and security auditing — no external API calls.
Cloudflare Access Zero-trust authentication for the admin dashboard — cryptographic JWT verification, no passwords to manage.
Built-in geo metadata Every agent request carries colo, city, country, latitude, longitude from the CF edge — no GeoIP library needed.

How it works

Edgex separates concerns into two distinct token-based flows:

Deployment flow (admin)

sequenceDiagram
    participant Admin
    participant Dashboard
    participant Worker
    participant D1
    participant R2

    Admin->>Dashboard: Upload script
    Dashboard->>Worker: POST /v1/admin/scripts
    Worker->>R2: Store script file
    Worker->>D1: INSERT Scripts (checksum, version)

    Admin->>Dashboard: Create deployment
    Dashboard->>Worker: POST /v1/admin/deployments
    Worker->>D1: Fetch script checksum + wrapper template
    Worker->>D1: INSERT Deployments (master_token, script_checksum, wrapper_snapshot)
    Worker-->>Dashboard: master_token + installer one-liner
Loading

Agent flow (target machine)

sequenceDiagram
    participant Machine
    participant Worker
    participant D1
    participant R2

    Note over Machine: Admin pastes one-liner on target

    Machine->>Worker: GET /v1/agent/installer (Bearer: master_token)
    Worker-->>Machine: Bootstrapper script (PS1 or Bash)

    Machine->>Worker: POST /v1/agent/enroll (hostname, machine_id)
    Note over Worker: TOFU — first seen machine_id → new target
    Worker->>D1: INSERT DeploymentTargets (agent_token)
    Worker-->>Machine: agent_token (unique per machine)

    Machine->>Worker: GET /v1/agent/download (Bearer: agent_token)
    Worker->>R2: Fetch script content
    Note over Worker: SHA-256 verify vs. pinned checksum
    Worker->>D1: Inject agent_token into wrapper
    Worker-->>Machine: Wrapped + encoded script

    loop Execution
        Machine->>Worker: POST /v1/agent/feedback (status, message, step)
        Worker->>D1: INSERT Logs, UPDATE target status
    end
Loading

Token lifecycle

stateDiagram-v2
    [*] --> master_token: Deployment created
    master_token --> agent_token: Machine enrolls (TOFU)
    master_token --> [*]: Deployment expires / cancelled

    agent_token --> running: /download called
    running --> completed: feedback step=complete status=success
    running --> failed: feedback status=error
    running --> expired: admin cancel
    completed --> running: re-run (if under limit)
    failed --> running: re-run (if under limit)
Loading

Architecture

graph TB
    subgraph Admin["Admin (browser)"]
        UI["Dashboard UI\n/dashboard/*"]
    end

    subgraph CF["Cloudflare Edge"]
        Access["Cloudflare Access\nJWT verification"]
        Worker["Edgex Worker\nHono on Workers Runtime"]
        D1[("D1 Database\nSQLite")]
        R2[("R2 Bucket\nScript files")]
        AI["Workers AI\nQwen 2.5 Coder 32B"]
    end

    subgraph Machines["Target machines"]
        PS["Windows\nPowerShell 5.1+"]
        Bash["Linux / macOS\nBash"]
    end

    UI -->|HTTPS + CF Access JWT| Access
    Access --> Worker
    Worker --- D1
    Worker --- R2
    Worker --- AI

    PS -->|Bearer agent_token| Worker
    Bash -->|Bearer agent_token| Worker
Loading

Wrapper execution model

The script is never transmitted in plain text to the machine. It is:

  1. Base64-encoded server-side before injection into the wrapper template.
  2. Decoded in memory on the machine at runtime — no file written to disk.
  3. Executed inside an isolated scope (PowerShell [scriptblock]::Create() dot-sourced; Bash eval inside a subshell).
  4. Discarded after execution — nothing persists on the machine.
graph LR
    Raw["Raw script\n(R2)"] -->|SHA-256 verify| Check{"Checksum\nmatch?"}
    Check -->|No| Kill["Kill Switch:\nexpire deployment\nlog violation\nreturn 500"]
    Check -->|Yes| Encode["Base64 encode payload"]
    Encode --> Template["Inject into\nwrapper template"]
    Template --> Send["Serve to machine\nCache-Control: no-store"]
    Send --> Exec["Machine decodes\n+ executes in memory"]
Loading

Security

Authentication layers

Layer Mechanism Protects
Cloudflare Access JWKS-verified JWT (Cf-Access-Jwt-Assertion) All admin routes + dashboard UI
deploymentAuth Bearer master_token (shared per deployment) /installer, /enroll only
agentAuth Bearer agent_token (unique per machine) /download, /feedback

Cloudflare Access is verified in-process inside the Worker (src/middleware/accessAuth.ts): the JWT signature is validated against the team's JWKS endpoint using Web Crypto, and the aud + exp claims are checked. This is defense-in-depth — even if the edge-level CF Access policy were bypassed, the Worker itself rejects unsigned requests.

workers.dev subdomain

Important: Every Cloudflare Worker gets a public *.workers.dev URL that is not covered by your custom-domain CF Access policies by default. Anyone who knows the workers.dev URL can reach your Worker without going through the Access-protected custom domain.

Mitigate this by doing one of the following (both recommended):

  1. Disable workers.dev — go to Workers & Pages → scripter → Settings → Domains & Routes and toggle workers.dev Route off.
  2. Add a separate CF Access policy for the workers.dev domain covering /dashboard/* and /v1/admin/*.

Agent routes (/v1/agent/*) rely on Bearer tokens, not CF Access, so they are unaffected.

Local development bypass

When WORKER_URL starts with http://localhost, the Worker skips JWKS verification entirely and injects dev@local.host as the authenticated user. This makes wrangler dev work without a real CF Access setup.

Never set WORKER_URL=http://localhost (or any non-HTTPS value) in a deployed environment — it disables all JWT authentication for admin routes.

Key security properties

Zero Trust Storage (Cryptographic Kill Switch) At deployment creation, the SHA-256 checksum of the script is pinned in D1. On every /download request, the Worker recomputes the hash from the R2 object and compares it to the pinned value. A single-bit difference (tampering, corruption, supply-chain attack) triggers:

  • Immediate expiry of the entire deployment
  • kill_reason = 'INTEGRITY_VIOLATION' written to D1
  • Generic 500 returned to the agent — no hash details leaked

TOFU enrollment (Trust On First Use) The first machine to enroll with a given machine_id gets a unique agent_token. Subsequent enrollments with the same machine_id return the same token (idempotent). A different machine claiming the same hostname is rejected with 409 HOSTNAME_CONFLICT.

Token isolation master_token is used only for enrollment. Once a machine has its agent_token, the master_token is never used again by that machine. Leaking the master token allows enrollment of new machines, but does not grant access to any existing machine's execution context.

Enrollment cap POST /v1/agent/enroll checks enrolled_count >= expected_machines before creating a new target. Prevents unbounded target creation even with a leaked master token.

Wrapper snapshot isolation The wrapper template active at deployment creation time is snapshotted into D1. Subsequent admin edits to the wrapper do not affect running deployments — each deployment executes with exactly the wrapper that was in effect when it was created.

Re-run TOCTOU protection Re-runs use an atomic UPDATE … WHERE run_count < maxRuns. If two concurrent requests race, exactly one succeeds — the other gets 429 RERUN_LIMIT_EXCEEDED. No race condition.

Input validation

Field Constraint
hostname ≤ 253 chars, no CRLF / null bytes
machine_id ≤ 300 chars, no CRLF / null bytes
step ≤ 100 chars
message ≤ 4 096 chars
script content ≤ 1 MiB
prompt (AI) ≤ 8 000 chars
Route :id params Parsed with parseIntNaN / non-positive → 400

HTTP security headers (applied to all responses):

Strict-Transport-Security: max-age=63072000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Content-Security-Policy: default-src 'none'; script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com https://unpkg.com https://static.cloudflareinsights.com; connect-src 'self' https://cloudflareinsights.com; …
Cache-Control: no-store  (all JSON API responses)

Stack

Component Technology
Runtime Cloudflare Workers (V8 isolates)
Framework Hono v4.12.2
Database Cloudflare D1 (SQLite)
Object storage Cloudflare R2
AI Workers AI — @cf/qwen/qwen2.5-coder-32b-instruct
Auth Cloudflare Access (JWKS + Web Crypto JWT verification)
UI Single-file SPA (Tailwind CDN, Leaflet, vanilla JS)
Tooling Wrangler v4, TypeScript

Project structure

src/
├── index.ts                    # Entry point — routing, CSP, security headers
├── env.ts                      # Hono AppEnv / Bindings / Variables types
│
├── db/
│   ├── schema.sql              # D1 schema (apply once per environment)
│   ├── types.ts                # TypeScript interfaces for DB rows
│   └── migrations/
│       ├── 001_geo_fields.sql          # Geo columns on DeploymentTargets
│       ├── 003_rerun.sql               # run_count + SystemSettings
│       ├── 004_wrapper_templates.sql   # WrapperTemplates + logs
│       ├── 005_enrollment.sql          # agent_token + machine_id (TOFU)
│       ├── 006_integrity_snapshot.sql  # script_checksum, wrapper_snapshot
│       ├── 007_kill_reason.sql         # kill_reason on Deployments
│       └── 008_permanent_deployments.sql # expires_at nullable
│
├── middleware/
│   ├── accessAuth.ts           # Cloudflare Access JWT verification
│   └── auth.ts                 # deploymentAuth, agentAuth, adminAuth
│
├── routes/
│   ├── agent.ts                # /v1/agent/* — installer, enroll, download, feedback
│   ├── admin.ts                # /v1/admin/* — scripts, deployments, AI, settings, wrappers
│   ├── deployments.ts          # DEPRECATED — empty stub, do not import
│   └── feedback.ts             # DEPRECATED — empty stub, do not import
│
├── ui/
│   └── dashboard_v2.ts         # Dashboard SPA (glassmorphism, enterprise black theme)
│
└── lib/
    ├── wrapper.ts              # buildWrapper(), default PS1 + Bash templates
    └── ai-prompts.ts           # System prompts for generate / refactor / audit / explain

Database schema

erDiagram
    Scripts {
        int id PK
        text name
        text type "powershell | bash"
        text r2_key
        text version "semver"
        text checksum "SHA-256"
        text created_by_email
        text created_at
    }

    Deployments {
        int id PK
        int script_id FK
        text name
        text master_token "UNIQUE — shared per deployment"
        text expires_at "NULL = permanent"
        int expected_machines
        text script_checksum "pinned at creation"
        text script_version "pinned at creation"
        text wrapper_snapshot "pinned at creation"
        text kill_reason "NULL or INTEGRITY_VIOLATION"
        text created_by_email
        text created_at
    }

    DeploymentTargets {
        int id PK
        int deployment_id FK
        text hostname
        text machine_id "hardware UUID"
        text agent_token "UNIQUE per machine"
        text status "running|completed|failed|expired"
        int run_count
        text colo
        text city
        text country
        real latitude
        real longitude
        text started_at
        text completed_at
        text last_seen_at
    }

    Logs {
        int id PK
        int target_id FK
        text step
        text status "info|success|error|warning"
        text message
        text timestamp
    }

    SystemSettings {
        text key PK
        text value
    }

    WrapperTemplates {
        text type PK "powershell | bash"
        text content
        text updated_at
        text updated_by
    }

    Scripts ||--o{ Deployments : "deployed as"
    Deployments ||--o{ DeploymentTargets : "runs on"
    DeploymentTargets ||--o{ Logs : "emits"
Loading

API reference

Agent endpoints (Bearer: token)

Method Path Auth Description
GET /v1/agent/installer?type=ps1|bash master_token Returns bootstrapper script
POST /v1/agent/enroll master_token TOFU enrollment → agent_token
GET /v1/agent/download agent_token Returns wrapped + encoded script
POST /v1/agent/feedback agent_token Send execution log entry

Enroll request body:

{ "hostname": "WORKSTATION-01", "machine_id": "a1b2c3d4-..." }

Feedback request body:

{ "status": "info|success|error|warning", "message": "...", "step": "optional-step-name" }

Admin endpoints (Cloudflare Access JWT required)

Method Path Description
GET /v1/admin/me Returns authenticated user email
GET /v1/admin/scripts List all scripts
POST /v1/admin/scripts Create script (upload to R2 + D1)
GET /v1/admin/scripts/:id Script metadata by ID
GET /v1/admin/scripts/:id/content Raw script content from R2
PUT /v1/admin/scripts/:id Update script (bumps patch version)
DELETE /v1/admin/scripts/:id Delete script
GET /v1/admin/deployments List deployments with target counts
POST /v1/admin/deployments Create deployment
GET /v1/admin/deployments/:id Deployment detail + targets
GET /v1/admin/deployments/:id/installer Universal bootstrapper
PUT /v1/admin/deployments/:id/cancel Cancel deployment + running targets
DELETE /v1/admin/deployments/:id Hard delete deployment
GET /v1/admin/deployments/:id/logs Recent logs feed (all targets)
GET /v1/admin/deployments/:id/targets/:tid/logs Logs for one target
PUT /v1/admin/deployments/:id/targets/:tid/cancel Cancel single target
GET /v1/admin/settings Get platform settings
PUT /v1/admin/settings Update platform settings
POST /v1/admin/ai/process AI code generation / refactor / audit / explain
GET /v1/admin/wrapper/:type Get wrapper template (ps1 or bash)
PUT /v1/admin/wrapper/:type Update wrapper template
DELETE /v1/admin/wrapper/:type Reset wrapper to default
GET /v1/admin/wrapper/:type/logs Wrapper change log

Create deployment body:

{
  "script_id": 1,
  "name": "Patch Tuesday — Floor 2",
  "description": "optional",
  "expected_machines": 10,
  "expires_hours": 72
}

Set expires_hours to 0 or null for a permanent deployment (never expires).


Configuration

Copy wrangler.toml.example to wrangler.toml and fill in your values:

name            = "edgex"
main            = "src/index.ts"
compatibility_date = "2026-02-22"

[[routes]]
pattern    = "edgex.your-domain.com"
zone_name  = "your-domain.com"
custom_domain = true

[vars]
WORKER_URL = "https://edgex.your-domain.com"

[[d1_databases]]
binding       = "DB"
database_name = "edgex-db"
database_id   = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

[[r2_buckets]]
binding     = "SCRIPTS_BUCKET"
bucket_name = "edgex-scripts"

[ai]
binding = "AI"

Secrets (never in wrangler.toml)

Set via wrangler secret put:

wrangler secret put CF_ACCESS_AUD          # Cloudflare Access application AUD tag
wrangler secret put CF_ACCESS_TEAM_DOMAIN  # e.g. your-team.cloudflareaccess.com

Find these values in your Cloudflare Zero Trust dashboard under Access → Applications.


Setup

Prerequisites

1. Clone and install

git clone https://github.com/your-username/edgex.git
cd edgex
npm install

2. Create Cloudflare resources

# D1 database
wrangler d1 create edgex-db

# R2 bucket
wrangler r2 bucket create edgex-scripts

Paste the returned database_id into wrangler.toml.

3. Apply database schema

# Local development
wrangler d1 execute edgex-db --local --file=src/db/schema.sql

# Production
wrangler d1 execute edgex-db --file=src/db/schema.sql

4. Configure Cloudflare Access

  1. Go to Zero Trust → Access → Applications → Add an application
  2. Choose Self-hosted, set the domain to match WORKER_URL
  3. Copy the AUD tag and your team domain
  4. Set them as secrets:
wrangler secret put CF_ACCESS_AUD
wrangler secret put CF_ACCESS_TEAM_DOMAIN

5. Deploy

wrangler deploy

Local development

# Reset local D1 state
rm -rf .wrangler/state/v3/d1/

# Apply schema locally
wrangler d1 execute edgex-db --local --file=src/db/schema.sql

# Start local dev server (CF Access is bypassed automatically for localhost)
wrangler dev

The dashboard is available at http://localhost:8787/dashboard/.

In local dev mode, CF Access JWT verification is skipped and dev@local.host is used as the authenticated email. The Authorization header for agent endpoints still needs to be provided with a valid token from the local D1 database.

Injected wrapper functions

Scripts uploaded to Edgex have access to these helpers at runtime — they are injected by the wrapper and do not need to be imported or defined:

PowerShell

_Orc-Send -Status 'info'|'success'|'error'|'warning' -Message 'text' [-Step 'step-name']
Get-OrcSystemInfo       # Returns hashtable: Hostname, OS, OSBuild, RAM_GB, IP, User, Domain, TimeZone, MachineId
Test-OrcAdmin           # Returns $true if running as Administrator
Invoke-OrcWithRetry -ScriptBlock { ... } [-MaxAttempts 3] [-DelaySeconds 5]

Bash

_orc_send 'info|success|error|warning' 'message' ['step-name']
_orc_system_info        # Prints: Hostname | OS | Arch | RAM | IP | User
_orc_is_root            # Returns exit code 0 if running as root
_orc_retry 'command' [max_attempts] [delay_sec] ['step-name']

# Read-only platform variables:
# $_ORC_OS           linux | macos | freebsd | …
# $_ORC_OS_VERSION   e.g. "Ubuntu 24.04.1 LTS"
# $_ORC_ARCH         x86_64 | arm64 | …
# $_ORC_MACHINE_ID   stable hardware UUID (or fallback:sha256)
# $_ORC_HOSTNAME     hostname

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors