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
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# ─── Application ─────────────────────────────────────
GC_APP_NAME=GateControl
GC_HOST=0.0.0.0
# Loopback by default: the shipped compose uses network_mode: host, so
# 0.0.0.0 would expose the admin UI in plain HTTP on every interface —
# past Caddy, TLS and HSTS. Caddy reaches the app on 127.0.0.1.
# Set 0.0.0.0 only if you publish the port from a bridge network.
GC_HOST=127.0.0.1
GC_PORT=3000
GC_BASE_URL=https://gate.example.com
GC_SECRET=
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

## [Unreleased]

### Security
- **Das Admin-UI war standardmäßig unverschlüsselt aus dem Internet erreichbar.** Die ausgelieferte Compose-Datei nutzt `network_mode: host`; zusammen mit dem bisherigen Default `GC_HOST=0.0.0.0` lauschte der Node-Prozess damit auf allen Interfaces des Hosts — an Caddy, TLS und HSTS vorbei. Der Default bindet jetzt `127.0.0.1`, in `config/default.js` **und** in beiden `.env.example`. Caddy erreicht die App unverändert über `127.0.0.1:3000`.
**Achtung beim Update:** Wer den Port aus einem Bridge-Netzwerk veröffentlicht statt Host-Networking zu nutzen, muss `GC_HOST=0.0.0.0` künftig ausdrücklich setzen. Bestehende Installationen übernehmen die Änderung erst nach Anpassung der `.env` und einem Neustart des Containers.

### Fixed
- Einstellungen → Allgemein: Das Standard-Design ließ sich in den Themes Classic und Pro nicht auf Aurora stellen — der Aurora-Knopf fehlte dort, obwohl der Server den Wert längst akzeptiert. Beide Templates bieten jetzt alle drei Designs an, ein Paritätstest hält die Auswahl über alle Themes und beide Seiten (Profil + Einstellungen) synchron.
- Caddy versuchte dauerhaft, für den internen Ownership-Marker `gc-owner.invalid` ein öffentliches Zertifikat zu beziehen, was den Log mit fehlschlagenden ACME-Versuchen füllte. Die Marker-Route wird erst nach dem Aufbau der TLS-Automation angehängt und erreichte deren TLD-Klassifizierung nie; sie ist jetzt ausdrücklich vom automatischen HTTPS ausgenommen.

---

