mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 16:56:33 +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
|
### Server
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install Node.js 23
|
# Install Node.js 23 + Node-RED
|
||||||
curl -fsSL https://deb.nodesource.com/setup_23.x | sudo bash -
|
curl -fsSL https://deb.nodesource.com/setup_23.x | sudo bash -
|
||||||
sudo apt install -y nodejs build-essential
|
sudo apt install -y nodejs build-essential
|
||||||
|
sudo npm install -g --unsafe-perm node-red
|
||||||
|
|
||||||
# Create user + dirs
|
# Create user + dirs
|
||||||
sudo useradd -r -m -d /var/lib/betterframe betterframe
|
sudo useradd -r -m -d /var/lib/betterframe betterframe
|
||||||
sudo mkdir -p /opt/betterframe /var/log/betterframe /etc/betterframe
|
sudo mkdir -p /opt/betterframe /var/log/betterframe /etc/betterframe /var/lib/betterframe/nodered
|
||||||
sudo chown betterframe:betterframe /var/lib/betterframe /var/log/betterframe
|
sudo chown -R betterframe:betterframe /var/lib/betterframe /var/log/betterframe
|
||||||
|
|
||||||
# Deploy code
|
# Deploy code
|
||||||
sudo git clone https://github.com/BetterCorp/BetterFrame.git /opt/betterframe
|
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 -u betterframe npm run build
|
||||||
sudo cp sec-config.yaml /opt/betterframe/server/sec-config.yaml
|
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-server.service /etc/systemd/system/
|
||||||
|
sudo cp deploy/systemd/betterframe-nodered.service /etc/systemd/system/
|
||||||
sudo systemctl daemon-reload
|
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
|
### Kiosk
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -63,6 +70,9 @@ sudo systemctl reload angie
|
||||||
The Angie config gates `/nrdp/*` with the admin session/API-key auth-check
|
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.
|
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
|
## Docker
|
||||||
|
|
||||||
```bash
|
```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.
|
Kiosk still runs natively on the Pi (needs Wayland/HDMI), not in Docker.
|
||||||
The Compose stack uses `deploy/angie/betterframe.docker.conf` because service
|
The Compose stack uses `deploy/angie/betterframe.docker.conf` and
|
||||||
names, not `127.0.0.1`, are the correct upstreams inside the Docker network.
|
`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.
|
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
|
# 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
|
# 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)
|
# Upstreams (BSB services)
|
||||||
upstream betterframe_admin { server 127.0.0.1:18080; keepalive 16; }
|
upstream betterframe_admin { server 127.0.0.1:18080; keepalive 16; }
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- betterframe-data:/var/lib/betterframe
|
- betterframe-data:/var/lib/betterframe
|
||||||
- ../../sec-config.yaml:/app/server/sec-config.yaml:ro
|
- ./sec-config.yaml:/app/server/sec-config.yaml:ro
|
||||||
expose:
|
expose:
|
||||||
- "18080"
|
- "18080"
|
||||||
- "18081"
|
- "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
|
WorkingDirectory=%h/.betterframe-kiosk
|
||||||
Environment=XDG_RUNTIME_DIR=/run/user/%U
|
Environment=XDG_RUNTIME_DIR=/run/user/%U
|
||||||
Environment=WAYLAND_DISPLAY=wayland-0
|
Environment=WAYLAND_DISPLAY=wayland-0
|
||||||
|
Environment=NO_AT_BRIDGE=1
|
||||||
Environment=GST_DEBUG=1
|
Environment=GST_DEBUG=1
|
||||||
ExecStart=/opt/betterframe/kiosk/betterframe-kiosk
|
ExecStart=/opt/betterframe/kiosk/betterframe-kiosk
|
||||||
Restart=on-failure
|
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
|
fi
|
||||||
|
|
||||||
local candidates=(
|
local candidates=(
|
||||||
"http://localhost:18081"
|
"http://localhost"
|
||||||
"http://betterframe.local:18081"
|
"http://betterframe.local"
|
||||||
"https://frame.betterportal.cloud"
|
"https://frame.betterportal.cloud"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ pub fn discover_server(override_url: Option<&str>) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
let candidates = [
|
let candidates = [
|
||||||
"http://localhost:18081",
|
"http://localhost",
|
||||||
"http://betterframe.local:18081",
|
"http://betterframe.local",
|
||||||
"https://frame.betterportal.cloud",
|
"https://frame.betterportal.cloud",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -175,22 +175,23 @@ fn show_pairing_code(window: &ApplicationWindow, code: &str) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_bundle(window: &ApplicationWindow, bundle: KioskBundle) {
|
fn render_bundle(window: &ApplicationWindow, bundle: KioskBundle) {
|
||||||
let layout = bundle.layouts.iter()
|
let layout = match bundle.display.default_layout_id {
|
||||||
.find(|l| l.is_default)
|
Some(default_layout_id) => bundle.layouts.iter()
|
||||||
.or_else(|| bundle.layouts.first());
|
.find(|l| l.id == default_layout_id)
|
||||||
|
.or_else(|| bundle.layouts.iter().find(|l| l.is_default)),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
let Some(layout) = layout else {
|
let Some(layout) = layout else {
|
||||||
warn!("no layouts in bundle");
|
warn!("display has no default layout");
|
||||||
WARM_CAMERAS.with(|w| {
|
clear_warm_cameras();
|
||||||
for (_, (pipe, _)) in w.borrow().iter() { pipeline::stop(pipe); }
|
|
||||||
w.borrow_mut().clear();
|
|
||||||
});
|
|
||||||
show_logo(window);
|
show_logo(window);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if layout.cells.is_empty() {
|
if layout.cells.is_empty() {
|
||||||
warn!("layout has no cells");
|
warn!("layout has no cells");
|
||||||
|
clear_warm_cameras();
|
||||||
show_logo(window);
|
show_logo(window);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -296,6 +297,13 @@ fn render_bundle(window: &ApplicationWindow, bundle: KioskBundle) {
|
||||||
window.set_child(Some(&grid));
|
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.
|
/// Returns the paintable for a camera, creating a warm pipeline if missing.
|
||||||
fn ensure_warm(
|
fn ensure_warm(
|
||||||
cam_id: u32,
|
cam_id: u32,
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ fn build_ws_url(http_url: &str, token: &str) -> String {
|
||||||
format!("ws://{http_url}")
|
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_port = base.rsplit(':').next().unwrap_or("");
|
||||||
let base = if base_port == "18081" {
|
let base = if base_port == "18081" {
|
||||||
base.replace(":18081", ":18082")
|
base.replace(":18081", ":18082")
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ default:
|
||||||
plugin: service-admin-http
|
plugin: service-admin-http
|
||||||
enabled: true
|
enabled: true
|
||||||
config:
|
config:
|
||||||
host: 0.0.0.0
|
host: 127.0.0.1
|
||||||
port: 18080
|
port: 18080
|
||||||
# Secrets (was service-secrets)
|
# Secrets (was service-secrets)
|
||||||
dataDir: /var/lib/betterframe
|
dataDir: /var/lib/betterframe
|
||||||
|
|
@ -50,7 +50,7 @@ default:
|
||||||
plugin: service-api-http
|
plugin: service-api-http
|
||||||
enabled: true
|
enabled: true
|
||||||
config:
|
config:
|
||||||
host: 0.0.0.0
|
host: 127.0.0.1
|
||||||
port: 18081
|
port: 18081
|
||||||
codeTtlSeconds: 600 # 10m pairing code TTL
|
codeTtlSeconds: 600 # 10m pairing code TTL
|
||||||
dataDir: /var/lib/betterframe
|
dataDir: /var/lib/betterframe
|
||||||
|
|
@ -64,7 +64,7 @@ default:
|
||||||
plugin: service-coordinator-ws
|
plugin: service-coordinator-ws
|
||||||
enabled: true
|
enabled: true
|
||||||
config:
|
config:
|
||||||
host: 0.0.0.0
|
host: 127.0.0.1
|
||||||
port: 18082
|
port: 18082
|
||||||
noderedUrl: http://127.0.0.1:1880
|
noderedUrl: http://127.0.0.1:1880
|
||||||
dataDir: /var/lib/betterframe
|
dataDir: /var/lib/betterframe
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import type { SecretsApi } from "../../shared/secrets.js";
|
||||||
|
|
||||||
const ConfigSchema = av.object(
|
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),
|
port: av.int().min(1).max(65535).default(18081),
|
||||||
codeTtlSeconds: av.int().min(60).max(3600).default(600),
|
codeTtlSeconds: av.int().min(60).max(3600).default(600),
|
||||||
// Secrets + auth config (shared with admin-http for now)
|
// Secrets + auth config (shared with admin-http for now)
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import { setCoordinator } from "../../shared/coordinator-registry.js";
|
||||||
|
|
||||||
const ConfigSchema = av.object(
|
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),
|
port: av.int().min(1).max(65535).default(18082),
|
||||||
noderedUrl: av.string().minLength(1).default("http://127.0.0.1:1880"),
|
noderedUrl: av.string().minLength(1).default("http://127.0.0.1:1880"),
|
||||||
dataDir: av.string().minLength(1).default("/var/lib/betterframe"),
|
dataDir: av.string().minLength(1).default("/var/lib/betterframe"),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue