commit aed6d1a64cb9b4f0179117fadb9a68a6c188eb4d Author: wolf-demon Date: Fri Oct 2 16:22:16 2026 +0100 inital commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d4e72c8 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Copy this file to .env for local use. Do not put passwords in this file. +# The directory must contain the three secret files documented in README.md. +SECRETS_DIR=./.local/secrets + +# Sandbox-only loopback port for direct Grafana access. +GRAFANA_PORT=3000 + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f61030 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Local environment and generated configuration +.env +.env.* +!.env.example +.local/ + +# Secrets and authentication material +secrets/ +*.htpasswd +*.password +*.token +*.key +*.pem +*.p12 +*.pfx +*.crt +*.cer + +# Runtime and database state +data/ +postgres-data/ +grafana-data/ +loki-data/ +*.dump +*.backup +*.sql.gz +*.tar.gz + +# Logs and unreviewed application samples +*.log +testing/logs/incoming/** +!testing/logs/incoming/.gitkeep + +# Tooling and operating-system files +.DS_Store +Thumbs.db +.idea/ +.vscode/ +__pycache__/ +.pytest_cache/ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..4f376fb --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# Kiosk observability proof of concept + +This repository contains the first local milestone for the kiosk reporting +platform: Grafana OSS backed by a dedicated PostgreSQL metadata database. Loki, +reporting aggregation, Nginx/TLS, tenant bootstrap, and production deployment +will be added in later milestones. + +Grafana is available only through a loopback sandbox port. PostgreSQL has no +published host port. Nginx will replace the direct Grafana port when HTTPS is +introduced. + +## Prerequisites + +- Docker Engine with the Docker Compose plugin +- PowerShell 7 or Windows PowerShell 5.1 for the local secret helper + +The pinned images are `grafana/grafana:13.2.2` and +`postgres:18.6-alpine`. Do not replace them with `latest`. + +## Prepare local secrets + +Copy the non-secret environment template and create three random secret files: + +```powershell +Copy-Item .env.example .env +./scripts/initialize-local-secrets.ps1 +``` + +The helper creates these ignored files without printing their values: + +```text +.local/secrets/grafana_admin_password +.local/secrets/grafana_database_password +.local/secrets/postgres_admin_password +``` + +It does not overwrite an existing secret. For a deployed Linux environment, +create equivalent restricted files below `/etc/kiosk-observability/secrets` and +set `SECRETS_DIR` in the host's untracked `.env` file to that directory. + +## Start and verify + +Start the local stack: + +```powershell +docker compose -f compose.yaml -f compose.sandbox.yaml up -d +docker compose -f compose.yaml -f compose.sandbox.yaml ps +``` + +Open and sign in as `admin` with the value stored in +`.local/secrets/grafana_admin_password`. The health endpoint is +. + +Confirm that PostgreSQL is not published to the host: + +```powershell +docker compose -f compose.yaml -f compose.sandbox.yaml port postgres 5432 +``` + +The command should report that no public port exists. To confirm Grafana is +using PostgreSQL, inspect the health response and container logs; the health +response should report `"database": "ok"`. + +## Stop or reset + +Stop containers while preserving their named volumes: + +```powershell +docker compose -f compose.yaml -f compose.sandbox.yaml down +``` + +Starting the stack again should retain Grafana metadata. Removing volumes +permanently deletes the local databases and must only be done when an intentional +clean reset is required: + +```powershell +docker compose -f compose.yaml -f compose.sandbox.yaml down --volumes +``` + +## Supply test logs + +Put logs awaiting review in `testing/logs/incoming/`. The directory is present +in Git, but its contents are ignored. Read `testing/logs/README.md` before adding +files. Only fully sanitized, explicitly approved samples may later be placed in +`tests/fixtures/`. + +## Validate configuration + +After preparing `.env` and the local secrets, render the merged configuration: + +```powershell +docker compose -f compose.yaml -f compose.sandbox.yaml config +``` + +The output must show secret file paths only. It must not contain the contents of +any password file. Do not commit `.env`, `.local/`, raw logs, certificates, +private keys, database dumps, or runtime data. + diff --git a/compose.sandbox.yaml b/compose.sandbox.yaml new file mode 100644 index 0000000..750e23e --- /dev/null +++ b/compose.sandbox.yaml @@ -0,0 +1,5 @@ +services: + grafana: + ports: + - "127.0.0.1:${GRAFANA_PORT:-3000}:3000" + diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..030b03f --- /dev/null +++ b/compose.yaml @@ -0,0 +1,70 @@ +name: kiosk-observability + +services: + postgres: + image: postgres:18.6-alpine + restart: unless-stopped + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres_admin + POSTGRES_PASSWORD_FILE: /run/secrets/postgres_admin_password + secrets: + - postgres_admin_password + - grafana_database_password + volumes: + - postgres_data:/var/lib/postgresql + - ./postgres/init/10-create-grafana-database.sh:/docker-entrypoint-initdb.d/10-create-grafana-database.sh:ro + networks: + - backend + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres_admin -d postgres"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 10s + + grafana: + image: grafana/grafana:13.2.2 + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + environment: + GF_DATABASE_TYPE: postgres + GF_DATABASE_HOST: postgres:5432 + GF_DATABASE_NAME: grafana + GF_DATABASE_USER: grafana + GF_DATABASE_PASSWORD__FILE: /run/secrets/grafana_database_password + GF_SECURITY_ADMIN_USER: admin + GF_SECURITY_ADMIN_PASSWORD__FILE: /run/secrets/grafana_admin_password + secrets: + - grafana_admin_password + - grafana_database_password + volumes: + - grafana_data:/var/lib/grafana + - ./grafana/grafana.ini:/etc/grafana/grafana.ini:ro + - ./grafana/provisioning:/etc/grafana/provisioning:ro + networks: + - backend + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 20s + +networks: + backend: + internal: true + +volumes: + grafana_data: + postgres_data: + +secrets: + grafana_admin_password: + file: ${SECRETS_DIR:-./.local/secrets}/grafana_admin_password + grafana_database_password: + file: ${SECRETS_DIR:-./.local/secrets}/grafana_database_password + postgres_admin_password: + file: ${SECRETS_DIR:-./.local/secrets}/postgres_admin_password diff --git a/grafana/grafana.ini b/grafana/grafana.ini new file mode 100644 index 0000000..037cc03 --- /dev/null +++ b/grafana/grafana.ini @@ -0,0 +1,38 @@ +app_mode = production + +[server] +protocol = http +http_addr = 0.0.0.0 +http_port = 3000 +domain = localhost +root_url = http://localhost:3000/ +enforce_domain = false + +[database] +ssl_mode = disable + +[analytics] +reporting_enabled = false +check_for_updates = false +check_for_plugin_updates = false + +[security] +disable_gravatar = true +cookie_secure = false +cookie_samesite = strict +strict_transport_security = false + +[users] +allow_sign_up = false +allow_org_create = false +auto_assign_org = false + +[auth.anonymous] +enabled = false + +[log] +mode = console +level = info + +[paths] +provisioning = /etc/grafana/provisioning diff --git a/grafana/provisioning/README.md b/grafana/provisioning/README.md new file mode 100644 index 0000000..b941fab --- /dev/null +++ b/grafana/provisioning/README.md @@ -0,0 +1,6 @@ +# Grafana provisioning + +Version-controlled data-source and dashboard provisioning will be added here as +the Loki and reporting services are implemented. The empty subdirectories are +intentional and keep the eventual layout stable. + diff --git a/grafana/provisioning/dashboards/.gitkeep b/grafana/provisioning/dashboards/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/grafana/provisioning/dashboards/.gitkeep @@ -0,0 +1 @@ + diff --git a/grafana/provisioning/datasources/.gitkeep b/grafana/provisioning/datasources/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/grafana/provisioning/datasources/.gitkeep @@ -0,0 +1 @@ + diff --git a/postgres/init/10-create-grafana-database.sh b/postgres/init/10-create-grafana-database.sh new file mode 100644 index 0000000..44c416e --- /dev/null +++ b/postgres/init/10-create-grafana-database.sh @@ -0,0 +1,36 @@ +#!/bin/sh +set -eu + +password_file=/run/secrets/grafana_database_password + +if [ ! -r "$password_file" ]; then + echo "Grafana database password file is missing or unreadable" >&2 + exit 1 +fi + +grafana_password=$(cat "$password_file") + +if [ -z "$grafana_password" ]; then + echo "Grafana database password must not be empty" >&2 + exit 1 +fi + +psql \ + --set=ON_ERROR_STOP=1 \ + --set=grafana_password="$grafana_password" \ + --username "$POSTGRES_USER" \ + --dbname "$POSTGRES_DB" <<-'EOSQL' +SELECT format('CREATE ROLE grafana LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT PASSWORD %L', :'grafana_password') +WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'grafana') \gexec + +ALTER ROLE grafana PASSWORD :'grafana_password'; + +SELECT 'CREATE DATABASE grafana OWNER grafana' +WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_database WHERE datname = 'grafana') \gexec + +REVOKE ALL ON DATABASE grafana FROM PUBLIC; +GRANT CONNECT, TEMPORARY ON DATABASE grafana TO grafana; +EOSQL + +unset grafana_password + diff --git a/scripts/initialize-local-secrets.ps1 b/scripts/initialize-local-secrets.ps1 new file mode 100644 index 0000000..a5afdcd --- /dev/null +++ b/scripts/initialize-local-secrets.ps1 @@ -0,0 +1,39 @@ +[CmdletBinding()] +param( + [string]$Destination = (Join-Path $PSScriptRoot "..\.local\secrets") +) + +$ErrorActionPreference = "Stop" +$secretNames = @( + "grafana_admin_password", + "grafana_database_password", + "postgres_admin_password" +) + +$resolvedDestination = [System.IO.Path]::GetFullPath($Destination) +[System.IO.Directory]::CreateDirectory($resolvedDestination) | Out-Null + +foreach ($secretName in $secretNames) { + $secretPath = Join-Path $resolvedDestination $secretName + + if (Test-Path -LiteralPath $secretPath) { + Write-Host "Keeping existing secret: $secretName" + continue + } + + $bytes = New-Object byte[] 32 + $generator = [System.Security.Cryptography.RandomNumberGenerator]::Create() + try { + $generator.GetBytes($bytes) + } + finally { + $generator.Dispose() + } + + $value = [Convert]::ToBase64String($bytes) + [System.IO.File]::WriteAllText($secretPath, $value, [System.Text.UTF8Encoding]::new($false)) + Write-Host "Created secret: $secretName" +} + +Write-Host "Local secrets are ready in $resolvedDestination" + diff --git a/testing/logs/README.md b/testing/logs/README.md new file mode 100644 index 0000000..99468de --- /dev/null +++ b/testing/logs/README.md @@ -0,0 +1,16 @@ +# Kiosk log intake + +Place log files for initial review in `incoming/`. Everything below that +directory is ignored by Git so an unreviewed file cannot be committed by +accident. + +Logs may still contain personal or payment-related information. Before a sample +is approved as a version-controlled test fixture, replace guest names, booking +references, room numbers, session and correlation identifiers, transaction +identifiers, payment details, free-text notes, and any other identifying values. +Keep the original line structure, delimiters, timestamps, event names, and field +layout so that parser development remains representative. + +Reviewed and fully sanitized samples can later be copied deliberately into +`tests/fixtures/`. There is no automatic promotion from this inbox. + diff --git a/testing/logs/incoming/.gitkeep b/testing/logs/incoming/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/testing/logs/incoming/.gitkeep @@ -0,0 +1 @@ + diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md new file mode 100644 index 0000000..7d02ac9 --- /dev/null +++ b/tests/fixtures/README.md @@ -0,0 +1,6 @@ +# Approved log fixtures + +Only reviewed, synthetic or fully sanitized log samples belong here. Raw files +from `testing/logs/incoming/` must never be copied here without an explicit +privacy review. +