feat: implement Remote WinBox worker, API, frontend integration, OpenBao persistence, and supporting docs
This commit is contained in:
@@ -23,7 +23,7 @@ server {
|
||||
|
||||
# CSP for React SPA with Tailwind CSS and Leaflet maps
|
||||
# worker-src required for SRP key derivation Web Worker (Safari won't fall back to script-src)
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.tile.openstreetmap.org; font-src 'self'; connect-src 'self' ws: wss:; worker-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.tile.openstreetmap.org; font-src 'self'; connect-src 'self' ws: wss:; worker-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
|
||||
|
||||
# Proxy API requests to the backend service
|
||||
# The api container is reachable via Docker internal DNS as "api" on port 8000
|
||||
@@ -68,6 +68,29 @@ server {
|
||||
proxy_buffers 8 512k;
|
||||
}
|
||||
|
||||
# Proxy Xpra HTML5 client requests to the winbox-worker container
|
||||
location ~ ^/xpra/(\d+)/(.*) {
|
||||
resolver 127.0.0.11 valid=10s ipv6=off;
|
||||
set $xpra_port $1;
|
||||
set $xpra_path $2;
|
||||
set $worker_upstream winbox-worker;
|
||||
|
||||
proxy_pass http://$worker_upstream:$xpra_port/$xpra_path$is_args$args;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_buffering off;
|
||||
# Xpra HTML5 client uses inline event handlers and eval — override the
|
||||
# strict server-level CSP. Adding any add_header in a location block
|
||||
# replaces all inherited server-level add_header directives in nginx.
|
||||
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' ws: wss: data: blob:; frame-ancestors 'self';" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
}
|
||||
|
||||
# Serve static assets with long cache headers
|
||||
# Note: add_header in a location block clears parent-block headers,
|
||||
# so we re-add the essential security header for static assets.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "mikrotik-portal.name" -}}
|
||||
{{- define "the-other-dude.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
@@ -10,7 +10,7 @@ Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "mikrotik-portal.fullname" -}}
|
||||
{{- define "the-other-dude.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
@@ -26,16 +26,16 @@ If release name contains chart name it will be used as a full name.
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "mikrotik-portal.chart" -}}
|
||||
{{- define "the-other-dude.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels applied to all resources.
|
||||
*/}}
|
||||
{{- define "mikrotik-portal.labels" -}}
|
||||
helm.sh/chart: {{ include "mikrotik-portal.chart" . }}
|
||||
{{ include "mikrotik-portal.selectorLabels" . }}
|
||||
{{- define "the-other-dude.labels" -}}
|
||||
helm.sh/chart: {{ include "the-other-dude.chart" . }}
|
||||
{{ include "the-other-dude.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
@@ -45,81 +45,81 @@ app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{/*
|
||||
Selector labels — used in Deployments/Services to match pods.
|
||||
*/}}
|
||||
{{- define "mikrotik-portal.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "mikrotik-portal.name" . }}
|
||||
{{- define "the-other-dude.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "the-other-dude.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
API component labels
|
||||
*/}}
|
||||
{{- define "mikrotik-portal.apiLabels" -}}
|
||||
{{ include "mikrotik-portal.labels" . }}
|
||||
{{- define "the-other-dude.apiLabels" -}}
|
||||
{{ include "the-other-dude.labels" . }}
|
||||
app.kubernetes.io/component: api
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
API selector labels
|
||||
*/}}
|
||||
{{- define "mikrotik-portal.apiSelectorLabels" -}}
|
||||
{{ include "mikrotik-portal.selectorLabels" . }}
|
||||
{{- define "the-other-dude.apiSelectorLabels" -}}
|
||||
{{ include "the-other-dude.selectorLabels" . }}
|
||||
app.kubernetes.io/component: api
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Frontend component labels
|
||||
*/}}
|
||||
{{- define "mikrotik-portal.frontendLabels" -}}
|
||||
{{ include "mikrotik-portal.labels" . }}
|
||||
{{- define "the-other-dude.frontendLabels" -}}
|
||||
{{ include "the-other-dude.labels" . }}
|
||||
app.kubernetes.io/component: frontend
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Frontend selector labels
|
||||
*/}}
|
||||
{{- define "mikrotik-portal.frontendSelectorLabels" -}}
|
||||
{{ include "mikrotik-portal.selectorLabels" . }}
|
||||
{{- define "the-other-dude.frontendSelectorLabels" -}}
|
||||
{{ include "the-other-dude.selectorLabels" . }}
|
||||
app.kubernetes.io/component: frontend
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
PostgreSQL component labels
|
||||
*/}}
|
||||
{{- define "mikrotik-portal.postgresLabels" -}}
|
||||
{{ include "mikrotik-portal.labels" . }}
|
||||
{{- define "the-other-dude.postgresLabels" -}}
|
||||
{{ include "the-other-dude.labels" . }}
|
||||
app.kubernetes.io/component: postgres
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
PostgreSQL selector labels
|
||||
*/}}
|
||||
{{- define "mikrotik-portal.postgresSelectorLabels" -}}
|
||||
{{ include "mikrotik-portal.selectorLabels" . }}
|
||||
{{- define "the-other-dude.postgresSelectorLabels" -}}
|
||||
{{ include "the-other-dude.selectorLabels" . }}
|
||||
app.kubernetes.io/component: postgres
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Redis component labels
|
||||
*/}}
|
||||
{{- define "mikrotik-portal.redisLabels" -}}
|
||||
{{ include "mikrotik-portal.labels" . }}
|
||||
{{- define "the-other-dude.redisLabels" -}}
|
||||
{{ include "the-other-dude.labels" . }}
|
||||
app.kubernetes.io/component: redis
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Redis selector labels
|
||||
*/}}
|
||||
{{- define "mikrotik-portal.redisSelectorLabels" -}}
|
||||
{{ include "mikrotik-portal.selectorLabels" . }}
|
||||
{{- define "the-other-dude.redisSelectorLabels" -}}
|
||||
{{ include "the-other-dude.selectorLabels" . }}
|
||||
app.kubernetes.io/component: redis
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use.
|
||||
*/}}
|
||||
{{- define "mikrotik-portal.serviceAccountName" -}}
|
||||
{{- define "the-other-dude.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "mikrotik-portal.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- default (include "the-other-dude.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
@@ -129,9 +129,9 @@ Create the name of the service account to use.
|
||||
Database URL for the API service (constructed from service names).
|
||||
Uses external URL if postgres.enabled=false.
|
||||
*/}}
|
||||
{{- define "mikrotik-portal.databaseUrl" -}}
|
||||
{{- define "the-other-dude.databaseUrl" -}}
|
||||
{{- if .Values.postgres.enabled }}
|
||||
{{- printf "postgresql+asyncpg://%s:%s@%s-postgres:%d/%s" .Values.postgres.auth.username .Values.secrets.dbPassword (include "mikrotik-portal.fullname" .) (int .Values.postgres.service.port) .Values.postgres.auth.database }}
|
||||
{{- printf "postgresql+asyncpg://%s:%s@%s-postgres:%d/%s" .Values.postgres.auth.username .Values.secrets.dbPassword (include "the-other-dude.fullname" .) (int .Values.postgres.service.port) .Values.postgres.auth.database }}
|
||||
{{- else }}
|
||||
{{- .Values.postgres.externalUrl }}
|
||||
{{- end }}
|
||||
@@ -140,9 +140,9 @@ Uses external URL if postgres.enabled=false.
|
||||
{{/*
|
||||
App user database URL (RLS enforced).
|
||||
*/}}
|
||||
{{- define "mikrotik-portal.appUserDatabaseUrl" -}}
|
||||
{{- define "the-other-dude.appUserDatabaseUrl" -}}
|
||||
{{- if .Values.postgres.enabled }}
|
||||
{{- printf "postgresql+asyncpg://%s:%s@%s-postgres:%d/%s" .Values.postgres.auth.appUsername .Values.secrets.dbAppPassword (include "mikrotik-portal.fullname" .) (int .Values.postgres.service.port) .Values.postgres.auth.database }}
|
||||
{{- printf "postgresql+asyncpg://%s:%s@%s-postgres:%d/%s" .Values.postgres.auth.appUsername .Values.secrets.dbAppPassword (include "the-other-dude.fullname" .) (int .Values.postgres.service.port) .Values.postgres.auth.database }}
|
||||
{{- else }}
|
||||
{{- .Values.postgres.externalUrl }}
|
||||
{{- end }}
|
||||
@@ -151,9 +151,9 @@ App user database URL (RLS enforced).
|
||||
{{/*
|
||||
Sync database URL for Alembic migrations.
|
||||
*/}}
|
||||
{{- define "mikrotik-portal.syncDatabaseUrl" -}}
|
||||
{{- define "the-other-dude.syncDatabaseUrl" -}}
|
||||
{{- if .Values.postgres.enabled }}
|
||||
{{- printf "postgresql+psycopg2://%s:%s@%s-postgres:%d/%s" .Values.postgres.auth.username .Values.secrets.dbPassword (include "mikrotik-portal.fullname" .) (int .Values.postgres.service.port) .Values.postgres.auth.database }}
|
||||
{{- printf "postgresql+psycopg2://%s:%s@%s-postgres:%d/%s" .Values.postgres.auth.username .Values.secrets.dbPassword (include "the-other-dude.fullname" .) (int .Values.postgres.service.port) .Values.postgres.auth.database }}
|
||||
{{- else }}
|
||||
{{- .Values.postgres.externalUrl | replace "asyncpg" "psycopg2" }}
|
||||
{{- end }}
|
||||
@@ -162,9 +162,9 @@ Sync database URL for Alembic migrations.
|
||||
{{/*
|
||||
Redis URL (constructed from service name).
|
||||
*/}}
|
||||
{{- define "mikrotik-portal.redisUrl" -}}
|
||||
{{- define "the-other-dude.redisUrl" -}}
|
||||
{{- if .Values.redis.enabled }}
|
||||
{{- printf "redis://%s-redis:%d/0" (include "mikrotik-portal.fullname" .) (int .Values.redis.service.port) }}
|
||||
{{- printf "redis://%s-redis:%d/0" (include "the-other-dude.fullname" .) (int .Values.redis.service.port) }}
|
||||
{{- else }}
|
||||
{{- .Values.redis.externalUrl | default "redis://localhost:6379/0" }}
|
||||
{{- end }}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-api
|
||||
name: {{ include "the-other-dude.fullname" . }}-api
|
||||
labels:
|
||||
{{- include "mikrotik-portal.apiLabels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.apiLabels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.api.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "mikrotik-portal.apiSelectorLabels" . | nindent 6 }}
|
||||
{{- include "the-other-dude.apiSelectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "mikrotik-portal.apiSelectorLabels" . | nindent 8 }}
|
||||
{{- include "the-other-dude.apiSelectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
@@ -29,28 +29,28 @@ spec:
|
||||
# Load non-sensitive config from ConfigMap
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-config
|
||||
name: {{ include "the-other-dude.fullname" . }}-config
|
||||
# Load secrets as individual environment variables
|
||||
env:
|
||||
- name: JWT_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-secrets
|
||||
name: {{ include "the-other-dude.fullname" . }}-secrets
|
||||
key: JWT_SECRET_KEY
|
||||
- name: CREDENTIAL_ENCRYPTION_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-secrets
|
||||
name: {{ include "the-other-dude.fullname" . }}-secrets
|
||||
key: CREDENTIAL_ENCRYPTION_KEY
|
||||
- name: FIRST_ADMIN_EMAIL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-secrets
|
||||
name: {{ include "the-other-dude.fullname" . }}-secrets
|
||||
key: FIRST_ADMIN_EMAIL
|
||||
- name: FIRST_ADMIN_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-secrets
|
||||
name: {{ include "the-other-dude.fullname" . }}-secrets
|
||||
key: FIRST_ADMIN_PASSWORD
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-api
|
||||
name: {{ include "the-other-dude.fullname" . }}-api
|
||||
labels:
|
||||
{{- include "mikrotik-portal.apiLabels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.apiLabels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.api.service.type }}
|
||||
ports:
|
||||
@@ -12,4 +12,4 @@ spec:
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "mikrotik-portal.apiSelectorLabels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.apiSelectorLabels" . | nindent 4 }}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-config
|
||||
name: {{ include "the-other-dude.fullname" . }}-config
|
||||
labels:
|
||||
{{- include "mikrotik-portal.labels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.labels" . | nindent 4 }}
|
||||
data:
|
||||
DATABASE_URL: {{ include "mikrotik-portal.databaseUrl" . | quote }}
|
||||
SYNC_DATABASE_URL: {{ include "mikrotik-portal.syncDatabaseUrl" . | quote }}
|
||||
APP_USER_DATABASE_URL: {{ include "mikrotik-portal.appUserDatabaseUrl" . | quote }}
|
||||
REDIS_URL: {{ include "mikrotik-portal.redisUrl" . | quote }}
|
||||
NATS_URL: {{ printf "nats://%s-nats:%d" (include "mikrotik-portal.fullname" .) (int .Values.nats.service.port) | quote }}
|
||||
DATABASE_URL: {{ include "the-other-dude.databaseUrl" . | quote }}
|
||||
SYNC_DATABASE_URL: {{ include "the-other-dude.syncDatabaseUrl" . | quote }}
|
||||
APP_USER_DATABASE_URL: {{ include "the-other-dude.appUserDatabaseUrl" . | quote }}
|
||||
REDIS_URL: {{ include "the-other-dude.redisUrl" . | quote }}
|
||||
NATS_URL: {{ printf "nats://%s-nats:%d" (include "the-other-dude.fullname" .) (int .Values.nats.service.port) | quote }}
|
||||
JWT_ALGORITHM: "HS256"
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: {{ .Values.api.env.jwtAccessTokenExpireMinutes | quote }}
|
||||
JWT_REFRESH_TOKEN_EXPIRE_DAYS: {{ .Values.api.env.jwtRefreshTokenExpireDays | quote }}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-frontend
|
||||
name: {{ include "the-other-dude.fullname" . }}-frontend
|
||||
labels:
|
||||
{{- include "mikrotik-portal.frontendLabels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.frontendLabels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.frontend.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "mikrotik-portal.frontendSelectorLabels" . | nindent 6 }}
|
||||
{{- include "the-other-dude.frontendSelectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "mikrotik-portal.frontendSelectorLabels" . | nindent 8 }}
|
||||
{{- include "the-other-dude.frontendSelectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
containers:
|
||||
- name: frontend
|
||||
@@ -42,9 +42,9 @@ spec:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-frontend
|
||||
name: {{ include "the-other-dude.fullname" . }}-frontend
|
||||
labels:
|
||||
{{- include "mikrotik-portal.frontendLabels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.frontendLabels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.frontend.service.type }}
|
||||
ports:
|
||||
@@ -53,4 +53,4 @@ spec:
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "mikrotik-portal.frontendSelectorLabels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.frontendSelectorLabels" . | nindent 4 }}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}
|
||||
name: {{ include "the-other-dude.fullname" . }}
|
||||
labels:
|
||||
{{- include "mikrotik-portal.labels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
@@ -16,11 +16,11 @@ spec:
|
||||
{{- if .Values.ingress.tls.enabled }}
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ .Values.ingress.host | default "mikrotik-portal.local" | quote }}
|
||||
secretName: {{ .Values.ingress.tls.secretName | default (printf "%s-tls" (include "mikrotik-portal.fullname" .)) | quote }}
|
||||
- {{ .Values.ingress.host | default "the-other-dude.local" | quote }}
|
||||
secretName: {{ .Values.ingress.tls.secretName | default (printf "%s-tls" (include "the-other-dude.fullname" .)) | quote }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: {{ .Values.ingress.host | default "mikrotik-portal.local" | quote }}
|
||||
- host: {{ .Values.ingress.host | default "the-other-dude.local" | quote }}
|
||||
http:
|
||||
paths:
|
||||
# API routes — send /api/* to the FastAPI service
|
||||
@@ -28,7 +28,7 @@ spec:
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-api
|
||||
name: {{ include "the-other-dude.fullname" . }}-api
|
||||
port:
|
||||
number: {{ .Values.api.service.port }}
|
||||
# Docs routes — proxy /docs and /redoc to API as well
|
||||
@@ -36,14 +36,14 @@ spec:
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-api
|
||||
name: {{ include "the-other-dude.fullname" . }}-api
|
||||
port:
|
||||
number: {{ .Values.api.service.port }}
|
||||
- path: /redoc
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-api
|
||||
name: {{ include "the-other-dude.fullname" . }}-api
|
||||
port:
|
||||
number: {{ .Values.api.service.port }}
|
||||
# Frontend SPA — all other routes go to nginx
|
||||
@@ -51,7 +51,7 @@ spec:
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-frontend
|
||||
name: {{ include "the-other-dude.fullname" . }}-frontend
|
||||
port:
|
||||
number: {{ .Values.frontend.service.port }}
|
||||
{{- end }}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-nats-headless
|
||||
name: {{ include "the-other-dude.fullname" . }}-nats-headless
|
||||
labels:
|
||||
{{- include "mikrotik-portal.labels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: nats
|
||||
spec:
|
||||
clusterIP: None
|
||||
@@ -15,16 +15,16 @@ spec:
|
||||
port: 4222
|
||||
targetPort: 4222
|
||||
selector:
|
||||
{{- include "mikrotik-portal.selectorLabels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: nats
|
||||
---
|
||||
# NATS ClusterIP service for client access
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-nats
|
||||
name: {{ include "the-other-dude.fullname" . }}-nats
|
||||
labels:
|
||||
{{- include "mikrotik-portal.labels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: nats
|
||||
spec:
|
||||
type: ClusterIP
|
||||
@@ -36,28 +36,28 @@ spec:
|
||||
port: 8222
|
||||
targetPort: 8222
|
||||
selector:
|
||||
{{- include "mikrotik-portal.selectorLabels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: nats
|
||||
---
|
||||
# NATS JetStream StatefulSet (needs stable storage)
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-nats
|
||||
name: {{ include "the-other-dude.fullname" . }}-nats
|
||||
labels:
|
||||
{{- include "mikrotik-portal.labels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: nats
|
||||
spec:
|
||||
replicas: 1
|
||||
serviceName: {{ include "mikrotik-portal.fullname" . }}-nats-headless
|
||||
serviceName: {{ include "the-other-dude.fullname" . }}-nats-headless
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "mikrotik-portal.selectorLabels" . | nindent 6 }}
|
||||
{{- include "the-other-dude.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: nats
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "mikrotik-portal.selectorLabels" . | nindent 8 }}
|
||||
{{- include "the-other-dude.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: nats
|
||||
spec:
|
||||
containers:
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-poller
|
||||
name: {{ include "the-other-dude.fullname" . }}-poller
|
||||
labels:
|
||||
{{- include "mikrotik-portal.labels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: poller
|
||||
spec:
|
||||
replicas: {{ .Values.poller.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "mikrotik-portal.selectorLabels" . | nindent 6 }}
|
||||
{{- include "the-other-dude.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: poller
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "mikrotik-portal.selectorLabels" . | nindent 8 }}
|
||||
{{- include "the-other-dude.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: poller
|
||||
spec:
|
||||
containers:
|
||||
@@ -26,32 +26,32 @@ spec:
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-secrets
|
||||
name: {{ include "the-other-dude.fullname" . }}-secrets
|
||||
key: POLLER_DATABASE_URL
|
||||
- name: CREDENTIAL_ENCRYPTION_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-secrets
|
||||
name: {{ include "the-other-dude.fullname" . }}-secrets
|
||||
key: CREDENTIAL_ENCRYPTION_KEY
|
||||
- name: NATS_URL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-config
|
||||
name: {{ include "the-other-dude.fullname" . }}-config
|
||||
key: NATS_URL
|
||||
- name: REDIS_URL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-config
|
||||
name: {{ include "the-other-dude.fullname" . }}-config
|
||||
key: REDIS_URL
|
||||
- name: POLL_INTERVAL_SECONDS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-config
|
||||
name: {{ include "the-other-dude.fullname" . }}-config
|
||||
key: POLL_INTERVAL_SECONDS
|
||||
- name: LOG_LEVEL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-config
|
||||
name: {{ include "the-other-dude.fullname" . }}-config
|
||||
key: POLLER_LOG_LEVEL
|
||||
resources:
|
||||
requests:
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-postgres
|
||||
name: {{ include "the-other-dude.fullname" . }}-postgres
|
||||
labels:
|
||||
{{- include "mikrotik-portal.postgresLabels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.postgresLabels" . | nindent 4 }}
|
||||
spec:
|
||||
serviceName: {{ include "mikrotik-portal.fullname" . }}-postgres
|
||||
serviceName: {{ include "the-other-dude.fullname" . }}-postgres
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "mikrotik-portal.postgresSelectorLabels" . | nindent 6 }}
|
||||
{{- include "the-other-dude.postgresSelectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "mikrotik-portal.postgresSelectorLabels" . | nindent 8 }}
|
||||
{{- include "the-other-dude.postgresSelectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
@@ -32,14 +32,14 @@ spec:
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-secrets
|
||||
name: {{ include "the-other-dude.fullname" . }}-secrets
|
||||
key: DB_PASSWORD
|
||||
- name: APP_USER
|
||||
value: {{ .Values.postgres.auth.appUsername | quote }}
|
||||
- name: APP_USER_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-secrets
|
||||
name: {{ include "the-other-dude.fullname" . }}-secrets
|
||||
key: DB_APP_PASSWORD
|
||||
volumeMounts:
|
||||
- name: postgres-data
|
||||
@@ -74,7 +74,7 @@ spec:
|
||||
volumes:
|
||||
- name: init-scripts
|
||||
configMap:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-postgres-init
|
||||
name: {{ include "the-other-dude.fullname" . }}-postgres-init
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: postgres-data
|
||||
@@ -90,9 +90,9 @@ spec:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-postgres
|
||||
name: {{ include "the-other-dude.fullname" . }}-postgres
|
||||
labels:
|
||||
{{- include "mikrotik-portal.postgresLabels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.postgresLabels" . | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
clusterIP: None
|
||||
@@ -102,14 +102,14 @@ spec:
|
||||
protocol: TCP
|
||||
name: postgres
|
||||
selector:
|
||||
{{- include "mikrotik-portal.postgresSelectorLabels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.postgresSelectorLabels" . | nindent 4 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-postgres-init
|
||||
name: {{ include "the-other-dude.fullname" . }}-postgres-init
|
||||
labels:
|
||||
{{- include "mikrotik-portal.postgresLabels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.postgresLabels" . | nindent 4 }}
|
||||
data:
|
||||
init.sql: |
|
||||
-- Create non-superuser app_user role for RLS enforcement
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-redis
|
||||
name: {{ include "the-other-dude.fullname" . }}-redis
|
||||
labels:
|
||||
{{- include "mikrotik-portal.redisLabels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.redisLabels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "mikrotik-portal.redisSelectorLabels" . | nindent 6 }}
|
||||
{{- include "the-other-dude.redisSelectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "mikrotik-portal.redisSelectorLabels" . | nindent 8 }}
|
||||
{{- include "the-other-dude.redisSelectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
@@ -45,9 +45,9 @@ spec:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-redis
|
||||
name: {{ include "the-other-dude.fullname" . }}-redis
|
||||
labels:
|
||||
{{- include "mikrotik-portal.redisLabels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.redisLabels" . | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
@@ -56,5 +56,5 @@ spec:
|
||||
protocol: TCP
|
||||
name: redis
|
||||
selector:
|
||||
{{- include "mikrotik-portal.redisSelectorLabels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.redisSelectorLabels" . | nindent 4 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "mikrotik-portal.fullname" . }}-secrets
|
||||
name: {{ include "the-other-dude.fullname" . }}-secrets
|
||||
labels:
|
||||
{{- include "mikrotik-portal.labels" . | nindent 4 }}
|
||||
{{- include "the-other-dude.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
JWT_SECRET_KEY: {{ .Values.secrets.jwtSecretKey | quote }}
|
||||
@@ -12,4 +12,4 @@ stringData:
|
||||
FIRST_ADMIN_PASSWORD: {{ .Values.secrets.firstAdminPassword | quote }}
|
||||
DB_PASSWORD: {{ .Values.secrets.dbPassword | quote }}
|
||||
DB_APP_PASSWORD: {{ .Values.secrets.dbAppPassword | quote }}
|
||||
POLLER_DATABASE_URL: {{ printf "postgres://poller_user:%s@%s-postgres:%d/%s" .Values.secrets.dbPollerPassword (include "mikrotik-portal.fullname" .) (int .Values.postgres.service.port) .Values.postgres.auth.database | quote }}
|
||||
POLLER_DATABASE_URL: {{ printf "postgres://poller_user:%s@%s-postgres:%d/%s" .Values.secrets.dbPollerPassword (include "the-other-dude.fullname" .) (int .Values.postgres.service.port) .Values.postgres.auth.database | quote }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Default values for mikrotik-portal.
|
||||
# Default values for the-other-dude.
|
||||
# These values should work with `helm install` out of the box for development.
|
||||
# Production deployments MUST override secrets.jwtSecretKey, secrets.credentialEncryptionKey,
|
||||
# and secrets.firstAdminPassword.
|
||||
@@ -10,7 +10,7 @@ api:
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: mikrotik-portal/api
|
||||
repository: the-other-dude/api
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
@@ -54,7 +54,7 @@ frontend:
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: mikrotik-portal/frontend
|
||||
repository: the-other-dude/frontend
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
@@ -161,7 +161,7 @@ poller:
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: mikrotik-portal/poller
|
||||
repository: the-other-dude/poller
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
@@ -191,7 +191,7 @@ ingress:
|
||||
|
||||
tls:
|
||||
enabled: false
|
||||
# secretName: mikrotik-portal-tls
|
||||
# secretName: the-other-dude-tls
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Secrets
|
||||
@@ -206,7 +206,7 @@ secrets:
|
||||
credentialEncryptionKey: ""
|
||||
|
||||
# First admin account (created on first startup)
|
||||
firstAdminEmail: "admin@mikrotik-portal.local"
|
||||
firstAdminEmail: "admin@the-other-dude.local"
|
||||
firstAdminPassword: ""
|
||||
|
||||
# PostgreSQL superuser password
|
||||
|
||||
11
infrastructure/openbao/config.hcl
Normal file
11
infrastructure/openbao/config.hcl
Normal file
@@ -0,0 +1,11 @@
|
||||
storage "file" {
|
||||
path = "/openbao/data"
|
||||
}
|
||||
|
||||
listener "tcp" {
|
||||
address = "0.0.0.0:8200"
|
||||
tls_disable = true
|
||||
}
|
||||
|
||||
api_addr = "http://127.0.0.1:8200"
|
||||
ui = false
|
||||
@@ -1,31 +1,107 @@
|
||||
#!/bin/sh
|
||||
#!/bin/sh
|
||||
# OpenBao Transit initialization script
|
||||
# Runs after OpenBao starts in dev mode
|
||||
# Handles first-run init, sealed unseal, and already-unsealed cases
|
||||
|
||||
set -e
|
||||
|
||||
export BAO_ADDR="http://127.0.0.1:8200"
|
||||
export BAO_TOKEN="${BAO_DEV_ROOT_TOKEN_ID:-dev-openbao-token}"
|
||||
|
||||
# Wait for OpenBao to be ready
|
||||
# ---------------------------------------------------------------------------
|
||||
# Wait for OpenBao HTTP listener to accept connections.
|
||||
# We hit /v1/sys/health which returns 200 (unsealed), 429 (standby),
|
||||
# 472 (perf-standby), 501 (uninitialized), or 503 (sealed).
|
||||
# Any HTTP response means the server is up; connection refused means not yet.
|
||||
# ---------------------------------------------------------------------------
|
||||
echo "Waiting for OpenBao to start..."
|
||||
until bao status >/dev/null 2>&1; do
|
||||
until wget -qO /dev/null http://127.0.0.1:8200/v1/sys/health 2>/dev/null; do
|
||||
# wget returns 0 only on 2xx; for 4xx/5xx it returns 8.
|
||||
# But connection refused returns 4. Check if we got ANY HTTP response.
|
||||
rc=0
|
||||
wget -S -qO /dev/null http://127.0.0.1:8200/v1/sys/health 2>&1 | grep -q "HTTP/" && break
|
||||
sleep 0.5
|
||||
done
|
||||
echo "OpenBao is ready"
|
||||
|
||||
# Enable Transit secrets engine (idempotent - ignores "already enabled" errors)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Determine current state via structured output
|
||||
# ---------------------------------------------------------------------------
|
||||
STATUS_JSON="$(bao status -format=json 2>/dev/null || true)"
|
||||
INITIALIZED="$(echo "$STATUS_JSON" | grep '"initialized"' | head -1 | awk -F: '{gsub(/[^a-z]/, "", $2); print $2}')"
|
||||
SEALED="$(echo "$STATUS_JSON" | grep '"sealed"' | head -1 | awk -F: '{gsub(/[^a-z]/, "", $2); print $2}')"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scenario 1 – First run (not initialized)
|
||||
# ---------------------------------------------------------------------------
|
||||
if [ "$INITIALIZED" != "true" ]; then
|
||||
echo "OpenBao is not initialized — running first-time setup..."
|
||||
|
||||
INIT_JSON="$(bao operator init -key-shares=1 -key-threshold=1 -format=json)"
|
||||
UNSEAL_KEY="$(echo "$INIT_JSON" | grep '"unseal_keys_b64"' -A1 | tail -1 | tr -d ' ",[]\r')"
|
||||
ROOT_TOKEN="$(echo "$INIT_JSON" | grep '"root_token"' | awk -F'"' '{print $4}')"
|
||||
|
||||
export BAO_TOKEN="$ROOT_TOKEN"
|
||||
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo " OPENBAO FIRST-RUN CREDENTIALS — SAVE THESE TO .env"
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo " BAO_UNSEAL_KEY=$UNSEAL_KEY"
|
||||
echo " OPENBAO_TOKEN=$ROOT_TOKEN"
|
||||
echo ""
|
||||
echo " Add both values to your .env file so subsequent starts"
|
||||
echo " can unseal and authenticate automatically."
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
echo "Unsealing OpenBao..."
|
||||
bao operator unseal "$UNSEAL_KEY"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scenario 2 – Sealed, key provided
|
||||
# ---------------------------------------------------------------------------
|
||||
elif [ "$SEALED" = "true" ]; then
|
||||
if [ -z "$BAO_UNSEAL_KEY" ]; then
|
||||
echo "ERROR: OpenBao is sealed but BAO_UNSEAL_KEY is not set." >&2
|
||||
echo " Provide BAO_UNSEAL_KEY in the environment or .env file." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "OpenBao is sealed — unsealing..."
|
||||
bao operator unseal "$BAO_UNSEAL_KEY"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scenario 3 – Already unsealed
|
||||
# ---------------------------------------------------------------------------
|
||||
else
|
||||
echo "OpenBao is already unsealed"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Verify BAO_TOKEN is available for Transit setup
|
||||
# (Scenario 1 exports it from init output; Scenarios 2/3 inherit from env)
|
||||
# ---------------------------------------------------------------------------
|
||||
if [ -z "$BAO_TOKEN" ]; then
|
||||
echo "ERROR: BAO_TOKEN is not set. Set OPENBAO_TOKEN in .env / .env.prod." >&2
|
||||
exit 1
|
||||
fi
|
||||
export BAO_TOKEN
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Transit engine + policy setup (idempotent)
|
||||
# ---------------------------------------------------------------------------
|
||||
echo "Configuring Transit engine and policies..."
|
||||
|
||||
bao secrets enable transit 2>/dev/null || true
|
||||
echo "Transit engine enabled"
|
||||
|
||||
# Create policy for the API backend (full Transit access)
|
||||
bao policy write api-policy - <<'POLICY'
|
||||
path "transit/*" {
|
||||
capabilities = ["create", "read", "update", "delete", "list"]
|
||||
}
|
||||
POLICY
|
||||
|
||||
# Create policy for the Go poller (encrypt + decrypt only)
|
||||
bao policy write poller-policy - <<'POLICY'
|
||||
path "transit/decrypt/tenant_*" {
|
||||
capabilities = ["update"]
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# The Other Dude — Apache reverse proxy example
|
||||
#
|
||||
# Required modules:
|
||||
# a2enmod proxy proxy_http proxy_wstunnel rewrite ssl headers
|
||||
#
|
||||
# This config assumes:
|
||||
# - TOD frontend runs on FRONTEND_HOST:3000
|
||||
# - TOD API runs on API_HOST:8001
|
||||
# - WinBox worker Xpra ports are on WORKER_HOST:10100-10119
|
||||
#
|
||||
# Replace tod.example.com and upstream addresses with your values.
|
||||
|
||||
<VirtualHost *:80>
|
||||
ServerName tod.example.com
|
||||
RewriteEngine On
|
||||
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:443>
|
||||
ServerName tod.example.com
|
||||
|
||||
SSLEngine on
|
||||
SSLCertificateFile /etc/ssl/certs/tod.example.com.pem
|
||||
SSLCertificateKeyFile /etc/ssl/private/tod.example.com.key
|
||||
|
||||
# ── Security headers ──────────────────────────────────────────────
|
||||
Header always set X-Frame-Options "SAMEORIGIN"
|
||||
Header always set X-Content-Type-Options "nosniff"
|
||||
Header always set X-XSS-Protection "1; mode=block"
|
||||
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||
|
||||
# ── Xpra (Remote WinBox) ─────────────────────────────────────────
|
||||
# Must appear BEFORE the general proxy rules.
|
||||
# WebSocket upgrade is required. Do NOT enable mod_deflate on this path
|
||||
# — compressing WebSocket binary frames corrupts Xpra mouse/keyboard data.
|
||||
#
|
||||
# ProxyPassMatch uses regex to capture the port and forward to the worker.
|
||||
# Ports 10100-10119 (up to 20 concurrent sessions).
|
||||
RewriteEngine On
|
||||
|
||||
# WebSocket upgrade for Xpra
|
||||
RewriteCond %{HTTP:Upgrade} =websocket [NC]
|
||||
RewriteRule ^/xpra/(\d+)/(.*)$ ws://YOUR_TOD_HOST:$1/$2 [P,L]
|
||||
|
||||
# Regular HTTP requests for Xpra HTML5 client assets
|
||||
ProxyPassMatch "^/xpra/(\d+)/(.*)" "http://YOUR_TOD_HOST:$1/$2"
|
||||
|
||||
# Relaxed CSP for Xpra HTML5 client (inline scripts + eval)
|
||||
<LocationMatch "^/xpra/">
|
||||
Header always set Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' ws: wss: data: blob:; frame-ancestors 'self';"
|
||||
SetEnv no-gzip 1
|
||||
</LocationMatch>
|
||||
|
||||
# ── API ───────────────────────────────────────────────────────────
|
||||
ProxyPass /api/ http://YOUR_TOD_HOST:8001/api/
|
||||
ProxyPassReverse /api/ http://YOUR_TOD_HOST:8001/api/
|
||||
|
||||
ProxyTimeout 300
|
||||
RequestHeader set X-Forwarded-Proto "https"
|
||||
|
||||
<Location /api/>
|
||||
# Let the API set its own CSP
|
||||
Header unset Content-Security-Policy
|
||||
</Location>
|
||||
|
||||
# ── Frontend (SPA) ────────────────────────────────────────────────
|
||||
ProxyPass / http://YOUR_TOD_HOST:3000/
|
||||
ProxyPassReverse / http://YOUR_TOD_HOST:3000/
|
||||
|
||||
ProxyPreserveHost On
|
||||
</VirtualHost>
|
||||
@@ -0,0 +1,71 @@
|
||||
# The Other Dude — Caddy reverse proxy example
|
||||
#
|
||||
# This config assumes:
|
||||
# - TOD frontend runs on FRONTEND_HOST:3000
|
||||
# - TOD API runs on API_HOST:8001
|
||||
# - WinBox worker Xpra ports are on WORKER_HOST:10100-10119
|
||||
#
|
||||
# Replace tod.example.com and the upstream IPs with your values.
|
||||
# Caddy handles TLS automatically via Let's Encrypt.
|
||||
|
||||
tod.example.com {
|
||||
log {
|
||||
output file /var/log/caddy/tod.log {
|
||||
roll_size 50mb
|
||||
roll_keep 5
|
||||
}
|
||||
format json
|
||||
}
|
||||
|
||||
encode zstd gzip
|
||||
|
||||
header {
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
X-XSS-Protection "1; mode=block"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
-Server
|
||||
}
|
||||
|
||||
# ── Xpra (Remote WinBox) ──────────────────────────────────────────
|
||||
# Proxies the Xpra HTML5 client to winbox-worker Xpra ports.
|
||||
# Port range 10100-10119 (up to 20 concurrent sessions).
|
||||
# Uses scoped compression to avoid corrupting WebSocket binary frames.
|
||||
@xpra path_regexp xpra ^/xpra/(101[0-1][0-9])/(.*)$
|
||||
handle @xpra {
|
||||
# Override parent encode — only compress text assets, NOT WebSocket frames
|
||||
encode {
|
||||
gzip
|
||||
match {
|
||||
header Content-Type text/*
|
||||
header Content-Type application/javascript*
|
||||
header Content-Type application/json*
|
||||
}
|
||||
}
|
||||
uri strip_prefix /xpra/{re.xpra.1}
|
||||
reverse_proxy {$WORKER_HOST:YOUR_TOD_HOST}:{re.xpra.1} {
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
# ── API ───────────────────────────────────────────────────────────
|
||||
handle /api/* {
|
||||
reverse_proxy http://{$API_HOST:YOUR_TOD_HOST}:8001 {
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
transport http {
|
||||
dial_timeout 30s
|
||||
response_header_timeout 60s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ── Frontend (SPA) ────────────────────────────────────────────────
|
||||
handle {
|
||||
reverse_proxy http://{$FRONTEND_HOST:YOUR_TOD_HOST}:3000 {
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
# The Other Dude — HAProxy reverse proxy example
|
||||
#
|
||||
# This config assumes:
|
||||
# - TOD frontend runs on FRONTEND_HOST:3000
|
||||
# - TOD API runs on API_HOST:8001
|
||||
# - WinBox worker Xpra ports are on WORKER_HOST:10100-10119
|
||||
# - TLS is terminated by HAProxy
|
||||
#
|
||||
# Replace tod.example.com and upstream addresses with your values.
|
||||
#
|
||||
# IMPORTANT: Do NOT enable compression on the xpra backend —
|
||||
# compressing WebSocket binary frames corrupts Xpra mouse/keyboard data.
|
||||
|
||||
global
|
||||
log stdout format raw local0
|
||||
maxconn 4096
|
||||
|
||||
defaults
|
||||
log global
|
||||
mode http
|
||||
option httplog
|
||||
timeout connect 10s
|
||||
timeout client 300s
|
||||
timeout server 300s
|
||||
timeout tunnel 3600s
|
||||
|
||||
# ── Frontend ─────────────────────────────────────────────────────────
|
||||
|
||||
frontend https
|
||||
bind *:443 ssl crt /etc/ssl/certs/tod.example.com.pem
|
||||
bind *:80
|
||||
redirect scheme https code 301 if !{ ssl_fc }
|
||||
|
||||
# Security headers
|
||||
http-response set-header X-Frame-Options "SAMEORIGIN"
|
||||
http-response set-header X-Content-Type-Options "nosniff"
|
||||
http-response set-header Referrer-Policy "strict-origin-when-cross-origin"
|
||||
|
||||
# Routing rules (order matters — first match wins)
|
||||
acl is_xpra path_beg /xpra/
|
||||
acl is_api path_beg /api/
|
||||
|
||||
use_backend xpra if is_xpra
|
||||
use_backend api if is_api
|
||||
default_backend frontend
|
||||
|
||||
# ── Backends ─────────────────────────────────────────────────────────
|
||||
|
||||
backend api
|
||||
option forwardfor
|
||||
http-request set-header X-Forwarded-Proto https
|
||||
server api1 YOUR_TOD_HOST:8001 check
|
||||
|
||||
backend frontend
|
||||
option forwardfor
|
||||
server fe1 YOUR_TOD_HOST:3000 check
|
||||
|
||||
# Xpra backend — uses a Lua or map-based approach to extract the port
|
||||
# from the URL path. This example covers port 10100; add servers for
|
||||
# 10101-10119 as needed, or use HAProxy's Lua scripting for dynamic routing.
|
||||
#
|
||||
# WARNING: Do NOT add "compression" directives to this backend.
|
||||
backend xpra
|
||||
option forwardfor
|
||||
|
||||
# Strip /xpra/{port} prefix
|
||||
http-request set-path %[path,regsub(^/xpra/[0-9]+/,/)]
|
||||
|
||||
# Route to the correct port based on URL
|
||||
# For dynamic port routing, use a map file or Lua script.
|
||||
# Static example for port 10100:
|
||||
acl xpra_10100 path_beg /xpra/10100/
|
||||
use-server xpra10100 if xpra_10100
|
||||
|
||||
server xpra10100 YOUR_TOD_HOST:10100 check
|
||||
# server xpra10101 YOUR_TOD_HOST:10101 check
|
||||
# ... add through 10119 as needed
|
||||
90
infrastructure/reverse-proxy-examples/nginx/tod.conf.example
Normal file
90
infrastructure/reverse-proxy-examples/nginx/tod.conf.example
Normal file
@@ -0,0 +1,90 @@
|
||||
# The Other Dude — nginx reverse proxy example
|
||||
#
|
||||
# This config assumes:
|
||||
# - TOD frontend runs on FRONTEND_HOST:3000
|
||||
# - TOD API runs on API_HOST:8001
|
||||
# - WinBox worker Xpra ports are on WORKER_HOST:10100-10119
|
||||
# - TLS is terminated by nginx (or upstream load balancer)
|
||||
#
|
||||
# Replace tod.example.com and upstream addresses with your values.
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
upstream tod_frontend {
|
||||
server YOUR_TOD_HOST:3000;
|
||||
}
|
||||
|
||||
upstream tod_api {
|
||||
server YOUR_TOD_HOST:8001;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name tod.example.com;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name tod.example.com;
|
||||
|
||||
ssl_certificate /etc/ssl/certs/tod.example.com.pem;
|
||||
ssl_certificate_key /etc/ssl/private/tod.example.com.key;
|
||||
|
||||
# ── Security headers ──────────────────────────────────────────────
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.tile.openstreetmap.org; font-src 'self'; connect-src 'self' ws: wss:; worker-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
|
||||
|
||||
# ── API ───────────────────────────────────────────────────────────
|
||||
location /api/ {
|
||||
proxy_pass http://tod_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
}
|
||||
|
||||
# ── Xpra (Remote WinBox) ─────────────────────────────────────────
|
||||
# Proxies Xpra HTML5 client to winbox-worker ports 10100-10119.
|
||||
# WebSocket support is required. Do NOT enable gzip on this location
|
||||
# — compressing WebSocket binary frames corrupts Xpra mouse/keyboard data.
|
||||
location ~ ^/xpra/(\d+)/(.*) {
|
||||
set $xpra_port $1;
|
||||
set $xpra_path $2;
|
||||
|
||||
proxy_pass http://YOUR_TOD_HOST:$xpra_port/$xpra_path$is_args$args;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_buffering off;
|
||||
|
||||
# Xpra HTML5 client needs relaxed CSP (inline scripts + eval)
|
||||
# Adding add_header in a location block replaces all server-level headers in nginx
|
||||
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' ws: wss: data: blob:; frame-ancestors 'self';" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
}
|
||||
|
||||
# ── Frontend (SPA) ────────────────────────────────────────────────
|
||||
location / {
|
||||
proxy_pass http://tod_frontend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
# The Other Dude — Traefik dynamic configuration example
|
||||
#
|
||||
# This config assumes:
|
||||
# - TOD frontend runs on FRONTEND_HOST:3000
|
||||
# - TOD API runs on API_HOST:8001
|
||||
# - WinBox worker Xpra ports are on WORKER_HOST:10100-10119
|
||||
# - Traefik entrypoints: web (80) and websecure (443)
|
||||
#
|
||||
# Replace tod.example.com and upstream addresses with your values.
|
||||
#
|
||||
# For Docker-based Traefik, labels can replace this file.
|
||||
# This example uses file provider for clarity.
|
||||
|
||||
http:
|
||||
routers:
|
||||
# ── Xpra (Remote WinBox) ────────────────────────────────────────
|
||||
# Must be higher priority than the frontend catch-all.
|
||||
# Each Xpra port needs its own service since Traefik doesn't
|
||||
# support dynamic port extraction from path regex.
|
||||
# Shown for port 10100; duplicate for 10101-10119 as needed.
|
||||
tod-xpra-10100:
|
||||
rule: "Host(`tod.example.com`) && PathPrefix(`/xpra/10100/`)"
|
||||
entryPoints: [websecure]
|
||||
service: tod-xpra-10100
|
||||
middlewares: [xpra-strip, xpra-headers]
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
priority: 30
|
||||
|
||||
# ── API ─────────────────────────────────────────────────────────
|
||||
tod-api:
|
||||
rule: "Host(`tod.example.com`) && PathPrefix(`/api/`)"
|
||||
entryPoints: [websecure]
|
||||
service: tod-api
|
||||
middlewares: [security-headers]
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
priority: 20
|
||||
|
||||
# ── Frontend (SPA) ──────────────────────────────────────────────
|
||||
tod-frontend:
|
||||
rule: "Host(`tod.example.com`)"
|
||||
entryPoints: [websecure]
|
||||
service: tod-frontend
|
||||
middlewares: [security-headers]
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
priority: 10
|
||||
|
||||
services:
|
||||
tod-xpra-10100:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://YOUR_TOD_HOST:10100"
|
||||
# Add tod-xpra-10101 through tod-xpra-10119 as needed
|
||||
|
||||
tod-api:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://YOUR_TOD_HOST:8001"
|
||||
|
||||
tod-frontend:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://YOUR_TOD_HOST:3000"
|
||||
|
||||
middlewares:
|
||||
xpra-strip:
|
||||
# Strip /xpra/{port} prefix before forwarding
|
||||
stripPrefixRegex:
|
||||
regex: ["^/xpra/[0-9]+"]
|
||||
|
||||
xpra-headers:
|
||||
headers:
|
||||
# Relaxed CSP for Xpra HTML5 client (inline scripts + eval)
|
||||
customResponseHeaders:
|
||||
Content-Security-Policy: "default-src 'self' 'unsafe-inline' 'unsafe-eval' ws: wss: data: blob:; frame-ancestors 'self';"
|
||||
X-Content-Type-Options: "nosniff"
|
||||
# IMPORTANT: Disable compression for Xpra — compressing WebSocket
|
||||
# binary frames corrupts mouse/keyboard coordinate data.
|
||||
|
||||
security-headers:
|
||||
headers:
|
||||
frameDeny: true
|
||||
contentTypeNosniff: true
|
||||
browserXssFilter: true
|
||||
referrerPolicy: "strict-origin-when-cross-origin"
|
||||
customResponseHeaders:
|
||||
X-Frame-Options: "SAMEORIGIN"
|
||||
|
||||
# IMPORTANT: Disable Traefik's built-in compression for Xpra routes.
|
||||
# If using --entrypoints.websecure.http.middlewares=compress@...,
|
||||
# exclude the xpra router or WebSocket binary frames will be corrupted.
|
||||
Reference in New Issue
Block a user