mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
feat(multi-node): warn when configuring remote node with plain HTTP URL (#292)
Show an inline warning banner in the Add Node form when the user enters an http:// URL, recommending HTTPS or VPN for public internet connections. HTTP remains fully supported for private networks (LAN, VPN, VPC). Also returns an optional `warning` field in POST/PUT /api/nodes responses for API-only consumers, and expands the multi-node security documentation with concrete deployment guidance (reverse proxy, VPN, private network).
This commit is contained in:
@@ -34,6 +34,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
* **scheduled-ops:** prune label filter — scheduled system prune operations can be scoped to resources matching a specific Docker label (e.g. `com.docker.compose.project=mystack`)
|
||||
* **scheduled-ops:** CSV export for execution history — one-click download of the full run history from the execution history panel
|
||||
|
||||
### Changed
|
||||
|
||||
* **multi-node:** show inline warning when configuring a remote node with an HTTP URL — recommends HTTPS or VPN for public internet connections; HTTP remains fully supported for private networks
|
||||
|
||||
### Fixed
|
||||
|
||||
* **fleet:** "Open in Editor" button in Fleet View's stack drill-down now correctly navigates to the stack editor instead of the dashboard. Also passes the stack name through the callback chain so the correct file is loaded.
|
||||
|
||||
+15
-2
@@ -4493,7 +4493,14 @@ app.post('/api/nodes', async (req: Request, res: Response) => {
|
||||
api_token: api_token || '',
|
||||
});
|
||||
|
||||
res.json({ success: true, id });
|
||||
const isPlainHttp = type === 'remote' && api_url && api_url.startsWith('http://');
|
||||
res.json({
|
||||
success: true,
|
||||
id,
|
||||
...(isPlainHttp && {
|
||||
warning: 'This node uses plain HTTP. Use HTTPS or a VPN for connections over the public internet.'
|
||||
})
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes('UNIQUE constraint')) {
|
||||
return res.status(409).json({ error: 'A node with that name already exists' });
|
||||
@@ -4527,7 +4534,13 @@ app.put('/api/nodes/:id', async (req: Request, res: Response) => {
|
||||
// Evict cached Docker connection so it reconnects with new config
|
||||
NodeRegistry.getInstance().evictConnection(id);
|
||||
|
||||
res.json({ success: true });
|
||||
const isPlainHttp = updates.api_url && updates.api_url.startsWith('http://');
|
||||
res.json({
|
||||
success: true,
|
||||
...(isPlainHttp && {
|
||||
warning: 'This node uses plain HTTP. Use HTTPS or a VPN for connections over the public internet.'
|
||||
})
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to update node:', error);
|
||||
res.status(500).json({ error: error.message || 'Failed to update node' });
|
||||
|
||||
@@ -77,8 +77,94 @@ Node status indicators:
|
||||
|
||||
Click the **pencil icon** on any remote node row to edit its name, URL, or token. Click the **trash icon** to remove it. The local node cannot be edited or deleted.
|
||||
|
||||
## Security considerations
|
||||
## Security
|
||||
|
||||
- Node tokens grant full control over the remote Sencho instance. Rotate them if compromised via Settings → Nodes → Generate Token (the old token is invalidated).
|
||||
- Use HTTPS between instances in production to prevent token interception.
|
||||
- The host console and container exec terminals are blocked for node-proxy tokens - interactive shell access always requires a real browser session on that instance.
|
||||
### Token security
|
||||
|
||||
Node tokens are long-lived JWTs that grant full control over the remote Sencho instance. Treat them like passwords:
|
||||
|
||||
- **Rotate immediately** if a token is compromised: Settings → Nodes → Generate Token on the remote instance. The old token is invalidated instantly.
|
||||
- Tokens are **encrypted at rest** using AES-256-GCM in Sencho's SQLite database. Even if the database file is extracted, the tokens cannot be read without the instance's encryption key.
|
||||
- Tokens are scoped to `node_proxy` — they cannot access the host console or container exec terminals. Interactive shell access always requires a real browser session on that specific instance.
|
||||
|
||||
### Transport encryption
|
||||
|
||||
Sencho delegates transport encryption to your infrastructure rather than implementing TLS at the application layer. This is the same approach used by [Dockge](https://github.com/louislam/dockge) and other self-hosted tools — it avoids certificate management burden while letting you use the encryption layer that best fits your environment.
|
||||
|
||||
<Warning>
|
||||
**Never send node tokens over plain HTTP across the public internet.** A token intercepted in transit grants full control of the remote instance. Always use one of the approaches below when nodes communicate over untrusted networks.
|
||||
</Warning>
|
||||
|
||||
Sencho shows an inline warning when you enter an HTTP URL in the Add Node form as a reminder:
|
||||
|
||||
<Frame>
|
||||
<img src="/images/multi-node/http-warning.png" alt="Add Node form showing an inline warning when an HTTP URL is entered" />
|
||||
</Frame>
|
||||
|
||||
There are three recommended approaches depending on your deployment:
|
||||
|
||||
#### Private network (LAN or VPC)
|
||||
|
||||
If all your Sencho instances are on the same local network, VPC, or subnet — HTTP is perfectly fine. The token never leaves the private network, so there is no interception risk.
|
||||
|
||||
```
|
||||
http://192.168.1.50:3000 ← safe on a private LAN
|
||||
http://10.0.1.20:3000 ← safe inside a VPC
|
||||
```
|
||||
|
||||
#### VPN tunnel (WireGuard, Tailscale)
|
||||
|
||||
<Tip>
|
||||
This is the simplest approach for connecting nodes across the internet. A VPN encrypts all traffic between your servers at the network layer — no certificate management required.
|
||||
</Tip>
|
||||
|
||||
With a mesh VPN like [Tailscale](https://tailscale.com) or [WireGuard](https://www.wireguard.com/), each server gets a private IP on the VPN. Use those IPs as your Sencho API URLs:
|
||||
|
||||
```
|
||||
http://100.64.0.2:3000 ← Tailscale IP, encrypted by the VPN tunnel
|
||||
```
|
||||
|
||||
All traffic between nodes is encrypted by the VPN — Sencho does not need to do anything additional.
|
||||
|
||||
#### Reverse proxy (Caddy, Nginx, Traefik)
|
||||
|
||||
If you prefer TLS termination at each node, place a reverse proxy in front of each Sencho instance. [Caddy](https://caddyserver.com/) is the simplest option — it auto-provisions HTTPS certificates from Let's Encrypt with zero configuration:
|
||||
|
||||
<CodeGroup>
|
||||
```text Caddyfile
|
||||
sencho.example.com {
|
||||
reverse_proxy localhost:3000
|
||||
}
|
||||
```
|
||||
|
||||
```nginx nginx.conf
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name sencho.example.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/sencho.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/sencho.example.com/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Then use the HTTPS URL when adding the remote node:
|
||||
|
||||
```
|
||||
https://sencho.example.com ← TLS terminated by Caddy/Nginx
|
||||
```
|
||||
|
||||
### Why Sencho doesn't implement application-layer TLS
|
||||
|
||||
Some tools auto-generate self-signed TLS certificates between their server and agents (e.g. Portainer's standard agent). In practice, this provides minimal security benefit — the certificates are self-signed with no verification, meaning they don't protect against man-in-the-middle attacks. It is effectively security theater.
|
||||
|
||||
Sencho takes a deliberate approach: infrastructure-level encryption (VPN, reverse proxy, or private networking) is more robust, easier to manage, and doesn't impose certificate rotation burden on the user. This is the same model used by Dockge, Yacht, and other self-hosted orchestrators.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
@@ -13,7 +13,7 @@ import { Separator } from './ui/separator';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||
import { Plus, Trash2, Wifi, WifiOff, Star, Pencil, Server, Monitor, Globe, Copy, KeyRound, Check } from 'lucide-react';
|
||||
import { Plus, Trash2, Wifi, WifiOff, Star, Pencil, Server, Monitor, Globe, Copy, KeyRound, Check, AlertTriangle } from 'lucide-react';
|
||||
|
||||
interface NodeFormData {
|
||||
name: string;
|
||||
@@ -273,6 +273,17 @@ export function NodeManager() {
|
||||
<p className="text-xs text-muted-foreground">
|
||||
The base URL of the Sencho instance running on the remote machine.
|
||||
</p>
|
||||
{formData.api_url.startsWith('http://') && (
|
||||
<div className="rounded-xl border border-warning/30 bg-warning/5 p-3 mt-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="w-4 h-4 text-warning shrink-0 mt-0.5" strokeWidth={1.5} />
|
||||
<p className="text-xs text-warning">
|
||||
This URL uses plain HTTP. If this node is reachable over the public internet, use HTTPS
|
||||
or a VPN to prevent token interception. HTTP is fine for private networks (LAN, VPN, VPC).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
|
||||
Reference in New Issue
Block a user