Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,31 @@ NODE_ENV=development
# Database (SQLite file path)
DATABASE_URL=./data/keylessh.db

# External TCP Bridge (optional, for production scaling)
# External TCP Bridge (optional, for production SSH scaling)
# BRIDGE_URL=wss://your-bridge.azurecontainerapps.io
# Note: Bridge authenticates using JWT verified against JWKS (same tidecloak.json)

# TideCloak client adapter config as JSON string (used by tcp-bridge)
# If not set, tcp-bridge falls back to data/tidecloak.json
# CLIENT_ADAPTER='{"realm":"...","auth-server-url":"...","resource":"...","jwk":{...}}'

# ============================================
# Signal Server (VM deployment — see signal-server/)
# ============================================
# The signal server handles P2P signaling, HTTP relay, and gateway portal.
# Deploy it on a VM alongside coturn using: signal-server/deploy.sh
#
# API_SECRET: Shared secret for gateway registration authentication (timing-safe)
# API_SECRET=your-shared-secret-here
#
# STUN/TURN configuration for WebRTC P2P upgrade:
# ICE_SERVERS: Comma-separated STUN server URLs
# ICE_SERVERS=stun:relay.example.com:3478
# TURN_SERVER: TURN relay fallback URL
# TURN_SERVER=turn:relay.example.com:3478
# TURN_SECRET: Shared secret for TURN REST API ephemeral credentials (HMAC-SHA256)
# TURN_SECRET=your-turn-secret-here

# Auth server override (optional, for testing)
# AUTH_SERVER_OVERRIDE_URL=http://localhost:8080

Expand Down
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,36 @@ The result: enterprise-grade SSH access control without any private keys to mana
- **Programmable policy encforcement** with Forseti contracts for SSH access
- **Simple, static, trustless SSH account access** (e.g., only `ssh:root` role holders can SSH as root)
- **Admin UX**: servers, users, roles, policy templates, change requests (access, roles, policies), sessions, logs
- **Optional external bastion** (`tcp-bridge`) for scalable WS↔TCP tunneling
- **Optional external bastion** (`bridges/tcp-bridge`) for scalable WS↔TCP tunneling
- **NAT-traversing HTTP gateway** (`bridges/punchd-bridge`) with WebRTC P2P upgrade

## Project Structure

```
keylessh/
├── client/ # React UI (xterm.js, SSH client, SFTP browser)
├── server/ # Express API + WebSocket bridge + SQLite
├── shared/ # Shared types + schema
├── signal-server/ # P2P signaling + HTTP relay for punchd-bridge
├── bridges/
│ ├── tcp-bridge/ # Stateless WS↔TCP forwarder (optional)
│ └── punchd-bridge/ # NAT-traversing HTTP reverse proxy gateway
│ └── gateway/ # Gateway source code
├── docs/ # Architecture, deployment, developer guides
└── script/ # TideCloak setup scripts
```

## Documentation

- Architecture: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
- Deployment: [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md)
- Developer guide: [docs/DEVELOPERS.md](docs/DEVELOPERS.md)

### Component docs

- **Punc'd Bridge** — NAT-traversing HTTP reverse proxy that lets you expose local web apps through a public signal server without port forwarding. Starts with HTTP relay over WebSocket, then upgrades to peer-to-peer WebRTC DataChannels. See [bridges/punchd-bridge/docs/ARCHITECTURE.md](bridges/punchd-bridge/docs/ARCHITECTURE.md) for the full connection lifecycle, PlantUML diagrams, and multi-backend routing.
- **Signal Server** — Public signaling hub that brokers WebSocket connections between gateways and clients. Handles gateway registration, ICE candidate exchange, HTTP request relay, and TURN credential provisioning. Deployed alongside a coturn sidecar for STUN/TURN. See [signal-server/deploy.sh](signal-server/deploy.sh) for the automated VM deployment script.

## Quickstart (Local Dev)

### 1. Clone and start TideCloak
Expand Down
12 changes: 12 additions & 0 deletions bridges/punchd-bridge/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
node_modules
dist
.DS_Store
*.tar.gz
data/
*.db
*.db-wal
*.db-shm
.env
.env.local
.env.*.local
.stun-deploy.env
104 changes: 104 additions & 0 deletions bridges/punchd-bridge/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Punc'd

NAT-traversing authenticated reverse proxy gateway. Access private web applications from anywhere through hole-punched WebRTC DataChannels.

## How it works

```
Browser → Signal Server (relay) → Gateway → Backend App
│ ↕ coturn │
└──── WebRTC DataChannel (P2P) ────┘
(after hole punch)
```

The system has three components:

1. **Signal Server** (public) — signaling hub, HTTP relay, portal & admin dashboard. Run by the infrastructure operator. Lives in the main repo as `signal-server/`.
2. **coturn** (public, sidecar) — STUN/TURN server for NAT traversal and relay fallback. Runs alongside the signal server with `--network host`.
3. **Gateway** (private) — authenticating reverse proxy that registers with the signal server. Run by anyone who wants to expose a local app.

