Add RustDesk Server Pro self-hosted relay stack
Scaffold the CDK stack for a self-hosted RustDesk Server Pro relay so remote support no longer depends on the public RustDesk rendezvous/relay infrastructure. Single ARM64 EC2 (SSM-managed, no SSH) runs hbbs+hbbr in Docker. The server key pair and DB live on a standalone RETAINed EBS volume so they survive instance replacement (clients keep trusting the same key). Relay ports are public; the Pro admin console (21114) is restricted to the office VPN + VPC. IMDSv2 is enforced and the data dir is locked to root. EIP + rustdesk.seahaven.com give clients a stable address.
This commit is contained in:
commit
9f18ecab50
17 changed files with 1196 additions and 0 deletions
15
.github/dependabot.yml
vendored
Normal file
15
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
assignees:
|
||||||
|
- amoussa1229
|
||||||
|
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
assignees:
|
||||||
|
- amoussa1229
|
||||||
10
.github/workflows/ci.yaml
vendored
Normal file
10
.github/workflows/ci.yaml
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@main
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
6
.github/workflows/dependency-review.yml
vendored
Normal file
6
.github/workflows/dependency-review.yml
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
name: Dependency Review
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
jobs:
|
||||||
|
review:
|
||||||
|
uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@main
|
||||||
20
.github/workflows/deploy.yaml
vendored
Normal file
20
.github/workflows/deploy.yaml
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
name: Deploy
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@main
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
secrets:
|
||||||
|
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
||||||
11
.github/workflows/labeler.yml
vendored
Normal file
11
.github/workflows/labeler.yml
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
name: Labeler
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
jobs:
|
||||||
|
label:
|
||||||
|
uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@main
|
||||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
.env
|
||||||
|
node_modules/
|
||||||
|
cdk.out/
|
||||||
|
*.js
|
||||||
|
*.d.ts
|
||||||
|
*.js.map
|
||||||
|
__pycache__/
|
||||||
87
README.md
Normal file
87
README.md
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
# rustdesk-server
|
||||||
|
|
||||||
|
Self-hosted [RustDesk Server Pro](https://rustdesk.com/docs/en/self-host/) (remote-desktop relay + rendezvous) for Sea Haven Industries, deployed to AWS via CDK.
|
||||||
|
|
||||||
|
> Status: **scaffold** — not yet deployed. See [First deploy](#first-deploy).
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
A single ARM64 EC2 instance runs RustDesk Server Pro (`hbbs` rendezvous/ID + `hbbr` relay) in Docker. Clients connect from anywhere over the public internet to a stable Elastic IP fronted by `rustdesk.seahaven.com`.
|
||||||
|
|
||||||
|
```
|
||||||
|
RustDesk clients (anywhere)
|
||||||
|
│ TCP 21114-21119 / UDP 21116
|
||||||
|
▼
|
||||||
|
Elastic IP ──► EC2 t4g.small (AL2023 arm64, SSM-managed)
|
||||||
|
│ Docker: hbbs + hbbr (network_mode: host)
|
||||||
|
├─ /dev/xvda 20 GiB root (OS only, ephemeral)
|
||||||
|
└─ /var/lib/rustdesk ◄── standalone EBS 20 GiB (RETAIN)
|
||||||
|
key pair + sled DB
|
||||||
|
Nightly DLM snapshots (retain 30, tag rustdesk-backup=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
All durable state (the `id_ed25519` server key pair and the sled database) lives on a **standalone, RETAINed** EBS data volume, never an inline block device, so it survives instance replacement and stack deletion. See [RUNBOOK.md](RUNBOOK.md).
|
||||||
|
|
||||||
|
## Ports
|
||||||
|
|
||||||
|
| Port | Proto | Purpose | Exposure |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 21114 | TCP | Pro web console / API | **VPN/VPC only** (10.10.0.0/16, 10.20.0.0/16) |
|
||||||
|
| 21115 | TCP | hbbs NAT type test | public |
|
||||||
|
| 21116 | TCP + UDP | hbbs registration / hole punch / heartbeat | public |
|
||||||
|
| 21117 | TCP | hbbr relay | public |
|
||||||
|
| 21118 | TCP | hbbs web client (websocket) | public |
|
||||||
|
| 21119 | TCP | hbbr web client (websocket) | public |
|
||||||
|
|
||||||
|
Relay/rendezvous ports are public so clients connect from anywhere. The Pro admin console (21114) is restricted to the office VPN + VPC, so you administer the server and activate the Pro license over VPN. To take the relay VPN-only later, flip the `public` flags in `RUSTDESK_PORTS` in the stack.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- **EC2** `rustdesk-server` — AL2023 arm64, `t4g.small`, SSM-managed (no inbound SSH)
|
||||||
|
- **EBS data volume** `rustdesk-data` — 20 GiB GP3, encrypted, `RemovalPolicy.RETAIN`, attached at `/dev/xvdf`
|
||||||
|
- **Elastic IP** — stable public address, associated to the instance
|
||||||
|
- **Security group** `rustdesk-server` — RustDesk ports above
|
||||||
|
- **IAM role** `rustdesk-server-instance` — `AmazonSSMManagedInstanceCore` + `secretsmanager:GetSecretValue` on `rustdesk/*`
|
||||||
|
- **DLM** `rustdesk-server-dlm` — nightly instance snapshots, retain 30
|
||||||
|
- **Route 53** A record `rustdesk.seahaven.com` → EIP
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Secrets (Secrets Manager) — created out of band
|
||||||
|
|
||||||
|
| Secret | Contents |
|
||||||
|
|---|---|
|
||||||
|
| `rustdesk/server-key-pair` | `id_ed25519` private + `.pub` public key generated by `hbbs` on first boot (mirror up post-deploy so a replacement host keeps the same key) |
|
||||||
|
| `rustdesk/pro-license` | RustDesk Server Pro license key |
|
||||||
|
|
||||||
|
### SSM parameters (non-secret)
|
||||||
|
|
||||||
|
| Parameter | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `/rustdesk-server/relay-host` | Public hostname clients use (`rustdesk.seahaven.com`) |
|
||||||
|
|
||||||
|
The pinned Docker image tag lives in `lib/rustdesk-server-stack.ts` (`RUSTDESK_IMAGE_TAG`).
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
CI/CD runs through the reusable org workflows (`ci-typescript-cdk.yaml`, `cd-cdk.yaml`). Pushes to `main` deploy automatically.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm ci
|
||||||
|
npx cdk diff
|
||||||
|
npx cdk deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## First deploy
|
||||||
|
|
||||||
|
1. Confirm a **public** subnet ID in `us-east-1a` and set `PUBLIC_SUBNET_ID` in `lib/rustdesk-server-stack.ts` (replace `subnet-REPLACE_ME`).
|
||||||
|
2. Verify/pin `RUSTDESK_IMAGE_TAG`.
|
||||||
|
3. Create the OIDC deploy role `githubdeploy-rustdesk-server` and the repo secret `AWS_DEPLOY_ROLE_ARN`.
|
||||||
|
4. `cdk deploy` (or push to `main`). The instance boots, pulls the images, and `hbbs` generates the key pair on the empty data volume.
|
||||||
|
5. SSM in, read `/var/lib/rustdesk/id_ed25519{,.pub}`, store into `rustdesk/server-key-pair`.
|
||||||
|
6. Over the office VPN, open `http://rustdesk.seahaven.com:21114`, activate the Pro license, create users (the console is not reachable off-VPN).
|
||||||
|
7. Point a test client at `rustdesk.seahaven.com` + the public key; confirm a session relays.
|
||||||
|
|
||||||
|
## Operations
|
||||||
|
|
||||||
|
See [RUNBOOK.md](RUNBOOK.md) for key rotation, restore-from-snapshot, and instance replacement.
|
||||||
67
RUNBOOK.md
Normal file
67
RUNBOOK.md
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
# RustDesk Server — Runbook
|
||||||
|
|
||||||
|
Operational procedures for the `rustdesk-server` stack. Account 328440206208, us-east-1.
|
||||||
|
|
||||||
|
## Access
|
||||||
|
|
||||||
|
The instance is SSM-managed (no inbound SSH):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aws ssm start-session --target <instance-id> --region us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
|
Find the instance: `aws ec2 describe-instances --filters Name=tag:Name,Values=rustdesk-server`.
|
||||||
|
|
||||||
|
## The data volume (read this before any redeploy)
|
||||||
|
|
||||||
|
All durable state — the `id_ed25519` server key pair and the sled database — lives on the standalone EBS volume `rustdesk-data`, mounted at `/var/lib/rustdesk`, attached at `/dev/xvdf`.
|
||||||
|
|
||||||
|
- It is declared as a separate `ec2.Volume` with `RemovalPolicy.RETAIN`, **not** an inline `blockDevice`. This is deliberate: an inline data volume gets replaced whenever CloudFormation replaces the instance, which would destroy the server key and force **every** client to re-trust the host.
|
||||||
|
- `userdata/bootstrap.sh` mounts the existing filesystem and only runs `mkfs` when `blkid` reports the device is blank, so reattaching to a fresh instance preserves data.
|
||||||
|
- Before any change that may replace the instance (AMI bump, instance-type change), confirm a recent EBS snapshot exists and that the key pair is mirrored to `rustdesk/server-key-pair`.
|
||||||
|
|
||||||
|
## Instance replacement
|
||||||
|
|
||||||
|
A replaced instance reattaches the same RETAINed volume and remounts it without reformatting, so the server key and DB are preserved. After replacement:
|
||||||
|
|
||||||
|
1. Confirm `docker compose ps` shows `hbbs` and `hbbr` healthy (`cd /opt/rustdesk`).
|
||||||
|
2. Confirm clients still connect without re-accepting a new key.
|
||||||
|
|
||||||
|
## Server key pair
|
||||||
|
|
||||||
|
`hbbs` generates `/var/lib/rustdesk/id_ed25519` (private) and `id_ed25519.pub` (public) on first boot. The public key is the fingerprint clients must trust.
|
||||||
|
|
||||||
|
Mirror after first deploy / after any intentional rotation. Read the key with
|
||||||
|
`fileb://` so it never lands in process argv (`ps`/`/proc`), shell history, or the
|
||||||
|
terminal (which SSM session logging would capture):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo aws secretsmanager put-secret-value \
|
||||||
|
--secret-id rustdesk/server-key-pair \
|
||||||
|
--secret-string fileb:///var/lib/rustdesk/id_ed25519 \
|
||||||
|
--region us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
|
Run this from an environment that holds `secretsmanager:PutSecretValue` — the
|
||||||
|
instance role only has `GetSecretValue`, so either configure operator credentials
|
||||||
|
on the box first, or pull the key off the host over SSM (without echoing it) and
|
||||||
|
push it from your workstation. Never `cat` the private key to the terminal.
|
||||||
|
|
||||||
|
To **rotate** the key (forces all clients to re-trust): stop the containers, delete `id_ed25519*`, restart, re-mirror, redistribute the new public key.
|
||||||
|
|
||||||
|
## Restore from snapshot
|
||||||
|
|
||||||
|
1. Create a volume from the desired snapshot in `us-east-1a`.
|
||||||
|
2. Detach the current `rustdesk-data` volume (stop the instance first).
|
||||||
|
3. Attach the restored volume at `/dev/xvdf`, tag `Name=rustdesk-data`, start the instance.
|
||||||
|
4. Verify the key pair and DB are present and containers come up healthy.
|
||||||
|
|
||||||
|
## Pro license
|
||||||
|
|
||||||
|
Activated in the web console (`http://rustdesk.seahaven.com:21114`). Keep the key in `rustdesk/pro-license`.
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/rustdesk && docker compose logs -f hbbs hbbr
|
||||||
|
```
|
||||||
11
bin/app.ts
Normal file
11
bin/app.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
import "source-map-support/register";
|
||||||
|
import * as cdk from "aws-cdk-lib";
|
||||||
|
import { RustdeskServerStack } from "../lib/rustdesk-server-stack";
|
||||||
|
|
||||||
|
const app = new cdk.App();
|
||||||
|
|
||||||
|
new RustdeskServerStack(app, "rustdesk-server", {
|
||||||
|
stackName: "rustdesk-server",
|
||||||
|
env: { account: "328440206208", region: "us-east-1" },
|
||||||
|
});
|
||||||
52
cdk.context.json
Normal file
52
cdk.context.json
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"vpc-provider:account=328440206208:filter.vpc-id=vpc-0d3d4b67bd0cf8a68:region=us-east-1:returnAsymmetricSubnets=true": {
|
||||||
|
"vpcId": "vpc-0d3d4b67bd0cf8a68",
|
||||||
|
"vpcCidrBlock": "10.20.0.0/16",
|
||||||
|
"ownerAccountId": "328440206208",
|
||||||
|
"availabilityZones": [],
|
||||||
|
"vpnGatewayId": "vgw-073737d44762dffc2",
|
||||||
|
"subnetGroups": [
|
||||||
|
{
|
||||||
|
"name": "Private",
|
||||||
|
"type": "Private",
|
||||||
|
"subnets": [
|
||||||
|
{
|
||||||
|
"subnetId": "subnet-04e38c507e96f1926",
|
||||||
|
"cidr": "10.20.30.0/24",
|
||||||
|
"availabilityZone": "us-east-1a",
|
||||||
|
"routeTableId": "rtb-06a2f56f492b9b4de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"subnetId": "subnet-0a0b4fc6f296dfba5",
|
||||||
|
"cidr": "10.20.40.0/24",
|
||||||
|
"availabilityZone": "us-east-1b",
|
||||||
|
"routeTableId": "rtb-01e152fe5cabca7d6"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Public",
|
||||||
|
"type": "Public",
|
||||||
|
"subnets": [
|
||||||
|
{
|
||||||
|
"subnetId": "subnet-0eea820effe1b3ae5",
|
||||||
|
"cidr": "10.20.10.0/24",
|
||||||
|
"availabilityZone": "us-east-1a",
|
||||||
|
"routeTableId": "rtb-0f2232493a5c43fe8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"subnetId": "subnet-0012f5895182c1580",
|
||||||
|
"cidr": "10.20.20.0/24",
|
||||||
|
"availabilityZone": "us-east-1b",
|
||||||
|
"routeTableId": "rtb-0f2232493a5c43fe8"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ssm:account=328440206208:parameterName=/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64:region=us-east-1": "ami-06c84fbfd615657d3",
|
||||||
|
"hosted-zone:account=328440206208:domainName=seahaven.com:region=us-east-1": {
|
||||||
|
"Id": "/hostedzone/Z06652411XKH89KTZD3XA",
|
||||||
|
"Name": "seahaven.com."
|
||||||
|
}
|
||||||
|
}
|
||||||
21
cdk.json
Normal file
21
cdk.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"app": "npx ts-node bin/app.ts",
|
||||||
|
"watch": {
|
||||||
|
"include": ["**"],
|
||||||
|
"exclude": [
|
||||||
|
"README.md",
|
||||||
|
"cdk*.json",
|
||||||
|
"**/*.d.ts",
|
||||||
|
"**/*.js",
|
||||||
|
"tsconfig.json",
|
||||||
|
"package*.json",
|
||||||
|
"node_modules",
|
||||||
|
"cdk.out"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
|
||||||
|
"@aws-cdk/core:checkSecretUsage": true,
|
||||||
|
"@aws-cdk/core:target-partitions": ["aws"]
|
||||||
|
}
|
||||||
|
}
|
||||||
23
docker/docker-compose.yml
Normal file
23
docker/docker-compose.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Reference copy only. The authoritative compose file is rendered onto the
|
||||||
|
# instance by userdata/bootstrap.sh (with the pinned image tag injected from
|
||||||
|
# lib/rustdesk-server-stack.ts). Keep this in sync for documentation purposes.
|
||||||
|
# RustDesk Server Pro persists its key pair + DB to /root inside the container.
|
||||||
|
services:
|
||||||
|
hbbs:
|
||||||
|
container_name: hbbs
|
||||||
|
image: rustdesk/rustdesk-server-pro:1.8.4
|
||||||
|
command: hbbs
|
||||||
|
network_mode: host
|
||||||
|
volumes:
|
||||||
|
- /var/lib/rustdesk:/root
|
||||||
|
depends_on:
|
||||||
|
- hbbr
|
||||||
|
restart: unless-stopped
|
||||||
|
hbbr:
|
||||||
|
container_name: hbbr
|
||||||
|
image: rustdesk/rustdesk-server-pro:1.8.4
|
||||||
|
command: hbbr
|
||||||
|
network_mode: host
|
||||||
|
volumes:
|
||||||
|
- /var/lib/rustdesk:/root
|
||||||
|
restart: unless-stopped
|
||||||
227
lib/rustdesk-server-stack.ts
Normal file
227
lib/rustdesk-server-stack.ts
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import * as cdk from "aws-cdk-lib";
|
||||||
|
import * as ec2 from "aws-cdk-lib/aws-ec2";
|
||||||
|
import * as iam from "aws-cdk-lib/aws-iam";
|
||||||
|
import * as route53 from "aws-cdk-lib/aws-route53";
|
||||||
|
import * as dlm from "aws-cdk-lib/aws-dlm";
|
||||||
|
import { Construct } from "constructs";
|
||||||
|
|
||||||
|
// Pinned RustDesk Server Pro image tag. Do NOT use "latest" (handbook: pin
|
||||||
|
// container versions). Confirm the current Pro tag before the first deploy and
|
||||||
|
// bump deliberately. https://hub.docker.com/r/rustdesk/rustdesk-server-pro/tags
|
||||||
|
const RUSTDESK_IMAGE_TAG = "1.8.4"; // multi-arch tag; resolves to arm64v8 on t4g
|
||||||
|
|
||||||
|
// Existing Sea Haven VPC (same account/region as forgejo et al.).
|
||||||
|
const VPC_ID = "vpc-0d3d4b67bd0cf8a68";
|
||||||
|
|
||||||
|
// RustDesk clients connect from anywhere, so the host sits in a public subnet
|
||||||
|
// (0.0.0.0/0 -> igw-011688a85a5474db2) with an Elastic IP. The data volume AZ
|
||||||
|
// must match the instance AZ.
|
||||||
|
const PUBLIC_SUBNET_ID = "subnet-0eea820effe1b3ae5"; // seahaven-subnet-public1-us-east-1a
|
||||||
|
const AVAILABILITY_ZONE = "us-east-1a";
|
||||||
|
|
||||||
|
// Public DNS name embedded in client configs.
|
||||||
|
const HOSTED_ZONE_NAME = "seahaven.com";
|
||||||
|
const RECORD_NAME = "rustdesk.seahaven.com";
|
||||||
|
|
||||||
|
// Internal trusted ranges for the admin/management plane.
|
||||||
|
const OFFICE_VPN_CIDR = "10.10.0.0/16";
|
||||||
|
const VPC_CIDR = "10.20.0.0/16";
|
||||||
|
|
||||||
|
// RustDesk Server Pro listening ports. (network_mode: host on the containers,
|
||||||
|
// so the security group is the only access control.) `public: true` opens the
|
||||||
|
// port to the internet (relay/rendezvous ports clients reach from anywhere);
|
||||||
|
// `public: false` restricts it to the office VPN + VPC (the admin console is a
|
||||||
|
// management plane and must not be internet-facing).
|
||||||
|
interface PortSpec {
|
||||||
|
port: number;
|
||||||
|
protocol: "tcp" | "udp";
|
||||||
|
desc: string;
|
||||||
|
public: boolean;
|
||||||
|
}
|
||||||
|
const RUSTDESK_PORTS: PortSpec[] = [
|
||||||
|
{ port: 21114, protocol: "tcp", desc: "Pro web console / API", public: false },
|
||||||
|
{ port: 21115, protocol: "tcp", desc: "hbbs NAT type test", public: true },
|
||||||
|
{ port: 21116, protocol: "tcp", desc: "hbbs registration / TCP hole punch", public: true },
|
||||||
|
{ port: 21116, protocol: "udp", desc: "hbbs registration / heartbeat", public: true },
|
||||||
|
{ port: 21117, protocol: "tcp", desc: "hbbr relay", public: true },
|
||||||
|
{ port: 21118, protocol: "tcp", desc: "hbbs web client (websocket)", public: true },
|
||||||
|
{ port: 21119, protocol: "tcp", desc: "hbbr web client (websocket)", public: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
export class RustdeskServerStack extends cdk.Stack {
|
||||||
|
constructor(scope: Construct, id: string, props: cdk.StackProps) {
|
||||||
|
super(scope, id, props);
|
||||||
|
|
||||||
|
const vpc = ec2.Vpc.fromLookup(this, "SeaHavenVpc", { vpcId: VPC_ID });
|
||||||
|
|
||||||
|
const publicSubnet = ec2.Subnet.fromSubnetAttributes(this, "PublicSubnet", {
|
||||||
|
subnetId: PUBLIC_SUBNET_ID,
|
||||||
|
availabilityZone: AVAILABILITY_ZONE,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Security group ──────────────────────────────────────────────
|
||||||
|
// Relay/rendezvous ports are public (clients connect from anywhere). The
|
||||||
|
// Pro admin console (21114) is restricted to the office VPN + VPC. To take
|
||||||
|
// the relay VPN-only later, flip the `public` flags in RUSTDESK_PORTS.
|
||||||
|
const sg = new ec2.SecurityGroup(this, "SecurityGroup", {
|
||||||
|
vpc,
|
||||||
|
securityGroupName: "rustdesk-server",
|
||||||
|
description: "RustDesk Server Pro - public relay; VPN-only admin console",
|
||||||
|
allowAllOutbound: true,
|
||||||
|
});
|
||||||
|
for (const p of RUSTDESK_PORTS) {
|
||||||
|
const port =
|
||||||
|
p.protocol === "tcp" ? ec2.Port.tcp(p.port) : ec2.Port.udp(p.port);
|
||||||
|
if (p.public) {
|
||||||
|
sg.addIngressRule(ec2.Peer.anyIpv4(), port, p.desc);
|
||||||
|
} else {
|
||||||
|
sg.addIngressRule(ec2.Peer.ipv4(OFFICE_VPN_CIDR), port, `${p.desc} (office VPN)`);
|
||||||
|
sg.addIngressRule(ec2.Peer.ipv4(VPC_CIDR), port, `${p.desc} (VPC)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Instance role (SSM-managed; no inbound SSH) ─────────────────
|
||||||
|
const role = new iam.Role(this, "InstanceRole", {
|
||||||
|
roleName: "rustdesk-server-instance",
|
||||||
|
assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
|
||||||
|
managedPolicies: [
|
||||||
|
iam.ManagedPolicy.fromAwsManagedPolicyName(
|
||||||
|
"AmazonSSMManagedInstanceCore",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
role.addToPolicy(
|
||||||
|
new iam.PolicyStatement({
|
||||||
|
actions: ["secretsmanager:GetSecretValue"],
|
||||||
|
resources: [
|
||||||
|
"arn:aws:secretsmanager:us-east-1:328440206208:secret:rustdesk/*",
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── User data ───────────────────────────────────────────────────
|
||||||
|
const bootstrap = fs
|
||||||
|
.readFileSync(path.join(__dirname, "..", "userdata", "bootstrap.sh"), "utf8")
|
||||||
|
.replace(/__RUSTDESK_IMAGE_TAG__/g, RUSTDESK_IMAGE_TAG);
|
||||||
|
const userData = ec2.UserData.custom(bootstrap);
|
||||||
|
|
||||||
|
// ── Instance ────────────────────────────────────────────────────
|
||||||
|
const instance = new ec2.Instance(this, "Instance", {
|
||||||
|
instanceName: "rustdesk-server",
|
||||||
|
vpc,
|
||||||
|
vpcSubnets: { subnets: [publicSubnet] },
|
||||||
|
instanceType: ec2.InstanceType.of(
|
||||||
|
ec2.InstanceClass.T4G,
|
||||||
|
ec2.InstanceSize.SMALL,
|
||||||
|
),
|
||||||
|
machineImage: ec2.MachineImage.latestAmazonLinux2023({
|
||||||
|
cpuType: ec2.AmazonLinuxCpuType.ARM_64,
|
||||||
|
// Cache the resolved AMI in cdk.context.json so deploys don't pick up
|
||||||
|
// new AL2023 releases implicitly (AMI change forces instance
|
||||||
|
// replacement). Refresh deliberately:
|
||||||
|
// cdk context --reset <ami key> && cdk synth
|
||||||
|
cachedInContext: true,
|
||||||
|
}),
|
||||||
|
securityGroup: sg,
|
||||||
|
role,
|
||||||
|
userData,
|
||||||
|
// Force IMDSv2 (token-required) so the instance role credentials can't be
|
||||||
|
// lifted via a tokenless IMDSv1 request from a host-network container.
|
||||||
|
requireImdsv2: true,
|
||||||
|
// OS-only root volume. ALL durable state lives on the standalone data
|
||||||
|
// volume below, never an inline blockDevice (see RUNBOOK + the EBS
|
||||||
|
// replacement gotcha).
|
||||||
|
blockDevices: [
|
||||||
|
{
|
||||||
|
deviceName: "/dev/xvda",
|
||||||
|
volume: ec2.BlockDeviceVolume.ebs(20, {
|
||||||
|
volumeType: ec2.EbsDeviceVolumeType.GP3,
|
||||||
|
encrypted: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
cdk.Tags.of(instance).add("rustdesk-backup", "true");
|
||||||
|
|
||||||
|
// ── Persistent data volume ──────────────────────────────────────
|
||||||
|
// RustDesk key pair (id_ed25519*) + sled DB live here, mounted at
|
||||||
|
// /var/lib/rustdesk. Standalone Volume + RETAIN means the data survives
|
||||||
|
// instance replacement AND stack deletion; userdata mounts the existing
|
||||||
|
// filesystem (blkid guard prevents reformatting). NEVER move this into the
|
||||||
|
// instance's inline blockDevices: an inline data volume is replaced
|
||||||
|
// whenever CFN replaces the instance, destroying the server key and
|
||||||
|
// forcing every client to re-trust the host.
|
||||||
|
const dataVolume = new ec2.Volume(this, "DataVolume", {
|
||||||
|
availabilityZone: AVAILABILITY_ZONE,
|
||||||
|
size: cdk.Size.gibibytes(20),
|
||||||
|
volumeType: ec2.EbsDeviceVolumeType.GP3,
|
||||||
|
encrypted: true,
|
||||||
|
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
||||||
|
});
|
||||||
|
cdk.Tags.of(dataVolume).add("Name", "rustdesk-data");
|
||||||
|
cdk.Tags.of(dataVolume).add("rustdesk-backup", "true");
|
||||||
|
|
||||||
|
new ec2.CfnVolumeAttachment(this, "DataVolumeAttachment", {
|
||||||
|
instanceId: instance.instanceId,
|
||||||
|
volumeId: dataVolume.volumeId,
|
||||||
|
device: "/dev/xvdf",
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Elastic IP (stable client-facing address) ───────────────────
|
||||||
|
const eip = new ec2.CfnEIP(this, "Eip", {
|
||||||
|
domain: "vpc",
|
||||||
|
instanceId: instance.instanceId,
|
||||||
|
tags: [{ key: "Name", value: "rustdesk-server" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── DNS ─────────────────────────────────────────────────────────
|
||||||
|
const zone = route53.HostedZone.fromLookup(this, "Zone", {
|
||||||
|
domainName: HOSTED_ZONE_NAME,
|
||||||
|
});
|
||||||
|
new route53.ARecord(this, "ARecord", {
|
||||||
|
zone,
|
||||||
|
recordName: RECORD_NAME,
|
||||||
|
target: route53.RecordTarget.fromIpAddresses(eip.ref),
|
||||||
|
ttl: cdk.Duration.minutes(5),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Nightly EBS snapshots ───────────────────────────────────────
|
||||||
|
const dlmRole = new iam.Role(this, "DlmRole", {
|
||||||
|
roleName: "rustdesk-server-dlm",
|
||||||
|
assumedBy: new iam.ServicePrincipal("dlm.amazonaws.com"),
|
||||||
|
managedPolicies: [
|
||||||
|
iam.ManagedPolicy.fromAwsManagedPolicyName(
|
||||||
|
"service-role/AWSDataLifecycleManagerServiceRole",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
new dlm.CfnLifecyclePolicy(this, "SnapshotPolicy", {
|
||||||
|
description: "Nightly EBS snapshots for RustDesk server",
|
||||||
|
state: "ENABLED",
|
||||||
|
executionRoleArn: dlmRole.roleArn,
|
||||||
|
policyDetails: {
|
||||||
|
resourceTypes: ["INSTANCE"],
|
||||||
|
targetTags: [{ key: "rustdesk-backup", value: "true" }],
|
||||||
|
schedules: [
|
||||||
|
{
|
||||||
|
name: "rustdesk-nightly",
|
||||||
|
createRule: {
|
||||||
|
interval: 24,
|
||||||
|
intervalUnit: "HOURS",
|
||||||
|
times: ["06:00"],
|
||||||
|
},
|
||||||
|
retainRule: { count: 30 },
|
||||||
|
copyTags: true,
|
||||||
|
tagsToAdd: [{ key: "rustdesk-backup", value: "true" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Outputs ─────────────────────────────────────────────────────
|
||||||
|
new cdk.CfnOutput(this, "PublicIp", { value: eip.ref });
|
||||||
|
new cdk.CfnOutput(this, "Hostname", { value: RECORD_NAME });
|
||||||
|
}
|
||||||
|
}
|
||||||
521
package-lock.json
generated
Normal file
521
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,521 @@
|
||||||
|
{
|
||||||
|
"name": "rustdesk-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "rustdesk-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"aws-cdk-lib": "2.260.0",
|
||||||
|
"constructs": "^10.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"app": "bin/app.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.9.3",
|
||||||
|
"aws-cdk": "^2.1128.1",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"typescript": "~6.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-cdk/asset-awscli-v1": {
|
||||||
|
"version": "2.2.282",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.282.tgz",
|
||||||
|
"integrity": "sha512-7hKMi5tTxDcKGIMIOq14PnY0GBcugW33Uh/2YHDZiEwSxLeFOCYBwhR+BFXONb/EJeVI3RETFgailNZbkcKF6g==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@aws-cdk/asset-node-proxy-agent-v6": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-pDiuqH+qY3zM9lhhLjbKJ1tnKOHzQ2V4Wr/3qsxyKeKAkuPMI/BVGvZG1PbrikUw949cGVTfVEt4ETKKYnrj0Q==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@aws-cdk/cloud-assembly-schema": {
|
||||||
|
"version": "54.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-54.5.0.tgz",
|
||||||
|
"integrity": "sha512-X37oRfMQYO/wXBBDotbW8msJ6AgrcMio/W6TpDR/9To9TUWld1KtY/jveHpU58nZyLVPTYEhDgFP548w3JJGsQ==",
|
||||||
|
"bundleDependencies": [
|
||||||
|
"jsonschema",
|
||||||
|
"semver"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"jsonschema": "^1.5.0",
|
||||||
|
"semver": "^7.8.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": {
|
||||||
|
"version": "7.8.4",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "25.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.4.tgz",
|
||||||
|
"integrity": "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": ">=7.24.0 <7.24.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk": {
|
||||||
|
"version": "2.1128.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1128.1.tgz",
|
||||||
|
"integrity": "sha512-y9OHn5/BOcIiq409vPvpypMIr7/8M1ScFe8IkFMSCN1/GI/5c73fQ4pfzNq+VDkj86T5zxs7BQ1qU2lQQytdXA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"cdk": "bin/cdk"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib": {
|
||||||
|
"version": "2.260.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.260.0.tgz",
|
||||||
|
"integrity": "sha512-2PPG+hbPDot8+ibkb5Jl9y3OY5rBE6TFwjzOi+yEyU4ZG6u8bM4DDKhhBi/S20NqqSFDso9rH1txVJAdwXNiuQ==",
|
||||||
|
"bundleDependencies": [
|
||||||
|
"@balena/dockerignore",
|
||||||
|
"@aws-cdk/cloud-assembly-api",
|
||||||
|
"case",
|
||||||
|
"fs-extra",
|
||||||
|
"ignore",
|
||||||
|
"jsonschema",
|
||||||
|
"minimatch",
|
||||||
|
"punycode",
|
||||||
|
"semver",
|
||||||
|
"table",
|
||||||
|
"yaml",
|
||||||
|
"mime-types"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-cdk/asset-awscli-v1": "2.2.282",
|
||||||
|
"@aws-cdk/asset-node-proxy-agent-v6": "^2.1.2",
|
||||||
|
"@aws-cdk/cloud-assembly-api": "^2.2.5",
|
||||||
|
"@aws-cdk/cloud-assembly-schema": "^54.0.0",
|
||||||
|
"@balena/dockerignore": "^1.0.2",
|
||||||
|
"case": "1.6.3",
|
||||||
|
"fs-extra": "^11.3.5",
|
||||||
|
"ignore": "^5.3.2",
|
||||||
|
"jsonschema": "^1.5.0",
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
|
"minimatch": "^10.2.5",
|
||||||
|
"punycode": "^2.3.1",
|
||||||
|
"semver": "^7.8.1",
|
||||||
|
"table": "^6.9.0",
|
||||||
|
"yaml": "1.10.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"constructs": "^10.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": {
|
||||||
|
"version": "2.2.5",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"jsonschema": "^1.5.0",
|
||||||
|
"semver": "^7.8.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@aws-cdk/cloud-assembly-schema": ">=53.28.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/ajv": {
|
||||||
|
"version": "8.20.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fast-uri": "^3.0.1",
|
||||||
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/astral-regex": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/balanced-match": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/brace-expansion": {
|
||||||
|
"version": "5.0.6",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/case": {
|
||||||
|
"version": "1.6.3",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "(MIT OR GPL-3.0-or-later)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/fast-deep-equal": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/fast-uri": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/fs-extra": {
|
||||||
|
"version": "11.3.5",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^6.0.1",
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/graceful-fs": {
|
||||||
|
"version": "4.2.11",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/ignore": {
|
||||||
|
"version": "5.3.2",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/json-schema-traverse": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/jsonfile": {
|
||||||
|
"version": "6.2.1",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/jsonschema": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/lodash.truncate": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/minimatch": {
|
||||||
|
"version": "10.2.5",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^5.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/punycode": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/require-from-string": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/semver": {
|
||||||
|
"version": "7.8.1",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/slice-ansi": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"astral-regex": "^2.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/table": {
|
||||||
|
"version": "6.9.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": "^8.0.1",
|
||||||
|
"lodash.truncate": "^4.4.2",
|
||||||
|
"slice-ansi": "^4.0.0",
|
||||||
|
"string-width": "^4.2.3",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/universalify": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/yaml": {
|
||||||
|
"version": "1.10.3",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/buffer-from": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/constructs": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/constructs/-/constructs-10.6.0.tgz",
|
||||||
|
"integrity": "sha512-TxHOnBO5zMo/G76ykzGF/wMpEHu257TbWiIxP9K0Yv/+t70UzgBQiTqjkAsWOPC6jW91DzJI0+ehQV6xDRNBuQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/source-map-support": {
|
||||||
|
"version": "0.5.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||||
|
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-from": "^1.0.0",
|
||||||
|
"source-map": "^0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "7.24.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||||
|
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
package.json
Normal file
24
package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "rustdesk-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"bin": {
|
||||||
|
"app": "bin/app.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"cdk": "cdk",
|
||||||
|
"synth": "cdk synth",
|
||||||
|
"deploy": "cdk deploy",
|
||||||
|
"diff": "cdk diff"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.9.3",
|
||||||
|
"aws-cdk": "^2.1128.1",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"typescript": "~6.0.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"aws-cdk-lib": "2.260.0",
|
||||||
|
"constructs": "^10.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"types": ["node"],
|
||||||
|
"declaration": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"inlineSourceMap": true,
|
||||||
|
"inlineSources": true,
|
||||||
|
"strictPropertyInitialization": false,
|
||||||
|
"outDir": "./cdk.out",
|
||||||
|
"rootDir": ".",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "cdk.out"]
|
||||||
|
}
|
||||||
70
userdata/bootstrap.sh
Normal file
70
userdata/bootstrap.sh
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# RustDesk Server Pro bootstrap (Amazon Linux 2023, ARM64).
|
||||||
|
# Rendered by lib/rustdesk-server-stack.ts; __RUSTDESK_IMAGE_TAG__ is replaced
|
||||||
|
# at synth time. Idempotent: safe to re-run on instance replacement.
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
RUSTDESK_DATA=/var/lib/rustdesk
|
||||||
|
RUSTDESK_IMAGE="rustdesk/rustdesk-server-pro:__RUSTDESK_IMAGE_TAG__"
|
||||||
|
|
||||||
|
# ── Persistent data volume (attached at /dev/xvdf via CfnVolumeAttachment;
|
||||||
|
# surfaces as an nvme device on Nitro). Wait for it, then mount WITHOUT
|
||||||
|
# reformatting if it already holds a filesystem (preserves the server key). ──
|
||||||
|
until lsblk -dno NAME | grep -v "$(lsblk -no PKNAME "$(findmnt -n -o SOURCE /)" 2>/dev/null || echo xvda)" | grep -q .; do
|
||||||
|
echo 'Waiting for data volume...'
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
DATA_DEVICE=/dev/$(lsblk -dno NAME | grep -v "$(lsblk -no PKNAME "$(findmnt -n -o SOURCE /)" 2>/dev/null || echo xvda)" | head -1)
|
||||||
|
if ! blkid "$DATA_DEVICE"; then
|
||||||
|
mkfs.ext4 -L rustdesk-data "$DATA_DEVICE"
|
||||||
|
fi
|
||||||
|
mkdir -p "$RUSTDESK_DATA"
|
||||||
|
grep -q 'LABEL=rustdesk-data' /etc/fstab || \
|
||||||
|
echo "LABEL=rustdesk-data $RUSTDESK_DATA ext4 defaults,nofail 0 2" >> /etc/fstab
|
||||||
|
mount -a
|
||||||
|
# Restrict the data dir so the server private key (id_ed25519, created inside the
|
||||||
|
# container at /root) is not readable by any non-root host user even if the
|
||||||
|
# container writes it world-readable.
|
||||||
|
chmod 700 "$RUSTDESK_DATA"
|
||||||
|
|
||||||
|
# ── Docker + compose plugin ──────────────────────────────────────────
|
||||||
|
dnf install -y docker
|
||||||
|
systemctl enable --now docker
|
||||||
|
DOCKER_CLI_PLUGINS=/usr/local/lib/docker/cli-plugins
|
||||||
|
mkdir -p "$DOCKER_CLI_PLUGINS"
|
||||||
|
curl -fsSL "https://github.com/docker/compose/releases/download/v2.29.7/docker-compose-linux-aarch64" \
|
||||||
|
-o "$DOCKER_CLI_PLUGINS/docker-compose"
|
||||||
|
chmod +x "$DOCKER_CLI_PLUGINS/docker-compose"
|
||||||
|
|
||||||
|
# ── Compose definition (host networking; SG is the access control) ───
|
||||||
|
mkdir -p /opt/rustdesk
|
||||||
|
cat > /opt/rustdesk/docker-compose.yml << COMPOSE
|
||||||
|
services:
|
||||||
|
hbbs:
|
||||||
|
container_name: hbbs
|
||||||
|
image: ${RUSTDESK_IMAGE}
|
||||||
|
command: hbbs
|
||||||
|
network_mode: host
|
||||||
|
volumes:
|
||||||
|
- ${RUSTDESK_DATA}:/root
|
||||||
|
depends_on:
|
||||||
|
- hbbr
|
||||||
|
restart: unless-stopped
|
||||||
|
hbbr:
|
||||||
|
container_name: hbbr
|
||||||
|
image: ${RUSTDESK_IMAGE}
|
||||||
|
command: hbbr
|
||||||
|
network_mode: host
|
||||||
|
volumes:
|
||||||
|
- ${RUSTDESK_DATA}:/root
|
||||||
|
restart: unless-stopped
|
||||||
|
COMPOSE
|
||||||
|
|
||||||
|
cd /opt/rustdesk
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Activate the Pro license and configure users in the web console at
|
||||||
|
# http://rustdesk.seahaven.com:21114 after first boot. The generated key pair
|
||||||
|
# lives at $RUSTDESK_DATA/id_ed25519{,.pub} -- mirror it into the
|
||||||
|
# rustdesk/server-key-pair secret (see RUNBOOK).
|
||||||
Reference in a new issue