Publish reviewed standalone otche-deploy sources
This commit is contained in:
@@ -0,0 +1 @@
|
||||
**
|
||||
@@ -0,0 +1,10 @@
|
||||
# Copy to .env; no credentials belong here.
|
||||
# Local development: http://localhost:8088 + ALLOW_INSECURE_HTTP=true.
|
||||
# Production: exact public HTTPS origin, Secure cookies remain required.
|
||||
PUBLIC_ORIGIN=https://otche.example.invalid
|
||||
ALLOW_INSECURE_HTTP=false
|
||||
BIND_ADDRESS=127.0.0.1
|
||||
WEB_PORT=8088
|
||||
MAX_UPLOAD_BYTES=536870912
|
||||
MAX_OWNER_BYTES=8589934592
|
||||
MAX_QUEUED_JOBS=20
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
secrets/
|
||||
.runtime/
|
||||
artifacts/
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
coverage/
|
||||
*.tsbuildinfo
|
||||
*.exe
|
||||
*.dll
|
||||
*.pfx
|
||||
*.p12
|
||||
*.key
|
||||
*.pem
|
||||
*.log
|
||||
*.zip
|
||||
*.iso
|
||||
*.mp4
|
||||
*.webm
|
||||
*.dump
|
||||
*.db
|
||||
*.sqlite*
|
||||
*.sql.gz
|
||||
__pycache__/
|
||||
.DS_Store
|
||||
|
||||
.git-credentials
|
||||
.netrc
|
||||
.npmrc
|
||||
*.crt
|
||||
*.cer
|
||||
*.p12
|
||||
coverage/
|
||||
.vite/
|
||||
playwright-report/
|
||||
test-results/
|
||||
@@ -0,0 +1,126 @@
|
||||
# Otche deployment
|
||||
|
||||
Portable deployment of the Otche Windows/Microsoft Defender observation service. The service accepts an immutable file, schedules one Run per qualified Windows profile, records the disposable VM console before delivery, and keeps results and Grub ZIP downloads private to their owner/admin. Observations are **not a certificate of file safety**.
|
||||
|
||||
## Architecture and canonical repositories
|
||||
|
||||
Clone these repositories **as siblings**. There is no monorepo, duplicated backend module, submodule, or generated-source export workflow.
|
||||
|
||||
| Repository | Canonical ownership |
|
||||
|---|---|
|
||||
| [otche-frontend](https://git.qomar.pw/otche/otche-frontend) | React/TypeScript UI, npm lockfile, Vite, nginx and web image |
|
||||
| [otche-backend](https://git.qomar.pw/otche/otche-backend) | One shared Go module; API, worker, watchdog and CLI; embedded PostgreSQL schema; unsigned Windows runner; recorder/extractor and trusted operator tools; API/config contracts |
|
||||
| [otche-deploy](https://git.qomar.pw/otche/otche-deploy) | Compose, nonsecret environment/TLS/systemd examples and this guide |
|
||||
|
||||
API, worker and watchdog are separate processes and containers built from the **same backend Go module**. They share code, not runtime credentials. PostgreSQL is on an internal network with no published port. API has only the database secret and private artifact volume; only worker/watchdog mount operator-supplied PVE configuration. Web has neither. Worker includes FFmpeg/xorriso; Windows runs the signed PowerShell controller. Sample execution requires independently prepared stopped sources and real isolation/source qualification; none is enabled by the default panel-only startup.
|
||||
|
||||
Long-running containers are nonroot, read-only, drop capabilities and use `no-new-privileges`. Named volumes/tmpfs are the only writable application storage. One terminating `storage-init` container sets artifact-volume ownership with only CHOWN/FOWNER. There is no privileged container, host network, Docker socket mount or production database credential in this repository.
|
||||
|
||||
## Prerequisites and exact clone/build commands
|
||||
|
||||
Use a dedicated Linux deployment host with Docker Engine, Compose v2+, Git, Python 3 and sufficient build/data space. Docker Desktop Linux containers can be used for loopback development; Windows-host bind-mounted secret permissions must be checked explicitly. Linux ownership commands below target a Linux host. Source-only builds need Go 1.26 / Node.js 22, but Docker supplies them. Obtain trusted images and verify dependencies under your organization's policy; no license is invented by this publication.
|
||||
|
||||
```sh
|
||||
mkdir otche-workspace
|
||||
cd otche-workspace
|
||||
git clone https://git.qomar.pw/otche/otche-backend.git
|
||||
git clone https://git.qomar.pw/otche/otche-frontend.git
|
||||
git clone https://git.qomar.pw/otche/otche-deploy.git
|
||||
cd otche-deploy
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
For a **loopback-only development panel**, edit `.env` to:
|
||||
|
||||
```dotenv
|
||||
PUBLIC_ORIGIN=http://127.0.0.1:8088
|
||||
ALLOW_INSECURE_HTTP=true
|
||||
BIND_ADDRESS=127.0.0.1
|
||||
WEB_PORT=8088
|
||||
```
|
||||
|
||||
Use a dedicated project name throughout these commands. Compose build contexts are `../otche-backend` and `../otche-frontend`, relative to this repository, not the invoking shell's arbitrary current directory.
|
||||
|
||||
## Generate local secrets without printing them
|
||||
|
||||
The following creates **new** secrets only; it refuses existing files. Run in `otche-deploy` on Linux. Preserve the restrictive directory/file ownership. PostgreSQL alpine uses UID70; backend uses UID10001. A trusted administrator performs this once; private files must never be pasted into Git, `.env`, command arguments, issue reports or screenshots.
|
||||
|
||||
Start a root administration shell **in this directory** and keep every subsequent secret-generation, Compose, bootstrap and operational command in that same root shell. This is intentional: a Docker-group-only caller cannot traverse the root-owned mode0700 secrets directory. Do not solve that by making secrets world-readable. Container file ownership remains UID70/UID10001.
|
||||
|
||||
```sh
|
||||
sudo -s
|
||||
# Confirm: id -u prints 0; pwd ends in otche-deploy.
|
||||
```
|
||||
|
||||
When returning for future administration, enter this directory and repeat `sudo -s`; finish with `exit` after the work. The source clone/npm developer commands in the other repositories do not require root.
|
||||
|
||||
```sh
|
||||
python3 - <<'PY'
|
||||
import os, pathlib, secrets
|
||||
os.umask(0o077)
|
||||
d = pathlib.Path('secrets')
|
||||
d.mkdir(mode=0o700, exist_ok=False)
|
||||
pw = secrets.token_hex(32)
|
||||
values = {
|
||||
'postgres-password': (pw, 70),
|
||||
'database-url': ('postgres://otche:' + pw + '@postgres:5432/otche?sslmode=disable', 10001),
|
||||
'bootstrap-password': (secrets.token_urlsafe(32), 0),
|
||||
}
|
||||
for name, (value, uid) in values.items():
|
||||
p = d / name
|
||||
with p.open('x') as f:
|
||||
f.write(value + '\n')
|
||||
os.chown(p, uid, uid)
|
||||
os.chmod(p, 0o600)
|
||||
PY
|
||||
```
|
||||
|
||||
The DSN's `sslmode=disable` applies only to this isolated Docker database network. External PostgreSQL requires verified TLS. Compose local file secrets are bind mounts: do not rely on ignored Compose `uid`/`mode` metadata to fix host ownership.
|
||||
|
||||
```sh
|
||||
docker compose -p otche-local config --quiet
|
||||
docker compose -p otche-local --profile execution build
|
||||
docker compose -p otche-local up -d --wait
|
||||
```
|
||||
|
||||
The explicit build covers API and worker image targets plus frontend, but the normal `up` does **not** activate the execution profile. `migrate` applies the embedded idempotent schema with an advisory lock; init/migrate then exit successfully. Worker absence is an expected fail-closed readiness blocker for panel-only use.
|
||||
|
||||
Bootstrap the first account via stdin, not a password argument. The protected file is read by the privileged shell; the password is not echoed:
|
||||
|
||||
```sh
|
||||
docker compose -p otche-local run --rm -T api user-create --username administrator --role admin --password-stdin < secrets/bootstrap-password
|
||||
```
|
||||
|
||||
Retrieve the generated password through your approved protected secret-manager workflow to log in at the exact `PUBLIC_ORIGIN`; never expose it in a shared terminal. Replace/remove the temporary bootstrap copy after secure handoff under your retention policy. Passwords must be 14–72 bytes; there is no default account/password. The UI can create further admin/operator accounts. Missing profiles/worker/proofs are genuine empty or blocked states, not demonstration data.
|
||||
|
||||
## Production HTTPS
|
||||
|
||||
Production requires an exact `https://` origin and `ALLOW_INSECURE_HTTP=false`. `examples/nginx-tls.conf` and `examples/compose.tls.yaml` demonstrate direct TLS termination in the existing nonroot web container; substitute your actual DNS hostname, approved certificate chain and private key. The files themselves are not shipped. Mount only server leaf chain/key into web, never a CA private key. nginx needs to read them as UID101; protect their directory and retain appropriate file ACLs/ownership. Keep the bind loopback if using a separate trusted proxy, or explicitly bind your approved interface for direct TLS. Do not expose API/PostgreSQL ports or use wildcard certificate-verification bypasses.
|
||||
|
||||
Set `.env` `COMPOSE_FILE=compose.yaml:examples/compose.tls.yaml`, `PUBLIC_ORIGIN=https://your-approved-hostname`, `WEB_PORT=443`, `BIND_ADDRESS=<approved-interface-address>`. Edit the example `server_name` and provide `secrets/tls/panel-chain.pem` and `secrets/tls/panel-key.pem`. Relative paths in the override are relative to the **first** Compose file (repository root). Verify trusted chain, SAN, validity and private-key match independently; install trust by explicit operator policy only. `docker compose ... exec -T web nginx -t` checks syntax, not certificate trust.
|
||||
|
||||
`examples/otche.service` assumes `/opt/otche-workspace/otche-deploy` with sibling clones and prebuilt images. Review/copy it into systemd only on the intended deployment host. Its stop action stops the app, not the host and not volumes. Do not use it on a PVE node merely because that node exists.
|
||||
|
||||
## Enable Windows execution deliberately
|
||||
|
||||
Follow backend [CONFIG.md](https://git.qomar.pw/otche/otche-backend/src/branch/main/docs/CONFIG.md) and [Source-Setup.txt](https://git.qomar.pw/otche/otche-backend/src/branch/main/windows/Source-Setup.txt). Start with `../otche-backend/provisioning/runtime-config.example.json`, replacing example IDs/paths with reviewed resources in protected `secrets/worker/config.json`. This configuration is mounted as `/run/otche/config.json`; private proof/token/CA paths must match container paths. Seals live on writable `/var/lib/otche/source-seals/`, not the read-only config mount.
|
||||
|
||||
Supply separately scoped provisioner/runtime/recorder/uploader/housekeeping credentials; runtime mounts are UID10001-readable and operator-protected. Authenticate all five through `bindings-sync`; establish genuine owner-isolation evidence and its expiry. Source preparation requires reviewed signed PS1/PSM1, approved execution policy and signing trust, a dedicated local split-token account, real interactive desktop/autologon, active Defender/UAC/Secure Boot and QGA. Never distribute signing private keys or passwords here. Do not run samples or EICAR on source VMs. Existing protected VMIDs 7000/7001 and shared-storage exclusions are deliberate code safety restrictions.
|
||||
|
||||
```sh
|
||||
docker compose -p otche-local --profile execution run --rm worker bindings-sync
|
||||
docker compose -p otche-local --profile execution run --rm worker source-maintenance --source-ref win11-stopped-revision
|
||||
# Operator performs approved maintenance and graceful shutdown separately.
|
||||
docker compose -p otche-local --profile execution run --rm worker source-validate --profile-id ACTUAL_PROFILE_UUID --source-ref win11-stopped-revision
|
||||
docker compose -p otche-local --profile execution up -d worker watchdog
|
||||
```
|
||||
|
||||
Admin queues qualification, reviews actual disposable EICAR/benign/video evidence and publishes the exact passed revision. Optional online and read-only extractor support require their own real matching proofs. Do not manufacture `passed:true`, extend proof expiry by editing dates, remove safety gates, enable broad host privileges or replace private storage with shared aliases. Empty installs intentionally cannot execute files.
|
||||
|
||||
## Operation, backup and updates
|
||||
|
||||
Use `docker compose -p otche-local ps -a` and service logs for diagnosis (logs may contain private job metadata; do not publish them). Authenticate and inspect `/api/v1/admin/health`; panel-only reports database ready and an unavailable worker, not full execution readiness. Login/session endpoints are described by the backend API contract. Browser assets and `/api` are same origin; login requires matching Origin.
|
||||
|
||||
Back up PostgreSQL, artifact volume, source seals/config, keys and credentials as a consistent **private** set. Drain admission/execution before coordinated updates; pull intended reviewed commits in each sibling clone, build, then recreate the application without removing data. After restoring, reconcile allocations and source/proof identity before admitting execution. Never use `down -v`, volume pruning or broad VM deletion on an existing deployment. For a newly created disposable smoke project only, `docker compose -p YOUR_DISPOSABLE_PROJECT down -v --remove-orphans` removes that project's containers/volumes after checking its name.
|
||||
|
||||
No uploaded files, video, ZIP, database dump, live inventory, operational IP addresses, certificates, runtime logs or acceptance proofs are published. Keep those in ignored protected locations, not alongside tracked sources.
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
name: otche
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
user: "70:70"
|
||||
read_only: true
|
||||
tmpfs: ["/tmp:size=32m,noexec,nosuid", "/var/run/postgresql:size=8m,noexec,nosuid"]
|
||||
cap_drop: [ALL]
|
||||
security_opt: [no-new-privileges:true]
|
||||
environment:
|
||||
POSTGRES_DB: otche
|
||||
POSTGRES_USER: otche
|
||||
POSTGRES_PASSWORD_FILE: /run/secrets/postgres-password
|
||||
secrets: [postgres-password]
|
||||
volumes: [postgres-data:/var/lib/postgresql/data]
|
||||
healthcheck:
|
||||
test: [CMD-SHELL, "pg_isready -U otche -d otche"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
restart: unless-stopped
|
||||
networks: [private]
|
||||
mem_limit: 768m
|
||||
storage-init:
|
||||
image: debian:bookworm-slim
|
||||
user: "0:0"
|
||||
command: [sh, -ec, "chown 10001:10001 /var/lib/otche && chmod 0700 /var/lib/otche"]
|
||||
volumes: [artifacts:/var/lib/otche]
|
||||
network_mode: none
|
||||
cap_drop: [ALL]
|
||||
cap_add: [CHOWN, FOWNER]
|
||||
security_opt: [no-new-privileges:true]
|
||||
migrate:
|
||||
build: {context: ../otche-backend, target: api}
|
||||
command: [migrate]
|
||||
environment: {DATABASE_URL_FILE: /run/secrets/database-url}
|
||||
secrets: [database-url]
|
||||
depends_on:
|
||||
postgres: {condition: service_healthy}
|
||||
networks: [private]
|
||||
read_only: true
|
||||
cap_drop: [ALL]
|
||||
security_opt: [no-new-privileges:true]
|
||||
api:
|
||||
build: {context: ../otche-backend, target: api}
|
||||
environment:
|
||||
DATABASE_URL_FILE: /run/secrets/database-url
|
||||
PUBLIC_ORIGIN: ${PUBLIC_ORIGIN:?Set PUBLIC_ORIGIN}
|
||||
ALLOW_INSECURE_HTTP: ${ALLOW_INSECURE_HTTP:-false}
|
||||
ARTIFACT_ROOT: /var/lib/otche
|
||||
MAX_UPLOAD_BYTES: ${MAX_UPLOAD_BYTES:-536870912}
|
||||
MAX_OWNER_BYTES: ${MAX_OWNER_BYTES:-8589934592}
|
||||
MAX_QUEUED_JOBS: ${MAX_QUEUED_JOBS:-20}
|
||||
secrets: [database-url]
|
||||
volumes: [artifacts:/var/lib/otche]
|
||||
depends_on:
|
||||
migrate: {condition: service_completed_successfully}
|
||||
storage-init: {condition: service_completed_successfully}
|
||||
restart: unless-stopped
|
||||
networks: [private, edge]
|
||||
read_only: true
|
||||
tmpfs: ["/tmp:size=32m,noexec,nosuid"]
|
||||
cap_drop: [ALL]
|
||||
security_opt: [no-new-privileges:true]
|
||||
mem_limit: 256m
|
||||
cpus: 1.0
|
||||
worker:
|
||||
build: {context: ../otche-backend, target: worker}
|
||||
command: [worker]
|
||||
profiles: [execution]
|
||||
environment:
|
||||
DATABASE_URL_FILE: /run/secrets/database-url
|
||||
WORKER_CONFIG_FILE: /run/otche/config.json
|
||||
ARTIFACT_ROOT: /var/lib/otche
|
||||
secrets: [database-url]
|
||||
volumes:
|
||||
- artifacts:/var/lib/otche
|
||||
- ./secrets/worker:/run/otche:ro
|
||||
depends_on:
|
||||
migrate: {condition: service_completed_successfully}
|
||||
storage-init: {condition: service_completed_successfully}
|
||||
restart: unless-stopped
|
||||
networks: [private, execution]
|
||||
read_only: true
|
||||
tmpfs: ["/tmp:size=128m,noexec,nosuid"]
|
||||
cap_drop: [ALL]
|
||||
security_opt: [no-new-privileges:true]
|
||||
mem_limit: 1536m
|
||||
cpus: 2.0
|
||||
pids_limit: 128
|
||||
stop_grace_period: 60s
|
||||
watchdog:
|
||||
build: {context: ../otche-backend, target: worker}
|
||||
command: [watchdog]
|
||||
profiles: [execution]
|
||||
environment:
|
||||
DATABASE_URL_FILE: /run/secrets/database-url
|
||||
WORKER_CONFIG_FILE: /run/otche/config.json
|
||||
secrets: [database-url]
|
||||
volumes: ["./secrets/worker:/run/otche:ro"]
|
||||
depends_on:
|
||||
migrate: {condition: service_completed_successfully}
|
||||
restart: unless-stopped
|
||||
networks: [private, execution]
|
||||
read_only: true
|
||||
cap_drop: [ALL]
|
||||
security_opt: [no-new-privileges:true]
|
||||
mem_limit: 128m
|
||||
cpus: 0.25
|
||||
web:
|
||||
build: {context: ../otche-frontend}
|
||||
ports: ["${BIND_ADDRESS:-127.0.0.1}:${WEB_PORT:-8088}:8080"]
|
||||
depends_on: [api]
|
||||
restart: unless-stopped
|
||||
networks: [edge]
|
||||
read_only: true
|
||||
tmpfs: ["/tmp:size=32m,noexec,nosuid", "/var/cache/nginx:size=32m,noexec,nosuid", "/var/run:size=1m,noexec,nosuid"]
|
||||
cap_drop: [ALL]
|
||||
security_opt: [no-new-privileges:true]
|
||||
mem_limit: 128m
|
||||
secrets:
|
||||
postgres-password:
|
||||
file: ./secrets/postgres-password
|
||||
database-url:
|
||||
file: ./secrets/database-url
|
||||
volumes:
|
||||
postgres-data:
|
||||
artifacts:
|
||||
networks:
|
||||
private: {internal: true}
|
||||
edge: {}
|
||||
execution: {}
|
||||
@@ -0,0 +1,6 @@
|
||||
services:
|
||||
web:
|
||||
ports: !override ["${BIND_ADDRESS:-127.0.0.1}:${WEB_PORT:-443}:8443"]
|
||||
volumes:
|
||||
- ./examples/nginx-tls.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./secrets/tls:/etc/otche-tls:ro
|
||||
@@ -0,0 +1,38 @@
|
||||
server {
|
||||
listen 8443 ssl;
|
||||
server_name otche.example.invalid;
|
||||
server_tokens off;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
ssl_certificate /etc/otche-tls/panel-chain.pem;
|
||||
ssl_certificate_key /etc/otche-tls/panel-key.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_session_cache shared:TLS:2m;
|
||||
ssl_session_timeout 10m;
|
||||
ssl_session_tickets off;
|
||||
client_max_body_size 0;
|
||||
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header Referrer-Policy same-origin always;
|
||||
add_header X-Frame-Options DENY always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; media-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'" always;
|
||||
location /api/ {
|
||||
proxy_pass http://api:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
location /assets/ {
|
||||
try_files $uri =404;
|
||||
expires 1y;
|
||||
}
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
expires -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=Otche persistent control panel
|
||||
Requires=docker.service
|
||||
After=docker.service network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
WorkingDirectory=/opt/otche-workspace/otche-deploy
|
||||
ExecStart=/usr/bin/docker compose -p otche-local up -d --no-build
|
||||
ExecStop=/usr/bin/docker compose -p otche-local stop --timeout 60
|
||||
TimeoutStartSec=300
|
||||
TimeoutStopSec=120
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user