mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 15:46:35 +00:00
fix(deploy): require proxied local services
Bind native backend services and Node-RED to loopback so Angie remains the public auth boundary. Keep Docker on an internal compose network and stop kiosk fallback to a layout when display default is none.
This commit is contained in:
parent
026325ccd0
commit
96d7cc45ba
14 changed files with 147 additions and 27 deletions
|
|
@ -5,14 +5,15 @@
|
|||
### Server
|
||||
|
||||
```bash
|
||||
# Install Node.js 23
|
||||
# Install Node.js 23 + Node-RED
|
||||
curl -fsSL https://deb.nodesource.com/setup_23.x | sudo bash -
|
||||
sudo apt install -y nodejs build-essential
|
||||
sudo npm install -g --unsafe-perm node-red
|
||||
|
||||
# Create user + dirs
|
||||
sudo useradd -r -m -d /var/lib/betterframe betterframe
|
||||
sudo mkdir -p /opt/betterframe /var/log/betterframe /etc/betterframe
|
||||
sudo chown betterframe:betterframe /var/lib/betterframe /var/log/betterframe
|
||||
sudo mkdir -p /opt/betterframe /var/log/betterframe /etc/betterframe /var/lib/betterframe/nodered
|
||||
sudo chown -R betterframe:betterframe /var/lib/betterframe /var/log/betterframe
|
||||
|
||||
# Deploy code
|
||||
sudo git clone https://github.com/BetterCorp/BetterFrame.git /opt/betterframe
|
||||
|
|
@ -21,12 +22,18 @@ sudo -u betterframe npm install
|
|||
sudo -u betterframe npm run build
|
||||
sudo cp sec-config.yaml /opt/betterframe/server/sec-config.yaml
|
||||
|
||||
# Install systemd unit
|
||||
# Install systemd units
|
||||
sudo cp deploy/systemd/betterframe-server.service /etc/systemd/system/
|
||||
sudo cp deploy/systemd/betterframe-nodered.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now betterframe-server
|
||||
sudo systemctl enable --now betterframe-server betterframe-nodered
|
||||
```
|
||||
|
||||
The native config binds BetterFrame service ports and Node-RED to `127.0.0.1`.
|
||||
Do not expose ports `18080`, `18081`, `18082`, or `1880` directly on the LAN.
|
||||
Use Angie/nginx as the public entry point so `/nrdp/`, `/in/kiosk/`, and the
|
||||
admin routes get the auth protections in `deploy/angie/betterframe.conf`.
|
||||
|
||||
### Kiosk
|
||||
|
||||
```bash
|
||||
|
|
@ -63,6 +70,9 @@ sudo systemctl reload angie
|
|||
The Angie config gates `/nrdp/*` with the admin session/API-key auth-check
|
||||
endpoint and `/in/kiosk/*` with the kiosk Bearer-key auth-check endpoint.
|
||||
|
||||
Access: `http://<pi-ip>/setup` for first-run. Kiosks should use the proxy URL
|
||||
(`http://<pi-ip>` or `http://betterframe.local`), not direct backend ports.
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
|
|
@ -70,8 +80,9 @@ docker compose -f deploy/docker/docker-compose.yml up -d
|
|||
```
|
||||
|
||||
Kiosk still runs natively on the Pi (needs Wayland/HDMI), not in Docker.
|
||||
The Compose stack uses `deploy/angie/betterframe.docker.conf` because service
|
||||
names, not `127.0.0.1`, are the correct upstreams inside the Docker network.
|
||||
The Compose stack uses `deploy/angie/betterframe.docker.conf` and
|
||||
`deploy/docker/sec-config.yaml` because service names, not `127.0.0.1`, are the
|
||||
correct upstreams inside the Docker network.
|
||||
|
||||
Access: `http://<pi-ip>/setup` for first-run.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
# Place in /etc/angie/conf.d/betterframe.conf or /etc/nginx/conf.d/betterframe.conf
|
||||
# Run on the Pi alongside the server. TLS termination here; backend services
|
||||
# bind to 0.0.0.0 but firewall should restrict to localhost in production.
|
||||
# bind to 127.0.0.1; Angie/nginx is the only public HTTP edge.
|
||||
|
||||
# Upstreams (BSB services)
|
||||
upstream betterframe_admin { server 127.0.0.1:18080; keepalive 16; }
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ services:
|
|||
restart: unless-stopped
|
||||
volumes:
|
||||
- betterframe-data:/var/lib/betterframe
|
||||
- ../../sec-config.yaml:/app/server/sec-config.yaml:ro
|
||||
- ./sec-config.yaml:/app/server/sec-config.yaml:ro
|
||||
expose:
|
||||
- "18080"
|
||||
- "18081"
|
||||
|
|
|
|||
62
deploy/docker/sec-config.yaml
Normal file
62
deploy/docker/sec-config.yaml
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# BSB runtime configuration for the Docker compose stack.
|
||||
# Backend services bind all interfaces inside the private compose network;
|
||||
# Angie/nginx is the only published host port.
|
||||
|
||||
default:
|
||||
observable:
|
||||
observable-default:
|
||||
plugin: observable-default
|
||||
enabled: true
|
||||
config: {}
|
||||
events:
|
||||
events-default:
|
||||
plugin: events-default
|
||||
enabled: true
|
||||
services:
|
||||
service-store:
|
||||
plugin: service-store
|
||||
enabled: true
|
||||
config:
|
||||
sqlitePath: /var/lib/betterframe/betterframe.db
|
||||
|
||||
service-admin-http:
|
||||
plugin: service-admin-http
|
||||
enabled: true
|
||||
config:
|
||||
host: 0.0.0.0
|
||||
port: 18080
|
||||
dataDir: /var/lib/betterframe
|
||||
sessionIdleSeconds: 43200
|
||||
sessionMaxSeconds: 2592000
|
||||
loginLockoutThreshold: 8
|
||||
loginLockoutSeconds: 900
|
||||
argon2Memory: 65536
|
||||
argon2TimeCost: 3
|
||||
argon2Parallelism: 2
|
||||
cookieName: betterframe_session
|
||||
totpIssuer: BetterFrame
|
||||
|
||||
service-api-http:
|
||||
plugin: service-api-http
|
||||
enabled: true
|
||||
config:
|
||||
host: 0.0.0.0
|
||||
port: 18081
|
||||
codeTtlSeconds: 600
|
||||
dataDir: /var/lib/betterframe
|
||||
argon2Memory: 65536
|
||||
argon2TimeCost: 3
|
||||
argon2Parallelism: 2
|
||||
noderedUrl: http://nodered:1880
|
||||
|
||||
service-coordinator-ws:
|
||||
plugin: service-coordinator-ws
|
||||
enabled: true
|
||||
config:
|
||||
host: 0.0.0.0
|
||||
port: 18082
|
||||
noderedUrl: http://nodered:1880
|
||||
dataDir: /var/lib/betterframe
|
||||
argon2Memory: 65536
|
||||
argon2TimeCost: 3
|
||||
argon2Parallelism: 2
|
||||
11
deploy/nodered/settings.js
Normal file
11
deploy/nodered/settings.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
const settings = {
|
||||
uiHost: "127.0.0.1",
|
||||
uiPort: Number(process.env.PORT || 1880),
|
||||
functionGlobalContext: {},
|
||||
};
|
||||
|
||||
if (process.env.NODE_RED_CREDENTIAL_SECRET) {
|
||||
settings.credentialSecret = process.env.NODE_RED_CREDENTIAL_SECRET;
|
||||
}
|
||||
|
||||
module.exports = settings;
|
||||
|
|
@ -10,6 +10,7 @@ Type=simple
|
|||
WorkingDirectory=%h/.betterframe-kiosk
|
||||
Environment=XDG_RUNTIME_DIR=/run/user/%U
|
||||
Environment=WAYLAND_DISPLAY=wayland-0
|
||||
Environment=NO_AT_BRIDGE=1
|
||||
Environment=GST_DEBUG=1
|
||||
ExecStart=/opt/betterframe/kiosk/betterframe-kiosk
|
||||
Restart=on-failure
|
||||
|
|
|
|||
27
deploy/systemd/betterframe-nodered.service
Normal file
27
deploy/systemd/betterframe-nodered.service
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
[Unit]
|
||||
Description=BetterFrame Node-RED
|
||||
Documentation=https://github.com/BetterCorp/BetterFrame
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=betterframe
|
||||
Group=betterframe
|
||||
WorkingDirectory=/var/lib/betterframe/nodered
|
||||
Environment=NODE_ENV=production
|
||||
Environment=PORT=1880
|
||||
ExecStart=/usr/bin/env node-red --userDir /var/lib/betterframe/nodered --settings /opt/betterframe/deploy/nodered/settings.js
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
PrivateTmp=true
|
||||
ReadWritePaths=/var/lib/betterframe/nodered
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -34,8 +34,8 @@ discover_server() {
|
|||
fi
|
||||
|
||||
local candidates=(
|
||||
"http://localhost:18081"
|
||||
"http://betterframe.local:18081"
|
||||
"http://localhost"
|
||||
"http://betterframe.local"
|
||||
"https://frame.betterportal.cloud"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ pub fn discover_server(override_url: Option<&str>) -> String {
|
|||
}
|
||||
|
||||
let candidates = [
|
||||
"http://localhost:18081",
|
||||
"http://betterframe.local:18081",
|
||||
"http://localhost",
|
||||
"http://betterframe.local",
|
||||
"https://frame.betterportal.cloud",
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -175,22 +175,23 @@ fn show_pairing_code(window: &ApplicationWindow, code: &str) {
|
|||
}
|
||||
|
||||
fn render_bundle(window: &ApplicationWindow, bundle: KioskBundle) {
|
||||
let layout = bundle.layouts.iter()
|
||||
.find(|l| l.is_default)
|
||||
.or_else(|| bundle.layouts.first());
|
||||
let layout = match bundle.display.default_layout_id {
|
||||
Some(default_layout_id) => bundle.layouts.iter()
|
||||
.find(|l| l.id == default_layout_id)
|
||||
.or_else(|| bundle.layouts.iter().find(|l| l.is_default)),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let Some(layout) = layout else {
|
||||
warn!("no layouts in bundle");
|
||||
WARM_CAMERAS.with(|w| {
|
||||
for (_, (pipe, _)) in w.borrow().iter() { pipeline::stop(pipe); }
|
||||
w.borrow_mut().clear();
|
||||
});
|
||||
warn!("display has no default layout");
|
||||
clear_warm_cameras();
|
||||
show_logo(window);
|
||||
return;
|
||||
};
|
||||
|
||||
if layout.cells.is_empty() {
|
||||
warn!("layout has no cells");
|
||||
clear_warm_cameras();
|
||||
show_logo(window);
|
||||
return;
|
||||
}
|
||||
|
|
@ -296,6 +297,13 @@ fn render_bundle(window: &ApplicationWindow, bundle: KioskBundle) {
|
|||
window.set_child(Some(&grid));
|
||||
}
|
||||
|
||||
fn clear_warm_cameras() {
|
||||
WARM_CAMERAS.with(|w| {
|
||||
for (_, (pipe, _)) in w.borrow().iter() { pipeline::stop(pipe); }
|
||||
w.borrow_mut().clear();
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the paintable for a camera, creating a warm pipeline if missing.
|
||||
fn ensure_warm(
|
||||
cam_id: u32,
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ fn build_ws_url(http_url: &str, token: &str) -> String {
|
|||
format!("ws://{http_url}")
|
||||
};
|
||||
|
||||
// coordinator-ws runs on a different port (18082 vs api-http on 18081)
|
||||
// Direct dev URLs may point at api-http; normal installs go through Angie.
|
||||
let base_port = base.rsplit(':').next().unwrap_or("");
|
||||
let base = if base_port == "18081" {
|
||||
base.replace(":18081", ":18082")
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ default:
|
|||
plugin: service-admin-http
|
||||
enabled: true
|
||||
config:
|
||||
host: 0.0.0.0
|
||||
host: 127.0.0.1
|
||||
port: 18080
|
||||
# Secrets (was service-secrets)
|
||||
dataDir: /var/lib/betterframe
|
||||
|
|
@ -50,7 +50,7 @@ default:
|
|||
plugin: service-api-http
|
||||
enabled: true
|
||||
config:
|
||||
host: 0.0.0.0
|
||||
host: 127.0.0.1
|
||||
port: 18081
|
||||
codeTtlSeconds: 600 # 10m pairing code TTL
|
||||
dataDir: /var/lib/betterframe
|
||||
|
|
@ -64,7 +64,7 @@ default:
|
|||
plugin: service-coordinator-ws
|
||||
enabled: true
|
||||
config:
|
||||
host: 0.0.0.0
|
||||
host: 127.0.0.1
|
||||
port: 18082
|
||||
noderedUrl: http://127.0.0.1:1880
|
||||
dataDir: /var/lib/betterframe
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import type { SecretsApi } from "../../shared/secrets.js";
|
|||
|
||||
const ConfigSchema = av.object(
|
||||
{
|
||||
host: av.string().default("0.0.0.0"),
|
||||
host: av.string().default("127.0.0.1"),
|
||||
port: av.int().min(1).max(65535).default(18081),
|
||||
codeTtlSeconds: av.int().min(60).max(3600).default(600),
|
||||
// Secrets + auth config (shared with admin-http for now)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import { setCoordinator } from "../../shared/coordinator-registry.js";
|
|||
|
||||
const ConfigSchema = av.object(
|
||||
{
|
||||
host: av.string().default("0.0.0.0"),
|
||||
host: av.string().default("127.0.0.1"),
|
||||
port: av.int().min(1).max(65535).default(18082),
|
||||
noderedUrl: av.string().minLength(1).default("http://127.0.0.1:1880"),
|
||||
dataDir: av.string().minLength(1).default("/var/lib/betterframe"),
|
||||
|
|
|
|||
Loading…
Reference in a new issue