diff --git a/.env.example b/.env.example index 8e66aec..8f582f8 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,26 @@ # Silo Environment Configuration -# Copy this file to .env and update values as needed +# Copy to .env (or deployments/.env) and update values as needed. +# For automated setup, run: ./scripts/setup-docker.sh # PostgreSQL POSTGRES_PASSWORD=silodev # MinIO -MINIO_ACCESS_KEY=minioadmin -MINIO_SECRET_KEY=minioadmin +MINIO_ACCESS_KEY=silominio +MINIO_SECRET_KEY=silominiosecret -# Silo API (optional overrides) -# SILO_SERVER_PORT=8080 +# OpenLDAP +LDAP_ADMIN_PASSWORD=ldapadmin +LDAP_USERS=siloadmin +LDAP_PASSWORDS=siloadmin + +# Silo Authentication +SILO_SESSION_SECRET=change-me-in-production +SILO_ADMIN_USERNAME=admin +SILO_ADMIN_PASSWORD=admin + +# Optional: OIDC (Keycloak) +# SILO_OIDC_CLIENT_SECRET= + +# Optional: LDAP service account +# SILO_LDAP_BIND_PASSWORD= diff --git a/.gitignore b/.gitignore index 0527ec8..0cbb933 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ Thumbs.db # Config with secrets config.yaml *.env +deployments/config.docker.yaml # Python __pycache__/ diff --git a/README.md b/README.md index 4dedb2e..2dce04e 100644 --- a/README.md +++ b/README.md @@ -53,15 +53,20 @@ silo/ ## Quick Start -```bash -# Docker Compose (quickest) -cp config.example.yaml config.yaml -# Edit config.yaml with your database, MinIO, and auth settings -make docker-up +See the **[Installation Guide](docs/INSTALL.md)** for complete setup instructions. -# Or manual setup -psql -h localhost -U silo -d silo -f migrations/*.sql -go run ./cmd/silod -config config.yaml +**Docker Compose (quickest — includes PostgreSQL, MinIO, OpenLDAP, and Silo):** + +```bash +./scripts/setup-docker.sh +docker compose -f deployments/docker-compose.allinone.yaml up -d +``` + +**Development (local Go + Docker services):** + +```bash +make docker-up # Start PostgreSQL + MinIO in Docker +make run # Run silo locally with Go ``` When auth is enabled, a default admin account is created on first startup using the credentials in `config.yaml` under `auth.local.default_admin_username` and `auth.local.default_admin_password`. @@ -104,9 +109,10 @@ The server provides the REST API and ODS endpoints consumed by these clients. | Document | Description | |----------|-------------| +| [docs/INSTALL.md](docs/INSTALL.md) | Installation guide (Docker Compose and daemon) | | [docs/SPECIFICATION.md](docs/SPECIFICATION.md) | Full design specification and API reference | | [docs/STATUS.md](docs/STATUS.md) | Implementation status | -| [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) | Production deployment guide | +| [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) | Production deployment and operations guide | | [docs/CONFIGURATION.md](docs/CONFIGURATION.md) | Configuration reference (all `config.yaml` options) | | [docs/AUTH.md](docs/AUTH.md) | Authentication system design | | [docs/AUTH_USER_GUIDE.md](docs/AUTH_USER_GUIDE.md) | User guide for login, tokens, and roles | diff --git a/config.example.yaml b/config.example.yaml index ec8677a..74f1f89 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -8,20 +8,20 @@ server: # read_only: false # Reject all write operations; toggle at runtime with SIGUSR1 database: - host: "psql.example.internal" + host: "localhost" # Use "postgres" for Docker Compose port: 5432 name: "silo" user: "silo" password: "" # Use SILO_DB_PASSWORD env var - sslmode: "require" + sslmode: "require" # Use "disable" for Docker Compose (internal network) max_connections: 10 storage: - endpoint: "minio.example.internal:9000" + endpoint: "localhost:9000" # Use "minio:9000" for Docker Compose access_key: "" # Use SILO_MINIO_ACCESS_KEY env var secret_key: "" # Use SILO_MINIO_SECRET_KEY env var bucket: "silo-files" - use_ssl: true + use_ssl: true # Use false for Docker Compose (internal network) region: "us-east-1" schemas: diff --git a/deployments/config.dev.yaml b/deployments/config.dev.yaml new file mode 100644 index 0000000..9f57d05 --- /dev/null +++ b/deployments/config.dev.yaml @@ -0,0 +1,35 @@ +# Silo Development Configuration +# Used by deployments/docker-compose.yaml — works with zero setup via `make docker-up`. +# For production Docker installs, run scripts/setup-docker.sh instead. + +server: + host: "0.0.0.0" + port: 8080 + base_url: "http://localhost:8080" + +database: + host: "postgres" + port: 5432 + name: "silo" + user: "silo" + password: "${POSTGRES_PASSWORD:-silodev}" + sslmode: "disable" + max_connections: 10 + +storage: + endpoint: "minio:9000" + access_key: "${MINIO_ACCESS_KEY:-silominio}" + secret_key: "${MINIO_SECRET_KEY:-silominiosecret}" + bucket: "silo-files" + use_ssl: false + region: "us-east-1" + +schemas: + directory: "/etc/silo/schemas" + default: "kindred-rd" + +freecad: + uri_scheme: "silo" + +auth: + enabled: false diff --git a/deployments/docker-compose.allinone.yaml b/deployments/docker-compose.allinone.yaml new file mode 100644 index 0000000..6d73fc8 --- /dev/null +++ b/deployments/docker-compose.allinone.yaml @@ -0,0 +1,172 @@ +# Silo All-in-One Stack +# PostgreSQL + MinIO + OpenLDAP + Silo API + Nginx (optional) +# +# Quick start: +# ./scripts/setup-docker.sh +# docker compose -f deployments/docker-compose.allinone.yaml up -d +# +# With nginx reverse proxy: +# docker compose -f deployments/docker-compose.allinone.yaml --profile nginx up -d +# +# View logs: +# docker compose -f deployments/docker-compose.allinone.yaml logs -f +# +# Stop: +# docker compose -f deployments/docker-compose.allinone.yaml down +# +# Stop and delete data: +# docker compose -f deployments/docker-compose.allinone.yaml down -v + +services: + # --------------------------------------------------------------------------- + # PostgreSQL 16 + # --------------------------------------------------------------------------- + postgres: + image: postgres:16-alpine + container_name: silo-postgres + restart: unless-stopped + environment: + POSTGRES_DB: silo + POSTGRES_USER: silo + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Run ./scripts/setup-docker.sh first} + volumes: + - postgres_data:/var/lib/postgresql/data + - ../migrations:/docker-entrypoint-initdb.d:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U silo -d silo"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - silo-net + + # --------------------------------------------------------------------------- + # MinIO (S3-compatible object storage) + # --------------------------------------------------------------------------- + minio: + image: minio/minio:latest + container_name: silo-minio + restart: unless-stopped + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:?Run ./scripts/setup-docker.sh first} + MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:?Run ./scripts/setup-docker.sh first} + volumes: + - minio_data:/data + ports: + - "9001:9001" # MinIO console (remove in hardened setups) + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - silo-net + + # --------------------------------------------------------------------------- + # OpenLDAP (user directory for LDAP authentication) + # --------------------------------------------------------------------------- + openldap: + image: bitnami/openldap:2.6 + container_name: silo-openldap + restart: unless-stopped + environment: + LDAP_ROOT: "dc=silo,dc=local" + LDAP_ADMIN_USERNAME: "admin" + LDAP_ADMIN_PASSWORD: ${LDAP_ADMIN_PASSWORD:?Run ./scripts/setup-docker.sh first} + LDAP_USERS: ${LDAP_USERS:-siloadmin} + LDAP_PASSWORDS: ${LDAP_PASSWORDS:?Run ./scripts/setup-docker.sh first} + LDAP_GROUP: "silo-users" + LDAP_USER_OU: "users" + LDAP_GROUP_OU: "groups" + volumes: + - openldap_data:/bitnami/openldap + - ./ldap:/docker-entrypoint-initdb.d:ro + ports: + - "1389:1389" # LDAP access for debugging (remove in hardened setups) + healthcheck: + test: ["CMD-SHELL", "ldapsearch -x -H ldap://localhost:1389 -b dc=silo,dc=local -D cn=admin,dc=silo,dc=local -w $${LDAP_ADMIN_PASSWORD} '(objectClass=organization)' >/dev/null 2>&1"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - silo-net + + # --------------------------------------------------------------------------- + # Silo API Server + # --------------------------------------------------------------------------- + silo: + build: + context: .. + dockerfile: build/package/Dockerfile + container_name: silo-api + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + minio: + condition: service_healthy + openldap: + condition: service_healthy + env_file: + - .env + environment: + # These override values in config.docker.yaml via the Go config loader's + # direct env var support (see internal/config/config.go). + SILO_DB_HOST: postgres + SILO_DB_NAME: silo + SILO_DB_USER: silo + SILO_DB_PASSWORD: ${POSTGRES_PASSWORD} + SILO_MINIO_ENDPOINT: minio:9000 + SILO_MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} + SILO_MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} + ports: + - "${SILO_PORT:-8080}:8080" + volumes: + - ../schemas:/etc/silo/schemas:ro + - ./config.docker.yaml:/etc/silo/config.yaml:ro + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 15s + networks: + - silo-net + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # --------------------------------------------------------------------------- + # Nginx reverse proxy (optional — enable with --profile nginx) + # --------------------------------------------------------------------------- + nginx: + image: nginx:alpine + container_name: silo-nginx + restart: unless-stopped + profiles: + - nginx + depends_on: + silo: + condition: service_healthy + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro + # Uncomment to mount TLS certificates: + # - /path/to/cert.pem:/etc/nginx/ssl/cert.pem:ro + # - /path/to/key.pem:/etc/nginx/ssl/key.pem:ro + networks: + - silo-net + +volumes: + postgres_data: + minio_data: + openldap_data: + +networks: + silo-net: + driver: bridge diff --git a/deployments/docker-compose.prod.yaml b/deployments/docker-compose.prod.yaml index b540e85..88b7175 100644 --- a/deployments/docker-compose.prod.yaml +++ b/deployments/docker-compose.prod.yaml @@ -16,22 +16,22 @@ services: restart: unless-stopped environment: # Database connection (psql.example.internal) + # Supported as direct env var overrides in the Go config loader: SILO_DB_HOST: psql.example.internal - SILO_DB_PORT: 5432 SILO_DB_NAME: silo SILO_DB_USER: silo SILO_DB_PASSWORD: ${SILO_DB_PASSWORD:?Database password required} - SILO_DB_SSLMODE: require + # Note: SILO_DB_PORT and SILO_DB_SSLMODE are NOT supported as direct + # env var overrides. Set these in config.yaml instead, or use ${VAR} + # syntax in the YAML file. See docs/CONFIGURATION.md for details. # MinIO storage (minio.example.internal) + # Supported as direct env var overrides: SILO_MINIO_ENDPOINT: minio.example.internal:9000 SILO_MINIO_ACCESS_KEY: ${SILO_MINIO_ACCESS_KEY:?MinIO access key required} SILO_MINIO_SECRET_KEY: ${SILO_MINIO_SECRET_KEY:?MinIO secret key required} - SILO_MINIO_BUCKET: silo-files - SILO_MINIO_USE_SSL: "true" - - # Server settings - SILO_SERVER_BASE_URL: ${SILO_BASE_URL:-http://silo.example.internal:8080} + # Note: SILO_MINIO_BUCKET and SILO_MINIO_USE_SSL are NOT supported as + # direct env var overrides. Set these in config.yaml instead. ports: - "8080:8080" volumes: diff --git a/deployments/docker-compose.yaml b/deployments/docker-compose.yaml index d733fe1..c13425b 100644 --- a/deployments/docker-compose.yaml +++ b/deployments/docker-compose.yaml @@ -69,7 +69,7 @@ services: - "8080:8080" volumes: - ../schemas:/etc/silo/schemas:ro - - ../configs/config.yaml:/etc/silo/config.yaml:ro + - ./config.dev.yaml:/etc/silo/config.yaml:ro healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"] interval: 10s diff --git a/deployments/ldap/memberof.ldif b/deployments/ldap/memberof.ldif new file mode 100644 index 0000000..533b79b --- /dev/null +++ b/deployments/ldap/memberof.ldif @@ -0,0 +1,36 @@ +# Enable the memberOf overlay for OpenLDAP. +# When a user is added to a groupOfNames, their entry automatically +# gets a memberOf attribute pointing to the group DN. +# This is required for Silo's LDAP role mapping. +# +# Loaded automatically by bitnami/openldap from /docker-entrypoint-initdb.d/ + +dn: cn=module{0},cn=config +changetype: modify +add: olcModuleLoad +olcModuleLoad: memberof + +dn: olcOverlay=memberof,olcDatabase={2}mdb,cn=config +changetype: add +objectClass: olcOverlayConfig +objectClass: olcMemberOf +olcOverlay: memberof +olcMemberOfRefInt: TRUE +olcMemberOfDangling: ignore +olcMemberOfGroupOC: groupOfNames +olcMemberOfMemberAD: member +olcMemberOfMemberOfAD: memberOf + +# Enable refint overlay to maintain referential integrity +# (removes memberOf when a user is removed from a group) +dn: cn=module{0},cn=config +changetype: modify +add: olcModuleLoad +olcModuleLoad: refint + +dn: olcOverlay=refint,olcDatabase={2}mdb,cn=config +changetype: add +objectClass: olcOverlayConfig +objectClass: olcRefintConfig +olcOverlay: refint +olcRefintAttribute: memberOf member diff --git a/deployments/ldap/silo-groups.ldif b/deployments/ldap/silo-groups.ldif new file mode 100644 index 0000000..0876f92 --- /dev/null +++ b/deployments/ldap/silo-groups.ldif @@ -0,0 +1,34 @@ +# Create Silo role groups for LDAP-based access control. +# These groups map to Silo roles via auth.ldap.role_mapping in config.yaml. +# +# Group hierarchy: +# silo-admins -> admin role (full access) +# silo-users -> editor role (create/modify items) +# silo-viewers -> viewer role (read-only) +# +# The initial LDAP user (set via LDAP_USERS env var) is added to silo-admins. +# Additional users can be added with ldapadd or ldapmodify. +# +# Loaded automatically by bitnami/openldap from /docker-entrypoint-initdb.d/ +# Note: This runs after the default tree is created (users/groups OUs exist). + +# Admin group — initial user is a member +dn: cn=silo-admins,ou=groups,dc=silo,dc=local +objectClass: groupOfNames +cn: silo-admins +description: Silo administrators (full access) +member: cn=siloadmin,ou=users,dc=silo,dc=local + +# Editor group +dn: cn=silo-users,ou=groups,dc=silo,dc=local +objectClass: groupOfNames +cn: silo-users +description: Silo editors (create and modify items) +member: cn=placeholder,ou=users,dc=silo,dc=local + +# Viewer group +dn: cn=silo-viewers,ou=groups,dc=silo,dc=local +objectClass: groupOfNames +cn: silo-viewers +description: Silo viewers (read-only access) +member: cn=placeholder,ou=users,dc=silo,dc=local diff --git a/deployments/nginx/nginx-nossl.conf b/deployments/nginx/nginx-nossl.conf new file mode 100644 index 0000000..52c5fa9 --- /dev/null +++ b/deployments/nginx/nginx-nossl.conf @@ -0,0 +1,44 @@ +# Silo Nginx Reverse Proxy — HTTP only (no TLS) +# +# Use this when TLS is terminated by an external load balancer or when +# running on a trusted internal network without HTTPS. + +upstream silo_backend { + server silo:8080; +} + +server { + listen 80; + listen [::]:80; + server_name _; + + location / { + proxy_pass http://silo_backend; + 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_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + # SSE support + proxy_set_header Connection ""; + proxy_buffering off; + + # Timeouts + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 300s; + + # File uploads (CAD files can be large) + client_max_body_size 100M; + } + + location /nginx-health { + access_log off; + return 200 "OK\n"; + add_header Content-Type text/plain; + } +} diff --git a/deployments/nginx/nginx.conf b/deployments/nginx/nginx.conf new file mode 100644 index 0000000..6c48500 --- /dev/null +++ b/deployments/nginx/nginx.conf @@ -0,0 +1,103 @@ +# Silo Nginx Reverse Proxy (Docker) +# +# HTTP reverse proxy with optional HTTPS. To enable TLS: +# 1. Uncomment the ssl server block below +# 2. Mount your certificate and key in docker-compose: +# volumes: +# - /path/to/cert.pem:/etc/nginx/ssl/cert.pem:ro +# - /path/to/key.pem:/etc/nginx/ssl/key.pem:ro +# 3. Uncomment the HTTP-to-HTTPS redirect in the port 80 block + +upstream silo_backend { + server silo:8080; +} + +# HTTP server +server { + listen 80; + listen [::]:80; + server_name _; + + # Uncomment the next line to redirect all HTTP traffic to HTTPS + # return 301 https://$host$request_uri; + + location / { + proxy_pass http://silo_backend; + 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_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + # SSE support + proxy_set_header Connection ""; + proxy_buffering off; + + # Timeouts + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 300s; + + # File uploads (CAD files can be large) + client_max_body_size 100M; + } + + # Health check endpoint for monitoring + location /nginx-health { + access_log off; + return 200 "OK\n"; + add_header Content-Type text/plain; + } +} + +# Uncomment for HTTPS (mount certs in docker-compose volumes) +# server { +# listen 443 ssl http2; +# listen [::]:443 ssl http2; +# server_name _; +# +# ssl_certificate /etc/nginx/ssl/cert.pem; +# ssl_certificate_key /etc/nginx/ssl/key.pem; +# +# ssl_protocols TLSv1.2 TLSv1.3; +# ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; +# ssl_prefer_server_ciphers off; +# ssl_session_timeout 1d; +# ssl_session_cache shared:SSL:10m; +# ssl_session_tickets off; +# +# # Security headers +# add_header X-Frame-Options "SAMEORIGIN" always; +# add_header X-Content-Type-Options "nosniff" always; +# add_header Referrer-Policy "strict-origin-when-cross-origin" always; +# +# location / { +# proxy_pass http://silo_backend; +# 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_set_header X-Forwarded-Host $host; +# proxy_set_header X-Forwarded-Port $server_port; +# +# proxy_set_header Connection ""; +# proxy_buffering off; +# +# proxy_connect_timeout 60s; +# proxy_send_timeout 60s; +# proxy_read_timeout 300s; +# +# client_max_body_size 100M; +# } +# +# location /nginx-health { +# access_log off; +# return 200 "OK\n"; +# add_header Content-Type text/plain; +# } +# } diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index ee78902..d6e2e3f 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,5 +1,9 @@ # Silo Production Deployment Guide +> **First-time setup?** See the [Installation Guide](INSTALL.md) for step-by-step +> instructions. This document covers ongoing maintenance and operations for an +> existing deployment. + This guide covers deploying Silo to a dedicated VM using external PostgreSQL and MinIO services. ## Table of Contents diff --git a/docs/INSTALL.md b/docs/INSTALL.md new file mode 100644 index 0000000..9429928 --- /dev/null +++ b/docs/INSTALL.md @@ -0,0 +1,518 @@ +# Installing Silo + +This guide covers two installation methods: + +- **[Option A: Docker Compose](#option-a-docker-compose)** — self-contained stack with all services. Recommended for evaluation, small teams, and environments where Docker is the standard. +- **[Option B: Daemon Install](#option-b-daemon-install-systemd--external-services)** — systemd service with external PostgreSQL, MinIO, and optional LDAP/nginx. Recommended for production deployments integrated with existing infrastructure. + +Both methods produce the same result: a running Silo server with a web UI, REST API, and authentication. + +--- + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Option A: Docker Compose](#option-a-docker-compose) + - [A.1 Prerequisites](#a1-prerequisites) + - [A.2 Clone the Repository](#a2-clone-the-repository) + - [A.3 Run the Setup Script](#a3-run-the-setup-script) + - [A.4 Start the Stack](#a4-start-the-stack) + - [A.5 Verify the Installation](#a5-verify-the-installation) + - [A.6 LDAP Users and Groups](#a6-ldap-users-and-groups) + - [A.7 Optional: Enable Nginx Reverse Proxy](#a7-optional-enable-nginx-reverse-proxy) + - [A.8 Stopping, Starting, and Upgrading](#a8-stopping-starting-and-upgrading) +- [Option B: Daemon Install (systemd + External Services)](#option-b-daemon-install-systemd--external-services) + - [B.1 Architecture Overview](#b1-architecture-overview) + - [B.2 Prerequisites](#b2-prerequisites) + - [B.3 Set Up External Services](#b3-set-up-external-services) + - [B.4 Prepare the Host](#b4-prepare-the-host) + - [B.5 Configure Credentials](#b5-configure-credentials) + - [B.6 Deploy](#b6-deploy) + - [B.7 Set Up Nginx and TLS](#b7-set-up-nginx-and-tls) + - [B.8 Verify the Installation](#b8-verify-the-installation) + - [B.9 Upgrading](#b9-upgrading) +- [Post-Install Configuration](#post-install-configuration) +- [Further Reading](#further-reading) + +--- + +## Prerequisites + +Regardless of which method you choose: + +- **Git** to clone the repository +- A machine with at least **2 GB RAM** and **10 GB free disk** +- Network access to pull container images or download Go/Node toolchains + +--- + +## Option A: Docker Compose + +A single Docker Compose file runs everything: PostgreSQL, MinIO, OpenLDAP, and Silo. An optional nginx container can be enabled for reverse proxying. + +### A.1 Prerequisites + +- [Docker Engine](https://docs.docker.com/engine/install/) 24+ with the [Compose plugin](https://docs.docker.com/compose/install/) (v2) +- `openssl` (used by the setup script to generate secrets) + +Verify your installation: + +```bash +docker --version # Docker Engine 24+ +docker compose version # Docker Compose v2+ +``` + +### A.2 Clone the Repository + +```bash +git clone https://git.kindred-systems.com/kindred/silo.git +cd silo +``` + +### A.3 Run the Setup Script + +The setup script generates credentials and configuration files: + +```bash +./scripts/setup-docker.sh +``` + +It prompts for: +- Server domain (default: `localhost`) +- PostgreSQL password (auto-generated if you press Enter) +- MinIO credentials (auto-generated) +- OpenLDAP admin password and initial user (auto-generated) +- Silo local admin account (fallback when LDAP is unavailable) + +For automated/CI environments, use non-interactive mode: + +```bash +./scripts/setup-docker.sh --non-interactive +``` + +The script writes two files: +- `deployments/.env` — secrets for Docker Compose +- `deployments/config.docker.yaml` — Silo server configuration + +### A.4 Start the Stack + +```bash +docker compose -f deployments/docker-compose.allinone.yaml up -d +``` + +Wait for all services to become healthy: + +```bash +docker compose -f deployments/docker-compose.allinone.yaml ps +``` + +You should see `silo-postgres`, `silo-minio`, `silo-openldap`, and `silo-api` all in a healthy state. + +View logs: + +```bash +# All services +docker compose -f deployments/docker-compose.allinone.yaml logs -f + +# Silo only +docker compose -f deployments/docker-compose.allinone.yaml logs -f silo +``` + +### A.5 Verify the Installation + +```bash +# Health check +curl http://localhost:8080/health + +# Readiness check (includes database and storage connectivity) +curl http://localhost:8080/ready +``` + +Open http://localhost:8080 in your browser. Log in with either: + +- **LDAP account**: the username and password shown by the setup script (default: `siloadmin`) +- **Local admin**: the local admin credentials shown by the setup script (default: `admin`) + +The credentials were printed at the end of the setup script output and are stored in `deployments/.env`. + +### A.6 LDAP Users and Groups + +The Docker stack includes an OpenLDAP server with three preconfigured groups that map to Silo roles: + +| LDAP Group | Silo Role | Access Level | +|------------|-----------|-------------| +| `cn=silo-admins,ou=groups,dc=silo,dc=local` | admin | Full access | +| `cn=silo-users,ou=groups,dc=silo,dc=local` | editor | Create and modify items | +| `cn=silo-viewers,ou=groups,dc=silo,dc=local` | viewer | Read-only | + +The initial LDAP user (default: `siloadmin`) is added to `silo-admins`. + +**Add a new LDAP user:** + +```bash +# From the host (using the exposed port) +ldapadd -x -H ldap://localhost:1389 \ + -D "cn=admin,dc=silo,dc=local" \ + -w "YOUR_LDAP_ADMIN_PASSWORD" << EOF +dn: cn=jdoe,ou=users,dc=silo,dc=local +objectClass: inetOrgPerson +cn: jdoe +sn: Doe +userPassword: changeme +mail: jdoe@example.com +EOF +``` + +**Add a user to a group:** + +```bash +ldapmodify -x -H ldap://localhost:1389 \ + -D "cn=admin,dc=silo,dc=local" \ + -w "YOUR_LDAP_ADMIN_PASSWORD" << EOF +dn: cn=silo-users,ou=groups,dc=silo,dc=local +changetype: modify +add: member +member: cn=jdoe,ou=users,dc=silo,dc=local +EOF +``` + +**List all users:** + +```bash +ldapsearch -x -H ldap://localhost:1389 \ + -b "ou=users,dc=silo,dc=local" \ + -D "cn=admin,dc=silo,dc=local" \ + -w "YOUR_LDAP_ADMIN_PASSWORD" "(objectClass=inetOrgPerson)" cn mail memberOf +``` + +### A.7 Optional: Enable Nginx Reverse Proxy + +To place nginx in front of Silo (for TLS termination or to serve on port 80): + +```bash +docker compose -f deployments/docker-compose.allinone.yaml --profile nginx up -d +``` + +By default nginx listens on ports 80 and 443 and proxies to the Silo container. The configuration is at `deployments/nginx/nginx.conf`. + +**To enable HTTPS**, edit `deployments/docker-compose.allinone.yaml` and uncomment the TLS certificate volume mounts in the `nginx` service, then uncomment the HTTPS server block in `deployments/nginx/nginx.conf`. See the comments in those files for details. + +If you already have your own reverse proxy or load balancer, skip the nginx profile and point your proxy at port 8080. + +### A.8 Stopping, Starting, and Upgrading + +```bash +# Stop the stack (data is preserved in Docker volumes) +docker compose -f deployments/docker-compose.allinone.yaml down + +# Start again +docker compose -f deployments/docker-compose.allinone.yaml up -d + +# Stop and delete all data (WARNING: destroys database, files, and LDAP data) +docker compose -f deployments/docker-compose.allinone.yaml down -v +``` + +**To upgrade to a newer version:** + +```bash +cd silo +git pull +docker compose -f deployments/docker-compose.allinone.yaml up -d --build +``` + +The Silo container is rebuilt from the updated source. Database migrations in `migrations/` are applied automatically on container startup via the PostgreSQL init mechanism. + +--- + +## Option B: Daemon Install (systemd + External Services) + +This method runs Silo as a systemd service on a dedicated host, connecting to externally managed PostgreSQL, MinIO, and optionally LDAP services. + +### B.1 Architecture Overview + +``` + ┌──────────────────────┐ + │ Silo Host │ + │ ┌────────────────┐ │ + HTTPS (443) ──►│ │ nginx │ │ + │ └───────┬────────┘ │ + │ │ :8080 │ + │ ┌───────▼────────┐ │ + │ │ silod │ │ + │ │ (API server) │ │ + │ └──┬─────────┬───┘ │ + └─────┼─────────┼──────┘ + │ │ + ┌───────────▼──┐ ┌───▼──────────────┐ + │ PostgreSQL 16│ │ MinIO (S3) │ + │ :5432 │ │ :9000 API │ + └──────────────┘ │ :9001 Console │ + └──────────────────┘ +``` + +### B.2 Prerequisites + +- Linux host (Debian/Ubuntu or RHEL/Fedora/AlmaLinux) +- Root or sudo access +- Network access to your PostgreSQL and MinIO servers + +The setup script installs Go and other build dependencies automatically. + +### B.3 Set Up External Services + +#### PostgreSQL 16 + +Install PostgreSQL and create the Silo database: + +- [PostgreSQL downloads](https://www.postgresql.org/download/) + +```bash +# After installing PostgreSQL, create the database and user: +sudo -u postgres createuser silo +sudo -u postgres createdb -O silo silo +sudo -u postgres psql -c "ALTER USER silo WITH PASSWORD 'your-password';" +``` + +Ensure the Silo host can connect (check `pg_hba.conf` on the PostgreSQL server). + +Verify: + +```bash +psql -h YOUR_PG_HOST -U silo -d silo -c 'SELECT 1' +``` + +#### MinIO + +Install MinIO and create a bucket and service account: + +- [MinIO quickstart](https://min.io/docs/minio/linux/index.html) + +```bash +# Using the MinIO client (mc): +mc alias set local http://YOUR_MINIO_HOST:9000 minioadmin minioadmin +mc mb local/silo-files +mc admin user add local silouser YOUR_MINIO_SECRET +mc admin policy attach local readwrite --user silouser +``` + +Verify: + +```bash +curl -I http://YOUR_MINIO_HOST:9000/minio/health/live +``` + +#### LDAP / FreeIPA (Optional) + +For LDAP authentication, you need an LDAP server with user and group entries. Options: + +- [FreeIPA](https://www.freeipa.org/page/Quick_Start_Guide) — full identity management (recommended for organizations already using it) +- [OpenLDAP](https://www.openldap.org/doc/admin26/) — lightweight LDAP server + +Silo needs: +- A base DN (e.g., `dc=example,dc=com`) +- Users under a known OU (e.g., `cn=users,cn=accounts,dc=example,dc=com`) +- Groups that map to Silo roles (`admin`, `editor`, `viewer`) +- The `memberOf` overlay enabled (so user entries have `memberOf` attributes) + +See [CONFIGURATION.md — LDAP](CONFIGURATION.md#ldap--freeipa) for the full LDAP configuration reference. + +### B.4 Prepare the Host + +Run the setup script on the target host: + +```bash +# Copy and run the script +scp scripts/setup-host.sh root@YOUR_HOST:/tmp/ +ssh root@YOUR_HOST 'bash /tmp/setup-host.sh' +``` + +Or directly on the host: + +```bash +sudo bash scripts/setup-host.sh +``` + +The script: +1. Installs dependencies (git, Go 1.24) +2. Creates the `silo` system user +3. Creates directories (`/opt/silo`, `/etc/silo`) +4. Clones the repository +5. Creates the environment file template + +To override the default service hostnames: + +```bash +SILO_DB_HOST=db.example.com SILO_MINIO_HOST=s3.example.com sudo -E bash scripts/setup-host.sh +``` + +### B.5 Configure Credentials + +Edit the environment file with your service credentials: + +```bash +sudo nano /etc/silo/silod.env +``` + +```bash +# Database +SILO_DB_PASSWORD=your-database-password + +# MinIO +SILO_MINIO_ACCESS_KEY=silouser +SILO_MINIO_SECRET_KEY=your-minio-secret + +# Authentication +SILO_SESSION_SECRET=generate-a-long-random-string +SILO_ADMIN_USERNAME=admin +SILO_ADMIN_PASSWORD=your-admin-password +``` + +Generate a session secret: + +```bash +openssl rand -hex 32 +``` + +Review the server configuration: + +```bash +sudo nano /etc/silo/config.yaml +``` + +Update `database.host`, `storage.endpoint`, `server.base_url`, and authentication settings for your environment. See [CONFIGURATION.md](CONFIGURATION.md) for all options. + +### B.6 Deploy + +Run the deploy script: + +```bash +sudo /opt/silo/src/scripts/deploy.sh +``` + +The script: +1. Pulls latest code from git +2. Builds the `silod` binary and React frontend +3. Installs files to `/opt/silo` and `/etc/silo` +4. Runs database migrations +5. Installs and starts the systemd service + +Deploy options: + +```bash +# Skip git pull (use current checkout) +sudo /opt/silo/src/scripts/deploy.sh --no-pull + +# Skip build (use existing binary) +sudo /opt/silo/src/scripts/deploy.sh --no-build + +# Just restart the service +sudo /opt/silo/src/scripts/deploy.sh --restart-only + +# Check service status +sudo /opt/silo/src/scripts/deploy.sh --status +``` + +To override the target host or database host: + +```bash +SILO_DEPLOY_TARGET=silo.example.com SILO_DB_HOST=db.example.com sudo -E scripts/deploy.sh +``` + +### B.7 Set Up Nginx and TLS + +#### With FreeIPA (automated) + +If your organization uses FreeIPA, the included script handles nginx setup, IPA enrollment, and certificate issuance: + +```bash +sudo /opt/silo/src/scripts/setup-ipa-nginx.sh +``` + +Override the hostname if needed: + +```bash +SILO_HOSTNAME=silo.example.com sudo -E /opt/silo/src/scripts/setup-ipa-nginx.sh +``` + +The script installs nginx, enrolls the host in FreeIPA, requests a TLS certificate from the IPA CA (auto-renewed by certmonger), and configures nginx as an HTTPS reverse proxy. + +#### Manual nginx setup + +Install nginx and create a config: + +```bash +sudo apt install nginx # or: sudo dnf install nginx +``` + +Use the template at `deployments/nginx/nginx.conf` as a starting point. Copy it to `/etc/nginx/sites-available/silo`, update the `server_name` and certificate paths, then enable it: + +```bash +sudo ln -sf /etc/nginx/sites-available/silo /etc/nginx/sites-enabled/silo +sudo nginx -t +sudo systemctl reload nginx +``` + +After enabling HTTPS, update `server.base_url` in `/etc/silo/config.yaml` to use `https://` and restart Silo: + +```bash +sudo systemctl restart silod +``` + +### B.8 Verify the Installation + +```bash +# Service status +sudo systemctl status silod + +# Health check +curl http://localhost:8080/health + +# Readiness check +curl http://localhost:8080/ready + +# Follow logs +sudo journalctl -u silod -f +``` + +Open your configured base URL in a browser and log in. + +### B.9 Upgrading + +```bash +# Pull latest code and redeploy +sudo /opt/silo/src/scripts/deploy.sh + +# Or deploy a specific version +cd /opt/silo/src +git fetch --all --tags +git checkout v1.2.3 +sudo /opt/silo/src/scripts/deploy.sh --no-pull +``` + +New database migrations are applied automatically during deployment. + +--- + +## Post-Install Configuration + +After a successful installation: + +- **Authentication**: Configure LDAP, OIDC, or local auth backends. See [CONFIGURATION.md — Authentication](CONFIGURATION.md#authentication). +- **Schemas**: Part numbering schemas are loaded from YAML files. See the `schemas/` directory and [CONFIGURATION.md — Schemas](CONFIGURATION.md#schemas). +- **Read-only mode**: Toggle write protection at runtime with `kill -USR1 $(pidof silod)` or by setting `server.read_only: true` in the config. +- **Ongoing maintenance**: See [DEPLOYMENT.md](DEPLOYMENT.md) for service management, log viewing, troubleshooting, and the security checklist. + +--- + +## Further Reading + +| Document | Description | +|----------|-------------| +| [CONFIGURATION.md](CONFIGURATION.md) | Complete `config.yaml` reference | +| [DEPLOYMENT.md](DEPLOYMENT.md) | Operations guide: maintenance, troubleshooting, security | +| [AUTH.md](AUTH.md) | Authentication system design | +| [AUTH_USER_GUIDE.md](AUTH_USER_GUIDE.md) | User guide for login, tokens, and roles | +| [SPECIFICATION.md](SPECIFICATION.md) | Full design specification and API reference | +| [STATUS.md](STATUS.md) | Implementation status | +| [GAP_ANALYSIS.md](GAP_ANALYSIS.md) | Gap analysis and revision control roadmap | +| [COMPONENT_AUDIT.md](COMPONENT_AUDIT.md) | Component audit tool design | diff --git a/scripts/deploy.sh b/scripts/deploy.sh index fca11a5..c6998a0 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -1,18 +1,23 @@ #!/bin/bash -# Deploy Silo to silo.example.internal +# Deploy Silo to a target host # # Usage: ./scripts/deploy.sh [host] -# host defaults to silo.example.internal +# host defaults to SILO_DEPLOY_TARGET env var, or silo.example.internal # # Prerequisites: # - SSH access to the target host # - /etc/silo/silod.env must exist on target with credentials filled in -# - PostgreSQL reachable from target at psql.example.internal -# - MinIO reachable from target at minio.example.internal +# - PostgreSQL reachable from target (set SILO_DB_HOST to override) +# - MinIO reachable from target (set SILO_MINIO_HOST to override) +# +# Environment variables: +# SILO_DEPLOY_TARGET - target host (default: silo.example.internal) +# SILO_DB_HOST - PostgreSQL host (default: psql.example.internal) set -euo pipefail -TARGET="${1:-silo.example.internal}" +TARGET="${1:-${SILO_DEPLOY_TARGET:-silo.example.internal}}" +DB_HOST="${SILO_DB_HOST:-psql.example.internal}" DEPLOY_DIR="/opt/silo" CONFIG_DIR="/etc/silo" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -104,7 +109,7 @@ echo " Files installed to $DEPLOY_DIR" REMOTE echo "[6/6] Running migrations and starting service..." -ssh "$TARGET" bash -s <<'REMOTE' +ssh "$TARGET" DB_HOST="$DB_HOST" bash -s <<'REMOTE' set -euo pipefail DEPLOY_DIR="/opt/silo" @@ -123,14 +128,14 @@ if command -v psql &>/dev/null && [ -n "${SILO_DB_PASSWORD:-}" ]; then for f in "$DEPLOY_DIR/migrations/"*.sql; do echo " $(basename "$f")" PGPASSWORD="$SILO_DB_PASSWORD" psql \ - -h psql.example.internal -p 5432 \ + -h "$DB_HOST" -p 5432 \ -U silo -d silo \ -f "$f" -q 2>&1 | grep -v "already exists" || true done echo " Migrations complete." else echo " WARNING: psql not available or SILO_DB_PASSWORD not set, skipping migrations." - echo " Run migrations manually: PGPASSWORD=... psql -h psql.example.internal -U silo -d silo -f /opt/silo/migrations/NNN_name.sql" + echo " Run migrations manually: PGPASSWORD=... psql -h $DB_HOST -U silo -d silo -f /opt/silo/migrations/NNN_name.sql" fi # Start service diff --git a/scripts/setup-docker.sh b/scripts/setup-docker.sh new file mode 100755 index 0000000..cd0d122 --- /dev/null +++ b/scripts/setup-docker.sh @@ -0,0 +1,344 @@ +#!/usr/bin/env bash +# +# Silo Docker Setup Script +# Generates .env and config.docker.yaml for the all-in-one Docker Compose stack. +# +# Usage: +# ./scripts/setup-docker.sh # interactive +# ./scripts/setup-docker.sh --non-interactive # use defaults / env vars +# ./scripts/setup-docker.sh --domain silo.example.com +# ./scripts/setup-docker.sh --with-nginx +# +# Output: +# deployments/.env - Docker Compose environment variables +# deployments/config.docker.yaml - Silo server configuration + +set -euo pipefail + +# Colors (disabled if not a terminal) +if [[ -t 1 ]]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + BLUE='\033[0;34m' + BOLD='\033[1m' + NC='\033[0m' +else + RED='' GREEN='' YELLOW='' BLUE='' BOLD='' NC='' +fi + +log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } +log_success() { echo -e "${GREEN}[OK]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +# --------------------------------------------------------------------------- +# Defaults +# --------------------------------------------------------------------------- +DOMAIN="localhost" +NON_INTERACTIVE=false +WITH_NGINX=false +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="${SCRIPT_DIR}/.." +OUTPUT_DIR="${PROJECT_DIR}/deployments" + +# --------------------------------------------------------------------------- +# Parse arguments +# --------------------------------------------------------------------------- +while [[ $# -gt 0 ]]; do + case "$1" in + --non-interactive) NON_INTERACTIVE=true; shift ;; + --domain) DOMAIN="$2"; shift 2 ;; + --with-nginx) WITH_NGINX=true; shift ;; + --output-dir) OUTPUT_DIR="$2"; shift 2 ;; + -h|--help) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --non-interactive Use defaults and env vars, no prompts" + echo " --domain DOMAIN Server hostname (default: localhost)" + echo " --with-nginx Print instructions for the nginx profile" + echo " --output-dir DIR Output directory (default: ./deployments)" + echo " -h, --help Show this help" + exit 0 + ;; + *) log_error "Unknown option: $1"; exit 1 ;; + esac +done + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +generate_secret() { + local len="${1:-32}" + openssl rand -hex "$len" 2>/dev/null \ + || head -c "$len" /dev/urandom | od -An -tx1 | tr -d ' \n' +} + +prompt() { + local var_name="$1" prompt_text="$2" default="$3" + if [[ "$NON_INTERACTIVE" == "true" ]]; then + eval "$var_name=\"$default\"" + return + fi + local input + read -r -p "$(echo -e "${BOLD}${prompt_text}${NC} [${default}]: ")" input + eval "$var_name=\"${input:-$default}\"" +} + +prompt_secret() { + local var_name="$1" prompt_text="$2" default="$3" + if [[ "$NON_INTERACTIVE" == "true" ]]; then + eval "$var_name=\"$default\"" + return + fi + local input + read -r -p "$(echo -e "${BOLD}${prompt_text}${NC} [auto-generated]: ")" input + eval "$var_name=\"${input:-$default}\"" +} + +# --------------------------------------------------------------------------- +# Banner +# --------------------------------------------------------------------------- +echo "" +echo -e "${BOLD}Silo Docker Setup${NC}" +echo "Generates configuration for the all-in-one Docker Compose stack." +echo "" + +# Check for existing files +if [[ -f "${OUTPUT_DIR}/.env" ]]; then + log_warn "deployments/.env already exists." + if [[ "$NON_INTERACTIVE" == "false" ]]; then + read -r -p "Overwrite? [y/N]: " overwrite + if [[ "${overwrite,,}" != "y" ]]; then + log_info "Aborted." + exit 0 + fi + fi +fi + +# --------------------------------------------------------------------------- +# Gather configuration +# --------------------------------------------------------------------------- +log_info "Gathering configuration..." +echo "" + +# Domain / base URL +prompt DOMAIN "Server domain" "$DOMAIN" + +if [[ "$WITH_NGINX" == "true" ]]; then + BASE_URL="http://${DOMAIN}" +elif [[ "$DOMAIN" == "localhost" ]]; then + BASE_URL="http://localhost:8080" +else + BASE_URL="http://${DOMAIN}:8080" +fi + +# PostgreSQL +PG_PASSWORD_DEFAULT="$(generate_secret 16)" +prompt_secret POSTGRES_PASSWORD "PostgreSQL password" "$PG_PASSWORD_DEFAULT" + +# MinIO +MINIO_AK_DEFAULT="$(generate_secret 10)" +MINIO_SK_DEFAULT="$(generate_secret 16)" +prompt_secret MINIO_ACCESS_KEY "MinIO access key" "$MINIO_AK_DEFAULT" +prompt_secret MINIO_SECRET_KEY "MinIO secret key" "$MINIO_SK_DEFAULT" + +# OpenLDAP +LDAP_ADMIN_PW_DEFAULT="$(generate_secret 16)" +prompt_secret LDAP_ADMIN_PASSWORD "LDAP admin password" "$LDAP_ADMIN_PW_DEFAULT" +prompt LDAP_USERS "LDAP initial username" "siloadmin" +LDAP_USER_PW_DEFAULT="$(generate_secret 12)" +prompt_secret LDAP_PASSWORDS "LDAP initial user password" "$LDAP_USER_PW_DEFAULT" + +# Session secret +SESSION_SECRET="$(generate_secret 32)" + +# Silo local admin (fallback when LDAP is unavailable) +prompt SILO_ADMIN_USERNAME "Silo local admin username" "admin" +ADMIN_PW_DEFAULT="$(generate_secret 12)" +prompt_secret SILO_ADMIN_PASSWORD "Silo local admin password" "$ADMIN_PW_DEFAULT" + +echo "" + +# --------------------------------------------------------------------------- +# Write .env +# --------------------------------------------------------------------------- +log_info "Writing ${OUTPUT_DIR}/.env ..." + +cat > "${OUTPUT_DIR}/.env" << EOF +# Generated by setup-docker.sh on $(date +%Y-%m-%d) +# Used by: docker compose -f deployments/docker-compose.allinone.yaml + +# PostgreSQL +POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + +# MinIO +MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY} +MINIO_SECRET_KEY=${MINIO_SECRET_KEY} + +# OpenLDAP +LDAP_ADMIN_PASSWORD=${LDAP_ADMIN_PASSWORD} +LDAP_USERS=${LDAP_USERS} +LDAP_PASSWORDS=${LDAP_PASSWORDS} + +# Silo +SILO_SESSION_SECRET=${SESSION_SECRET} +SILO_ADMIN_USERNAME=${SILO_ADMIN_USERNAME} +SILO_ADMIN_PASSWORD=${SILO_ADMIN_PASSWORD} +SILO_BASE_URL=${BASE_URL} + +# Uncomment if using OIDC (Keycloak) +# SILO_OIDC_CLIENT_SECRET= +EOF + +chmod 600 "${OUTPUT_DIR}/.env" +log_success "deployments/.env written" + +# --------------------------------------------------------------------------- +# Write config.docker.yaml +# --------------------------------------------------------------------------- +log_info "Writing ${OUTPUT_DIR}/config.docker.yaml ..." + +# Note: Values wrapped in ${VAR} (inside the single-quoted YAMLEOF blocks) +# are NOT expanded by bash — they are written literally into the YAML file +# and expanded at runtime by the Go config loader via os.ExpandEnv(). +# The base_url and cors origin use the bash variable directly since +# SILO_SERVER_BASE_URL is not a supported direct override in the Go loader. +{ +cat << 'YAMLEOF' +# Silo Configuration — Docker Compose (all-in-one) +# Generated by scripts/setup-docker.sh +# +# Values using ${VAR} syntax are expanded from environment variables at +# startup. Direct env var overrides (SILO_DB_PASSWORD, etc.) take precedence +# over YAML values — see docs/CONFIGURATION.md for the full reference. + +server: + host: "0.0.0.0" + port: 8080 +YAMLEOF + +cat << EOF + base_url: "${BASE_URL}" +EOF + +cat << 'YAMLEOF' + +database: + host: "postgres" + port: 5432 + name: "silo" + user: "silo" + password: "${SILO_DB_PASSWORD}" + sslmode: "disable" + max_connections: 10 + +storage: + endpoint: "minio:9000" + access_key: "${SILO_MINIO_ACCESS_KEY}" + secret_key: "${SILO_MINIO_SECRET_KEY}" + bucket: "silo-files" + use_ssl: false + region: "us-east-1" + +schemas: + directory: "/etc/silo/schemas" + default: "kindred-rd" + +freecad: + uri_scheme: "silo" + +auth: + enabled: true + session_secret: "${SILO_SESSION_SECRET}" + + # Local accounts (fallback when LDAP is unavailable) + local: + enabled: true + default_admin_username: "${SILO_ADMIN_USERNAME}" + default_admin_password: "${SILO_ADMIN_PASSWORD}" + + # OpenLDAP (provided by the Docker Compose stack) + ldap: + enabled: true + url: "ldap://openldap:1389" + base_dn: "dc=silo,dc=local" + user_search_dn: "ou=users,dc=silo,dc=local" + user_attr: "cn" + email_attr: "mail" + display_attr: "cn" + group_attr: "memberOf" + role_mapping: + admin: + - "cn=silo-admins,ou=groups,dc=silo,dc=local" + editor: + - "cn=silo-users,ou=groups,dc=silo,dc=local" + viewer: + - "cn=silo-viewers,ou=groups,dc=silo,dc=local" + tls_skip_verify: false + + oidc: + enabled: false + + cors: + allowed_origins: +YAMLEOF + +cat << EOF + - "${BASE_URL}" +EOF +} > "${OUTPUT_DIR}/config.docker.yaml" + +log_success "deployments/config.docker.yaml written" + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +echo "" +echo -e "${BOLD}============================================${NC}" +echo -e "${BOLD}Setup complete!${NC}" +echo -e "${BOLD}============================================${NC}" +echo "" +echo "Generated files:" +echo " deployments/.env - secrets and credentials" +echo " deployments/config.docker.yaml - server configuration" +echo "" +echo -e "${BOLD}Credentials:${NC}" +echo " PostgreSQL: silo / ${POSTGRES_PASSWORD}" +echo " MinIO: ${MINIO_ACCESS_KEY} / ${MINIO_SECRET_KEY}" +echo " MinIO Console: http://localhost:9001" +echo " LDAP Admin: cn=admin,dc=silo,dc=local / ${LDAP_ADMIN_PASSWORD}" +echo " LDAP User: ${LDAP_USERS} / ${LDAP_PASSWORDS}" +echo " Silo Admin: ${SILO_ADMIN_USERNAME} / ${SILO_ADMIN_PASSWORD} (local fallback)" +echo " Base URL: ${BASE_URL}" +echo "" +echo -e "${BOLD}Next steps:${NC}" +echo "" +echo " # Start the stack" +if [[ "$WITH_NGINX" == "true" ]]; then + echo " docker compose -f deployments/docker-compose.allinone.yaml --profile nginx up -d" +else + echo " docker compose -f deployments/docker-compose.allinone.yaml up -d" +fi +echo "" +echo " # Check status" +echo " docker compose -f deployments/docker-compose.allinone.yaml ps" +echo "" +echo " # View logs" +echo " docker compose -f deployments/docker-compose.allinone.yaml logs -f silo" +echo "" +echo " # Open in browser" +echo " ${BASE_URL}" +echo "" +echo " # Log in with LDAP: ${LDAP_USERS} / " +echo " # Or local admin: ${SILO_ADMIN_USERNAME} / " +echo "" +if [[ "$WITH_NGINX" != "true" ]]; then + echo " To add nginx later:" + echo " docker compose -f deployments/docker-compose.allinone.yaml --profile nginx up -d" + echo "" +fi +echo "Save these credentials somewhere safe. The passwords in deployments/.env" +echo "are the source of truth for the running stack." +echo "" diff --git a/scripts/setup-host.sh b/scripts/setup-host.sh index 65d8b6b..6a22baa 100755 --- a/scripts/setup-host.sh +++ b/scripts/setup-host.sh @@ -28,7 +28,9 @@ REPO_URL="${SILO_REPO_URL:-https://git.kindred-systems.com/kindred/silo.git}" REPO_BRANCH="${SILO_BRANCH:-main}" INSTALL_DIR="/opt/silo" CONFIG_DIR="/etc/silo" -GO_VERSION="1.23.0" +GO_VERSION="1.24.0" +DB_HOST="${SILO_DB_HOST:-psql.example.internal}" +MINIO_HOST="${SILO_MINIO_HOST:-minio.example.internal}" log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } log_success() { echo -e "${GREEN}[OK]${NC} $*"; } @@ -155,21 +157,28 @@ log_success "Directories created" ENV_FILE="${CONFIG_DIR}/silod.env" if [[ ! -f "${ENV_FILE}" ]]; then log_info "Creating environment file..." - cat > "${ENV_FILE}" << 'EOF' + cat > "${ENV_FILE}" << EOF # Silo daemon environment variables # Fill in the values below -# Database credentials (psql.example.internal) +# Database credentials (${DB_HOST}) # Database: silo, User: silo SILO_DB_PASSWORD= -# MinIO credentials (minio.example.internal) +# MinIO credentials (${MINIO_HOST}) # User: silouser SILO_MINIO_ACCESS_KEY=silouser SILO_MINIO_SECRET_KEY= +# Authentication +# Session secret (required when auth is enabled) +SILO_SESSION_SECRET= +# Default admin account (created on first startup if both are set) +SILO_ADMIN_USERNAME=admin +SILO_ADMIN_PASSWORD= + # Optional overrides -# SILO_SERVER_BASE_URL=http://silo.example.internal:8080 +# SILO_SERVER_BASE_URL=http://\$(hostname -f):8080 EOF chmod 600 "${ENV_FILE}" chown root:silo "${ENV_FILE}" @@ -214,10 +223,10 @@ echo "1. Edit ${ENV_FILE} and fill in credentials:" echo " sudo nano ${ENV_FILE}" echo "" echo "2. Verify database connectivity:" -echo " psql -h psql.example.internal -U silo -d silo -c 'SELECT 1'" +echo " psql -h ${DB_HOST} -U silo -d silo -c 'SELECT 1'" echo "" echo "3. Verify MinIO connectivity:" -echo " curl -I http://minio.example.internal:9000/minio/health/live" +echo " curl -I http://${MINIO_HOST}:9000/minio/health/live" echo "" echo "4. Run the deployment:" echo " sudo ${INSTALL_DIR}/src/scripts/deploy.sh" diff --git a/scripts/setup-ipa-nginx.sh b/scripts/setup-ipa-nginx.sh index 0a37d4c..2445249 100755 --- a/scripts/setup-ipa-nginx.sh +++ b/scripts/setup-ipa-nginx.sh @@ -8,7 +8,7 @@ # # Prerequisites: # - FreeIPA server at ipa.example.internal -# - DNS configured for silo.example.internal +# - DNS configured for the silo host (set SILO_HOSTNAME to override default) # - Admin credentials for IPA enrollment set -euo pipefail @@ -24,9 +24,9 @@ NC='\033[0m' IPA_SERVER="${IPA_SERVER:-ipa.example.internal}" IPA_DOMAIN="${IPA_DOMAIN:-example.internal}" IPA_REALM="${IPA_REALM:-KINDRED.INTERNAL}" -HOSTNAME="silo.example.internal" +SILO_HOSTNAME="${SILO_HOSTNAME:-silo.example.internal}" CERT_DIR="/etc/ssl/silo" -SILO_PORT=8080 +SILO_PORT="${SILO_PORT:-8080}" log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } log_success() { echo -e "${GREEN}[OK]${NC} $*"; } @@ -77,8 +77,8 @@ log_success "Packages installed" # # Step 2: Set hostname # -log_info "Setting hostname to ${HOSTNAME}..." -hostnamectl set-hostname "${HOSTNAME}" +log_info "Setting hostname to ${SILO_HOSTNAME}..." +hostnamectl set-hostname "${SILO_HOSTNAME}" log_success "Hostname set" # @@ -95,7 +95,7 @@ else --server="${IPA_SERVER}" \ --domain="${IPA_DOMAIN}" \ --realm="${IPA_REALM}" \ - --hostname="${HOSTNAME}" \ + --hostname="${SILO_HOSTNAME}" \ --mkhomedir \ --enable-dns-updates \ --unattended \ @@ -105,7 +105,7 @@ else --server="${IPA_SERVER}" \ --domain="${IPA_DOMAIN}" \ --realm="${IPA_REALM}" \ - --hostname="${HOSTNAME}" \ + --hostname="${SILO_HOSTNAME}" \ --mkhomedir \ --enable-dns-updates } @@ -135,9 +135,9 @@ else ipa-getcert request \ -f "${CERT_DIR}/silo.crt" \ -k "${CERT_DIR}/silo.key" \ - -K "HTTP/${HOSTNAME}" \ - -D "${HOSTNAME}" \ - -N "CN=${HOSTNAME}" \ + -K "HTTP/${SILO_HOSTNAME}" \ + -D "${SILO_HOSTNAME}" \ + -N "CN=${SILO_HOSTNAME}" \ -C "systemctl reload nginx" log_info "Waiting for certificate to be issued..." @@ -186,14 +186,14 @@ if [[ -f /etc/nginx/sites-enabled/default ]]; then fi # Create silo nginx config -cat > /etc/nginx/sites-available/silo << 'NGINX_EOF' +cat > /etc/nginx/sites-available/silo << NGINX_EOF # Silo API Server - Nginx Reverse Proxy Configuration # Redirect HTTP to HTTPS server { listen 80; listen [::]:80; - server_name silo.example.internal; + server_name ${SILO_HOSTNAME}; # Allow certmonger/ACME challenges location /.well-known/ { @@ -201,7 +201,7 @@ server { } location / { - return 301 https://$server_name$request_uri; + return 301 https://\\$server_name\\$request_uri; } } @@ -209,11 +209,11 @@ server { server { listen 443 ssl http2; listen [::]:443 ssl http2; - server_name silo.example.internal; + server_name ${SILO_HOSTNAME}; # SSL certificates (managed by certmonger/IPA) - ssl_certificate /etc/ssl/silo/silo.crt; - ssl_certificate_key /etc/ssl/silo/silo.key; + ssl_certificate ${CERT_DIR}/silo.crt; + ssl_certificate_key ${CERT_DIR}/silo.key; # SSL configuration ssl_protocols TLSv1.2 TLSv1.3; @@ -226,7 +226,7 @@ server { # OCSP stapling ssl_stapling on; ssl_stapling_verify on; - ssl_trusted_certificate /etc/ssl/silo/ca.crt; + ssl_trusted_certificate ${CERT_DIR}/ca.crt; # Security headers add_header X-Frame-Options "SAMEORIGIN" always; @@ -240,19 +240,19 @@ server { # Proxy settings location / { - proxy_pass http://127.0.0.1:8080; + proxy_pass http://127.0.0.1:${SILO_PORT}; proxy_http_version 1.1; # Headers - 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_set_header X-Forwarded-Host $host; - proxy_set_header X-Forwarded-Port $server_port; + 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_set_header X-Forwarded-Host \\$host; + proxy_set_header X-Forwarded-Port \\$server_port; # WebSocket support (for future use) - proxy_set_header Upgrade $http_upgrade; + proxy_set_header Upgrade \\$http_upgrade; proxy_set_header Connection "upgrade"; # Timeouts @@ -343,14 +343,14 @@ echo " getcert list" echo "" echo "2. Update silo config to use correct base URL:" echo " sudo nano /etc/silo/config.yaml" -echo " # Change base_url to: https://silo.example.internal" +echo " # Change base_url to: https://${SILO_HOSTNAME}" echo "" echo "3. Restart silo service:" echo " sudo systemctl restart silod" echo "" echo "4. Test the setup:" -echo " curl -k https://silo.example.internal/health" -echo " curl https://silo.example.internal/health # after trusting IPA CA" +echo " curl -k https://${SILO_HOSTNAME}/health" +echo " curl https://${SILO_HOSTNAME}/health # after trusting IPA CA" echo "" echo "5. Trust IPA CA on client machines:" echo " # The CA cert is at: ${CERT_DIR}/ca.crt"