Expand Down
7 changes: 6 additions & 1 deletion config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ function envList(key, fallback) {
const config = {
app: {
name: env('GC_APP_NAME', 'GateControl'),
host: env('GC_HOST', '0.0.0.0'),
// Loopback by default: the shipped compose runs with `network_mode: host`,
// so binding 0.0.0.0 would put the admin UI on every interface in plain
// HTTP — past Caddy, TLS and HSTS. Caddy reaches the app on 127.0.0.1
// (see caddyConfig.js upstreams). Deployments that publish the port from a
// bridge network must set GC_HOST=0.0.0.0 explicitly.
host: env('GC_HOST', '127.0.0.1'),
port: envInt('GC_PORT', 3000),
baseUrl: env('GC_BASE_URL', 'http://localhost:3000'),
secret: env('GC_SECRET', ''),
Expand Down
6 changes: 5 additions & 1 deletion deploy/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# ─── Application ─────────────────────────────────────
GC_APP_NAME=GateControl
GC_HOST=0.0.0.0
# Loopback by default: the shipped compose uses network_mode: host, so
# 0.0.0.0 would expose the admin UI in plain HTTP on every interface —
# past Caddy, TLS and HSTS. Caddy reaches the app on 127.0.0.1.
# Set 0.0.0.0 only if you publish the port from a bridge network.
GC_HOST=127.0.0.1
GC_PORT=3000
GC_BASE_URL=https://gate.example.com
GC_SECRET=
Expand Down
8 changes: 7 additions & 1 deletion src/services/caddyConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const { buildRouteAuthProxy, buildAuthHandlerChain } = require('./caddyAuthSubro
const { getAclPeers, setAclPeers } = require('./caddyAcl');
const { renderMaintenancePage } = require('./caddyMaintenance');
const { renderAccessWindowPage } = require('./caddyAccessWindow');
const { getOwnerId, ownerMarkerRoute, extractOwner, ownershipDecision } = require('./caddyOwner');
const { getOwnerId, ownerMarkerRoute, extractOwner, ownershipDecision, MARKER_HOST } = require('./caddyOwner');
const {
caddyApi,
_caddyApi,
Expand Down Expand Up @@ -843,6 +843,12 @@ function buildCaddyConfig(injectedRoutes, options = {}) {
caddyConfig.apps.http.servers.srv0 = {
listen: [':443', ':80'],
routes: serverRoutes,
// The marker host is an RFC 6761 reserved name that can never be issued a
// public certificate. It is appended AFTER buildTlsAutomation() has run,
// so it never reaches the TLD classification there and would otherwise
// fall through to Caddy's automatic HTTPS — producing a permanent stream
// of failing ACME attempts in the log.
automatic_https: { skip: [MARKER_HOST] },
logs: {
default_logger_name: 'access',
},
Expand Down
1 change: 1 addition & 0 deletions src/services/caddyOwner.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ function _resetOwnerCache() {

module.exports = {
OWNER_ID_PREFIX,
MARKER_HOST,
getOwnerId,
ownerMarkerRoute,
extractOwner,
Expand Down
60 changes: 60 additions & 0 deletions tests/bind_host_and_marker_tls.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use strict';
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');

const root = path.join(__dirname, '..');
const read = (p) => fs.readFileSync(path.join(root, p), 'utf8');

// ── Bind-Adresse ────────────────────────────────────────────────────────────
// Die ausgelieferte Compose-Datei nutzt `network_mode: host`. Es gibt also kein
// Port-Mapping, das die Bindung eingrenzen könnte: was der Prozess bindet, liegt
// direkt auf allen Interfaces des Hosts. Ein Default von 0.0.0.0 stellt damit das
// Admin-UI unverschlüsselt ins Netz — an Caddy, TLS und HSTS vorbei.

test('the shipped compose files still use host networking (premise of this guard)', () => {
for (const f of ['docker-compose.yml', 'deploy/docker-compose.yml']) {
assert.match(read(f), /network_mode:\s*host/, `${f} nutzt kein host networking mehr — Annahme dieses Tests prüfen`);
}
});

test('the code default binds loopback, not every interface', () => {
const src = read('config/default.js');
const m = src.match(/host:\s*env\('GC_HOST',\s*'([^']+)'\)/);
assert.ok(m, 'GC_HOST-Default nicht gefunden');
assert.equal(m[1], '127.0.0.1');
});

test('both shipped .env examples default to loopback', () => {
for (const f of ['.env.example', 'deploy/.env.example']) {
const m = read(f).match(/^GC_HOST=(.*)$/m);
assert.ok(m, `${f}: GC_HOST nicht gefunden`);
assert.equal(m[1].trim(), '127.0.0.1', `${f} bindet auf ${m[1]}`);
}
});

test('caddy reaches the app over loopback, so the loopback bind cannot break it', () => {
const src = read('src/services/caddyConfig.js');
assert.match(src, /dial:\s*`127\.0\.0\.1:\$\{config\.app\.port\}`/);
assert.doesNotMatch(src, /dial:\s*`0\.0\.0\.0:/);
});

// ── Marker-Host ─────────────────────────────────────────────────────────────
// `gc-owner.invalid` wird als Ownership-Marker NACH buildTlsAutomation() an die
// Routen gehängt und erreicht dort die TLD-Klassifizierung nie. Ohne expliziten
// Ausschluss greift Caddys automatisches HTTPS und versucht dauerhaft, für einen
// RFC-6761-Namen ein öffentliches Zertifikat zu holen.

test('the marker host is excluded from automatic HTTPS', () => {
const { MARKER_HOST } = require('../src/services/caddyOwner');
assert.equal(MARKER_HOST, 'gc-owner.invalid');
const src = read('src/services/caddyConfig.js');
assert.match(src, /automatic_https:\s*\{\s*skip:\s*\[MARKER_HOST\]\s*\}/,
'srv0 schließt den Marker-Host nicht von der automatischen Zertifikatsvergabe aus');
});

test('the marker host is still classified as non-public if it ever reaches the classifier', () => {
const { NON_PUBLIC_TLDS } = require('../src/services/caddyTlsAutomation');
assert.ok(NON_PUBLIC_TLDS.has('invalid'), 'invalid fehlt in NON_PUBLIC_TLDS');
});
Loading