The signal server and gateway can be run by **different operators**. Clients connect through the signal server's HTTP relay, then upgrade to peer-to-peer WebRTC DataChannels via NAT hole punching (using coturn for STUN binding and TURN relay fallback). A Service Worker transparently routes browser fetches through the DataChannel.

## Quick start

```bash
./start.sh
```

On first run this will:
1. Start TideCloak (Docker) and walk you through realm initialization
2. Prompt for `API_SECRET` and `TURN_SECRET` (get these from the signal server operator)
3. Install dependencies, build, and start the gateway

On subsequent runs, TideCloak and secrets are reused automatically.

### Options

```bash
./start.sh --skip-tc # skip TideCloak, gateway only
API_SECRET=xxx TURN_SECRET=yyy ./start.sh # pass secrets directly
SIGNAL_SERVER_URL=wss://signal.example.com:9090 ./start.sh # custom signal server
BACKEND_URL=http://localhost:4000 ./start.sh # custom backend
```

### Gateway only (without TideCloak)

```bash
script/gateway/start.sh
```

Prompts for secrets on first run and saves them to `script/gateway/.env`.

### Docker Compose (all services)

```bash
docker compose up --build
```

This starts coturn, the signal server, and the gateway together. Set `TURN_SECRET`, `API_SECRET`, and `EXTERNAL_IP` in your environment or a `.env` file.

## Secrets

| Secret | Generated by | Shared with | Purpose |
|--------|-------------|-------------|---------|
| `API_SECRET` | Signal server operator | Gateway operators | Authenticates gateway registration (timing-safe) |
| `TURN_SECRET` | Signal server operator | Gateway operators | Generates ephemeral TURN credentials (HMAC-SHA256) |

**Secret flow:**
1. Signal server operator deploys and generates secrets (or sets them manually)
2. Signal server operator shares `API_SECRET` + `TURN_SECRET` with gateway operators
3. Gateway operator sets them as env vars or pastes them when `./start.sh` prompts

The start scripts load secrets in this order:
1. Environment variables (if already set)
2. `script/gateway/.env` (saved from a previous run)
3. `signal-server/.env` (only if you run both locally)
4. Prompts you to paste them (saves to `script/gateway/.env` for next time)

## Environment variables

See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#configuration-reference) for the full configuration reference.

### Key variables

| Variable | Component | Description |
|----------|-----------|-------------|
| `SIGNAL_SERVER_URL` | Gateway | WebSocket URL of the signal server |
| `BACKEND_URL` | Gateway | Backend to proxy to |
| `BACKENDS` | Gateway | Multiple backends: `"App=http://host:3000,Auth=http://host:8080;noauth"` |
| `API_SECRET` | Both | Shared secret for gateway registration |
| `TURN_SECRET` | Both + coturn | Shared secret for TURN credentials (HMAC-SHA256) |
| `EXTERNAL_IP` | coturn | Public IP for TURN relay addresses |
| `TIDECLOAK_CONFIG_B64` | Both | Base64 TideCloak config for authentication |
| `TC_INTERNAL_URL` | Gateway | Internal TideCloak URL when `KC_HOSTNAME` is public |
| `GATEWAY_DISPLAY_NAME` | Gateway | Name shown in the portal |
| `GATEWAY_DESCRIPTION` | Gateway | Description shown in the portal |

## Documentation

- [Architecture & Protocol Details](docs/ARCHITECTURE.md) — full system docs with diagrams
- [PlantUML sources](docs/diagrams/) — editable sequence diagrams

## License

Proprietary
135 changes: 135 additions & 0 deletions bridges/punchd-bridge/admin.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env bash
# TideCloak admin helper — interactive admin session.
#
# Temporarily restarts TideCloak with KC_HOSTNAME=localhost so the admin
# console works natively (no proxy issues). When you're done, it restarts
# with the public KC_HOSTNAME and re-signs IdP settings.
#
# Usage:
# ./admin.sh # interactive admin session (localhost → public)
# ./admin.sh --resign # just re-sign IdP settings (no admin session)

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "$0")" && pwd)"
TC_PORT="${TC_PORT:-8080}"
TC_CONTAINER="${TC_CONTAINER:-mytidecloak}"
REALM_NAME="${NEW_REALM_NAME:-keylessh}"
CLIENT_NAME="${CLIENT_NAME:-myclient}"
STUN_SERVER_URL="${STUN_SERVER_URL:-wss://tidestun.codesyo.com:9090}"
TC_PUBLIC_URL="${TC_PUBLIC_URL:-$(echo "$STUN_SERVER_URL" | sed 's|^wss://|https://|;s|^ws://|http://|;s|:[0-9]*$||')}"

wait_for_tc() {
echo -n " Waiting for TideCloak..."
for i in $(seq 1 30); do
if curl -s -f --connect-timeout 3 "http://localhost:${TC_PORT}" > /dev/null 2>&1; then
echo " ready"
return
fi
echo -n "."
sleep 3
done
echo " timeout!"
exit 1
}

get_admin_token() {
curl -s -f -X POST "http://localhost:${TC_PORT}/realms/master/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin" -d "password=password" \
-d "grant_type=password" -d "client_id=admin-cli" | jq -r '.access_token'
}

restart_tc() {
local kc_hostname="$1"
docker rm -f "$TC_CONTAINER" > /dev/null 2>&1 || true
docker run \
--name "$TC_CONTAINER" \
-d \
-v "${REPO_ROOT}/script/tidecloak":/opt/keycloak/data/h2 \
-p "${TC_PORT}:8080" \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=password \
-e "KC_HOSTNAME=${kc_hostname}" \
-e SYSTEM_HOME_ORK=https://sork1.tideprotocol.com \
-e USER_HOME_ORK=https://sork1.tideprotocol.com \
-e THRESHOLD_T=3 \
-e THRESHOLD_N=5 \
-e PAYER_PUBLIC=20000011d6a0e8212d682657147d864b82d10e92776c15ead43dcfdc100ebf4dcfe6a8 \
tideorg/tidecloak-stg-dev:latest
wait_for_tc
}

resign_idp_settings() {
echo " Signing IdP settings..."
local token
token="$(get_admin_token)"
curl -s -f -X POST "http://localhost:${TC_PORT}/admin/realms/${REALM_NAME}/vendorResources/sign-idp-settings" \
-H "Authorization: Bearer $token" > /dev/null 2>&1
echo " IdP settings signed with current KC_HOSTNAME"
}

# ── --resign: just re-sign without interactive session ──────────
if [ "${1:-}" = "--resign" ]; then
echo "[Admin] Re-signing IdP settings with public URL: ${TC_PUBLIC_URL}"
echo ""

echo " Step 1/2: Restarting TideCloak with KC_HOSTNAME=${TC_PUBLIC_URL}"
restart_tc "$TC_PUBLIC_URL"

echo " Step 2/2: Signing IdP settings..."
resign_idp_settings

echo ""
echo "Done! IdP settings signed with ${TC_PUBLIC_URL}."
echo "Restart the gateway to pick up the new KC_HOSTNAME."
exit 0
fi

# ── Interactive admin session ───────────────────────────────────
echo ""
echo "=== TideCloak Admin Session ==="
echo ""
echo "This will:"
echo " 1. Restart TideCloak with KC_HOSTNAME=localhost (admin console works)"
echo " 2. Wait for you to do admin work"
echo " 3. Restart with KC_HOSTNAME=${TC_PUBLIC_URL} (for Tide SDK)"
echo " 4. Re-sign IdP settings"
echo ""
read -rp "Press Enter to start (Ctrl+C to cancel)..."

# Step 1: Restart with localhost
echo ""
echo "[Step 1/4] Restarting TideCloak with KC_HOSTNAME=http://localhost:${TC_PORT}"
restart_tc "http://localhost:${TC_PORT}"

echo ""
echo "============================================"
echo " Admin Console Ready!"
echo ""
echo " URL: http://localhost:${TC_PORT}/admin/master/console/"
echo " Login: admin / password"
echo " Realm: ${REALM_NAME}"
echo ""
echo " Do your admin work now (create token drafts, etc.)"
echo "============================================"
echo ""
read -rp "Press Enter when done with admin work..."

# Step 2: Restart with public URL
echo ""
echo "[Step 2/4] Restarting TideCloak with KC_HOSTNAME=${TC_PUBLIC_URL}"
restart_tc "$TC_PUBLIC_URL"

# Step 3: Re-sign IdP settings
echo "[Step 3/4] Re-signing IdP settings with public URL"
resign_idp_settings

# Step 4: Done
echo ""
echo "[Step 4/4] Done!"
echo ""
echo " KC_HOSTNAME is now: ${TC_PUBLIC_URL}"
echo " IdP settings signed with public URL."
echo " Restart the gateway to resume normal operation."
echo ""
36 changes: 36 additions & 0 deletions bridges/punchd-bridge/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
version: "3.8"

services:
coturn:
image: coturn/coturn:latest
network_mode: host
command:
- "--listening-port=3478"
- "--external-ip=${EXTERNAL_IP:-}"
- "--use-auth-secret"
- "--static-auth-secret=${TURN_SECRET:-}"
- "--realm=keylessh"
- "--min-port=49152"
- "--max-port=65535"
- "--fingerprint"
- "--no-multicast-peers"
- "--no-cli"
- "--log-file=stdout"
- "--verbose"
restart: unless-stopped

gateway:
build: ./gateway
environment:
- LISTEN_PORT=7891
- HEALTH_PORT=7892
- BACKEND_URL=${BACKEND_URL:-http://host.docker.internal:3000}
- STUN_SERVER_URL=${STUN_SERVER_URL:-}
- TIDECLOAK_CONFIG_B64=${TIDECLOAK_CONFIG_B64:-}
- ICE_SERVERS=${ICE_SERVERS:-}
- TURN_SERVER=${TURN_SERVER:-}
- TURN_SECRET=${TURN_SECRET:-}
- AUTH_SERVER_PUBLIC_URL=${AUTH_SERVER_PUBLIC_URL:-}
- STRIP_AUTH_HEADER=${STRIP_AUTH_HEADER:-false}
- API_SECRET=${API_SECRET:-}
restart: unless-stopped
Loading