From cfeb2d30b4bfc9fc6ec6c0fef4d1e1ff6066f88a Mon Sep 17 00:00:00 2001 From: Ravenink Date: Fri, 13 Feb 2026 20:25:52 +0100 Subject: [PATCH 01/16] feat(nix): add flake, NixOS modules, and componentized OCI mode --- README.md | 167 ++++++++++++++++--- blockchain-services | 2 +- flake.nix | 90 ++++++++++ nix/hosts/gateway.nix | 53 ++++++ nix/images/ops-worker-image.nix | 56 +++++++ nix/lab-gateway-docker.nix | 118 +++++++++++++ nix/nixos-components-module.nix | 283 ++++++++++++++++++++++++++++++++ nix/nixos-module.nix | 101 ++++++++++++ 8 files changed, 849 insertions(+), 21 deletions(-) create mode 100644 flake.nix create mode 100644 nix/hosts/gateway.nix create mode 100644 nix/images/ops-worker-image.nix create mode 100644 nix/lab-gateway-docker.nix create mode 100644 nix/nixos-components-module.nix create mode 100644 nix/nixos-module.nix diff --git a/README.md b/README.md index dd65cc7..778ba53 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,25 @@ DecentraLabs Gateway provides a complete blockchain-based authentication system ## ๐Ÿš€ Quick Deployment +### Choose an Installation Mode + +Use one of these modes depending on your target: + +1. **Setup Scripts (`setup.sh` / `setup.bat`)** + Best for first-time installs. It prepares env files, secrets, and can start the full stack. + +2. **Manual Docker Compose** + Best if you want full control over compose commands and deployment flow. + +3. **Nix Wrapper for Compose (`nix run .#lab-gateway-docker`)** + Same runtime stack as Docker Compose; Nix only provides a packaged CLI wrapper. + +4. **NixOS Compose-managed Host (`nixos-rebuild --flake ...#gateway`)** + Best for dedicated NixOS hosts where you want declarative system + service management. + +5. **NixOS Componentized Host (`nixos-rebuild --flake ...#gateway-components`)** + Runs each service as an OCI container managed by NixOS modules (no `docker-compose up`). + ### Using Setup Scripts (Recommended) The setup scripts will automatically: @@ -78,6 +97,103 @@ chmod +x setup.sh That's it! The script will guide you through the setup and start all services automatically. +### Nix / NixOS Deployment + +This repository also includes a `flake.nix` with: + +- `packages..lab-gateway-docker`: helper CLI for the current Docker Compose stack +- `packages..lab-gateway-ops-worker-image`: deterministic OCI image tarball built from Nix +- `nixosModules.default`: NixOS module to manage the stack through systemd +- `nixosModules.components`: componentized NixOS module (OCI containers, no compose) +- `nixosModules.gateway-host`: host defaults for a dedicated NixOS gateway machine +- `nixosConfigurations.gateway`: complete host config ready for `nixos-rebuild` +- `nixosConfigurations.gateway-components`: host config using the componentized module + +#### A) Nix wrapper for the existing Docker stack + +This mode still requires Docker Engine + Docker Compose on the host. +It runs the same `docker-compose.yml` stack; it does not replace Compose with a different runtime. + +```bash +nix run .#lab-gateway-docker -- --project-dir "$PWD" --env-file "$PWD/.env" up -d --build +``` + +#### B) NixOS host configuration (compose-managed) + +This mode is only for NixOS machines. + +Use the module directly (example): + +```nix +{ + inputs.lab-gateway.url = "path:/srv/lab-gateway"; + + outputs = { nixpkgs, lab-gateway, ... }: { + nixosConfigurations.gateway = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + lab-gateway.nixosModules.default + { + services.lab-gateway = { + enable = true; + projectDir = "/srv/lab-gateway"; + envFile = "/srv/lab-gateway/.env"; + # profiles = [ "cloudflare" ]; + }; + } + ]; + }; + }; +} +``` + +Then apply it: + +```bash +sudo nixos-rebuild switch --flake /srv/lab-gateway#gateway +``` + +Complete host flow (real machine): + +```bash +# 1) Put this repo on the target NixOS host +sudo mkdir -p /srv +sudo git clone https://github.com/DecentraLabsCom/lite-lab-gateway.git /srv/lab-gateway +cd /srv/lab-gateway + +# 2) Prepare env files +sudo cp .env.example .env +sudo cp blockchain-services/.env.example blockchain-services/.env + +# 3) Edit values (passwords, domain, tokens, RPC, contract address) +sudo nano .env +sudo nano blockchain-services/.env + +# 4) Apply the full NixOS configuration shipped by this flake +sudo nixos-rebuild switch --flake /srv/lab-gateway#gateway + +# 5) Validate the service +systemctl status lab-gateway.service +``` + +`blockchain-services/.env` must still exist under `projectDir`, because `docker-compose.yml` references it directly. + +`nixosConfigurations.gateway` imports your existing `/etc/nixos/configuration.nix` and layers the gateway module on top, so host-specific settings (bootloader, users, disks, hardware) are preserved. +Host-level values (hostname, timezone, firewall, profiles, SSH hardening) are installation-specific and should be overridden per environment. + +#### C) NixOS host configuration (componentized OCI containers) + +This mode avoids `docker compose up` and manages each component as a NixOS-defined OCI container. + +```bash +sudo nixos-rebuild switch --flake /srv/lab-gateway#gateway-components +``` + +Notes: +- OpenResty, Guacamole and blockchain-services images are built from local Dockerfiles by a systemd build step. +- `ops-worker` image is produced deterministically by Nix (`packages..lab-gateway-ops-worker-image`). +- Set `services.lab-gateway-components.opsMysqlDsn` if you want ops-worker reservation automation backed by MySQL. + ### Manual Deployment If you prefer manual configuration: @@ -373,28 +489,39 @@ Internet โ”€โ”€> [NIC with VLAN tagging] Lab Gateway โ”€โ”€> VLAN 10 / VLAN 20 ``` lab-gateway/ -โ”œโ”€โ”€ ๐Ÿ“ openresty/ # Reverse proxy configuration -โ”‚ โ”œโ”€โ”€ nginx.conf # Main Nginx configuration -โ”‚ โ”œโ”€โ”€ lab_access.conf # Lab access routes -โ”‚ โ””โ”€โ”€ lua/ # Lua modules for auth and session management -โ”œโ”€โ”€ ๐Ÿ“ guacamole/ # RDP/VNC/SSH client -โ”‚ โ””โ”€โ”€ extensions/ # Guacamole extensions -โ”œโ”€โ”€ ๐Ÿ“ mysql/ # DB scripts and schemas -โ”‚ โ”œโ”€โ”€ 001-create-schema.sql -โ”‚ โ”œโ”€โ”€ 002-labstation-ops.sql -โ”œโ”€โ”€ ๐Ÿ“ web/ # Web frontend (optional) -โ”œโ”€โ”€ ๐Ÿ“ blockchain-services/ # Blockchain auth & wallet service (Git submodule) -โ”œโ”€โ”€ ๐Ÿ“ blockchain-data/ # Encrypted wallet persistence (not in git) -โ”œโ”€โ”€ ๐Ÿ“ certs/ # SSL certificates (not in git) -โ”œโ”€โ”€ ๐Ÿ“ tests/ # Gateway tests (unit + smoke) -โ”‚ โ”œโ”€โ”€ smoke/ # End-to-end smoke tests -โ”‚ โ””โ”€โ”€ unit/ # Lua unit tests -โ”œโ”€โ”€ ๐Ÿ“„ docker-compose.yml # Service orchestration -โ”œโ”€โ”€ ๐Ÿ“„ .env.example # Configuration template -โ”œโ”€โ”€ ๐Ÿ“„ setup.sh/.bat # Installation scripts -โ””โ”€โ”€ ๐Ÿ“„ update-blockchain-services.sh/.bat # Submodule update scripts +โ”œโ”€โ”€ ๐Ÿ“„ flake.nix # Nix flake outputs (packages + NixOS config/module) +โ”œโ”€โ”€ ๐Ÿ“„ docker-compose.yml # Main service orchestration +โ”œโ”€โ”€ ๐Ÿ“„ .env.example # Gateway configuration template +โ”œโ”€โ”€ ๐Ÿ“„ setup.sh / setup.bat # Guided setup scripts +โ”œโ”€โ”€ ๐Ÿ“„ selfsigned-refresh.sh # Self-signed cert helper +โ”œโ”€โ”€ ๐Ÿ“ nix/ +โ”‚ โ”œโ”€โ”€ lab-gateway-docker.nix # Compose wrapper package +โ”‚ โ”œโ”€โ”€ nixos-module.nix # services.lab-gateway (compose-managed) module +โ”‚ โ”œโ”€โ”€ nixos-components-module.nix # services.lab-gateway-components module +โ”‚ โ”œโ”€โ”€ images/ops-worker-image.nix # Nix-built OCI image +โ”‚ โ””โ”€โ”€ hosts/gateway.nix # Host defaults for nixosConfigurations.gateway +โ”œโ”€โ”€ ๐Ÿ“ blockchain-services/ # Blockchain auth/wallet service (submodule) +โ”œโ”€โ”€ ๐Ÿ“ openresty/ # Reverse proxy (Nginx + Lua) +โ”‚ โ”œโ”€โ”€ nginx.conf +โ”‚ โ”œโ”€โ”€ lab_access.conf +โ”‚ โ”œโ”€โ”€ lua/ +โ”‚ โ””โ”€โ”€ tests/ # Lua unit test runner/specs +โ”œโ”€โ”€ ๐Ÿ“ guacamole/ # Guacamole image customizations +โ”œโ”€โ”€ ๐Ÿ“ mysql/ # DB bootstrap and schema scripts +โ”œโ”€โ”€ ๐Ÿ“ ops-worker/ # Lab station operations API worker +โ”œโ”€โ”€ ๐Ÿ“ web/ # Static frontend assets/pages +โ”œโ”€โ”€ ๐Ÿ“ certbot/ # ACME webroot/support files +โ”œโ”€โ”€ ๐Ÿ“ tests/ +โ”‚ โ”œโ”€โ”€ smoke/ # End-to-end smoke tests +โ”‚ โ””โ”€โ”€ integration/ # Integration tests with mocks +โ”œโ”€โ”€ ๐Ÿ“ certs/ # Runtime certificates/keys (not in git) +โ”œโ”€โ”€ ๐Ÿ“ blockchain-data/ # Runtime wallet/provider data (not in git) +โ””โ”€โ”€ ๐Ÿ“ configuring-lab-connections/ # Guacamole connection setup docs ``` +`certs/` and `blockchain-data/` are runtime directories and may not exist until first setup. +`blockchain-services/` is a Git submodule and must be initialized/updated before running the stack. + ## ๐Ÿงช Testing ### Gateway Tests diff --git a/blockchain-services b/blockchain-services index e208a25..9f3dcf2 160000 --- a/blockchain-services +++ b/blockchain-services @@ -1 +1 @@ -Subproject commit e208a2553dcf8ea7e22793037a11ce1a6fcda3be +Subproject commit 9f3dcf286ded937f7420cda5b97f6ad6ee1a79dd diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..840eccb --- /dev/null +++ b/flake.nix @@ -0,0 +1,90 @@ +{ + description = "DecentraLabs Gateway flake (Docker helpers, OCI images, and NixOS modules)"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = { self, nixpkgs, ... }: + let + systems = [ "x86_64-linux" "aarch64-linux" ]; + forAllSystems = nixpkgs.lib.genAttrs systems; + in + { + packages = forAllSystems (system: + let + pkgs = import nixpkgs { inherit system; }; + labGatewayDocker = pkgs.callPackage ./nix/lab-gateway-docker.nix { }; + labGatewayOpsWorkerImage = pkgs.callPackage ./nix/images/ops-worker-image.nix { }; + in + { + default = labGatewayDocker; + lab-gateway-docker = labGatewayDocker; + lab-gateway-ops-worker-image = labGatewayOpsWorkerImage; + }); + + apps = forAllSystems (system: { + default = { + type = "app"; + program = "${self.packages.${system}.lab-gateway-docker}/bin/lab-gateway"; + }; + lab-gateway-docker = { + type = "app"; + program = "${self.packages.${system}.lab-gateway-docker}/bin/lab-gateway"; + }; + }); + + formatter = forAllSystems (system: + (import nixpkgs { inherit system; }).nixfmt-rfc-style + ); + + nixosModules = { + default = import ./nix/nixos-module.nix; + lab-gateway = self.nixosModules.default; + components = import ./nix/nixos-components-module.nix; + gateway-host = import ./nix/hosts/gateway.nix; + }; + + nixosConfigurations = { + gateway = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + (if builtins.pathExists "/etc/nixos/configuration.nix" + then /etc/nixos/configuration.nix + else ({ ... }: { + # Fallback only for evaluation outside a NixOS host. + boot.isContainer = true; + fileSystems."/" = { + device = "none"; + fsType = "tmpfs"; + }; + })) + self.nixosModules.default + self.nixosModules.gateway-host + ]; + }; + + gateway-components = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + (if builtins.pathExists "/etc/nixos/configuration.nix" + then /etc/nixos/configuration.nix + else ({ ... }: { + boot.isContainer = true; + fileSystems."/" = { + device = "none"; + fsType = "tmpfs"; + }; + })) + self.nixosModules.default + self.nixosModules.components + self.nixosModules.gateway-host + ({ lib, ... }: { + services.lab-gateway.enable = lib.mkForce false; + services.lab-gateway-components.enable = true; + }) + ]; + }; + }; + }; +} diff --git a/nix/hosts/gateway.nix b/nix/hosts/gateway.nix new file mode 100644 index 0000000..64c2bc3 --- /dev/null +++ b/nix/hosts/gateway.nix @@ -0,0 +1,53 @@ +{ lib, pkgs, ... }: + +{ + networking.hostName = lib.mkDefault "lab-gateway"; + time.timeZone = lib.mkDefault "UTC"; + + services.openssh = { + enable = lib.mkDefault true; + settings = { + PasswordAuthentication = lib.mkDefault false; + KbdInteractiveAuthentication = lib.mkDefault false; + }; + }; + + networking.firewall = { + enable = lib.mkDefault true; + allowedTCPPorts = [ 22 80 443 ]; + }; + + virtualisation.docker = { + enable = lib.mkDefault true; + autoPrune = { + enable = lib.mkDefault true; + dates = lib.mkDefault "weekly"; + }; + }; + + systemd.tmpfiles.rules = [ + "d /srv/lab-gateway 0755 root root -" + "d /srv/lab-gateway/blockchain-data 0750 root root -" + "d /srv/lab-gateway/certs 0750 root root -" + "d /srv/lab-gateway/certbot 0755 root root -" + "d /srv/lab-gateway/certbot/www 0755 root root -" + ]; + + services.lab-gateway = { + enable = true; + projectDir = "/srv/lab-gateway"; + envFile = "/srv/lab-gateway/.env"; + buildOnStart = true; + removeOrphansOnStart = true; + removeVolumesOnStop = false; + }; + + environment.systemPackages = [ + pkgs.git + pkgs.docker-compose + (pkgs.callPackage ../lab-gateway-docker.nix { }) + ]; + + # Keep this aligned with the value already used by your host. + system.stateVersion = lib.mkDefault "24.11"; +} diff --git a/nix/images/ops-worker-image.nix b/nix/images/ops-worker-image.nix new file mode 100644 index 0000000..32def0f --- /dev/null +++ b/nix/images/ops-worker-image.nix @@ -0,0 +1,56 @@ +{ dockerTools +, python3 +, bash +, coreutils +, iputils +, cacert +}: + +let + pythonEnv = python3.withPackages (ps: with ps; [ + flask + pywinrm + requests + requests-ntlm + wakeonlan + apscheduler + sqlalchemy + pymysql + cryptography + pytz + tzlocal + urllib3 + xmltodict + ntlm-auth + ]); +in +dockerTools.buildLayeredImage { + name = "lab-gateway-ops-worker"; + tag = "nix"; + + contents = [ + pythonEnv + bash + coreutils + iputils + cacert + ]; + + extraCommands = '' + mkdir -p app + cp ${../../ops-worker/worker.py} app/worker.py + ''; + + config = { + WorkingDir = "/app"; + Env = [ + "PYTHONDONTWRITEBYTECODE=1" + "PYTHONUNBUFFERED=1" + "OPS_BIND=0.0.0.0" + "OPS_PORT=8081" + "OPS_CONFIG=/app/hosts.json" + "SSL_CERT_FILE=${cacert}/etc/ssl/certs/ca-bundle.crt" + ]; + Cmd = [ "${pythonEnv}/bin/python" "/app/worker.py" ]; + }; +} diff --git a/nix/lab-gateway-docker.nix b/nix/lab-gateway-docker.nix new file mode 100644 index 0000000..d203bdd --- /dev/null +++ b/nix/lab-gateway-docker.nix @@ -0,0 +1,118 @@ +{ writeShellApplication, docker, docker-compose }: + +writeShellApplication { + name = "lab-gateway"; + runtimeInputs = [ docker docker-compose ]; + text = '' + set -euo pipefail + + project_dir="''${LAB_GATEWAY_PROJECT_DIR:-}" + project_name="''${LAB_GATEWAY_PROJECT_NAME:-lab-gateway}" + env_file="''${LAB_GATEWAY_ENV_FILE:-}" + profiles=() + + usage() { + cat <<'EOF' +Usage: + lab-gateway [--project-dir DIR] [--project-name NAME] [--env-file FILE] [--profile NAME]... [args...] + +Examples: + lab-gateway --project-dir . up -d --build + lab-gateway --project-dir /srv/lab-gateway --env-file /srv/lab-gateway/.env logs -f openresty +EOF + } + + while [[ $# -gt 0 ]]; do + case "$1" in + --project-dir) + shift + if [[ $# -eq 0 ]]; then + echo "Missing value for --project-dir" >&2 + exit 1 + fi + project_dir="$1" + ;; + --project-name) + shift + if [[ $# -eq 0 ]]; then + echo "Missing value for --project-name" >&2 + exit 1 + fi + project_name="$1" + ;; + --env-file) + shift + if [[ $# -eq 0 ]]; then + echo "Missing value for --env-file" >&2 + exit 1 + fi + env_file="$1" + ;; + --profile) + shift + if [[ $# -eq 0 ]]; then + echo "Missing value for --profile" >&2 + exit 1 + fi + profiles+=("$1") + ;; + -h|--help) + usage + exit 0 + ;; + --) + shift + break + ;; + -*) + echo "Unknown option: $1" >&2 + usage >&2 + exit 1 + ;; + *) + break + ;; + esac + shift + done + + if [[ $# -lt 1 ]]; then + usage >&2 + exit 1 + fi + + if [[ -z "$project_dir" ]]; then + project_dir="$(pwd)" + fi + + compose_file="$project_dir/docker-compose.yml" + if [[ ! -f "$compose_file" ]]; then + echo "docker-compose.yml not found: $compose_file" >&2 + exit 1 + fi + + subcommand="$1" + shift + + compose_cmd=() + if docker compose version >/dev/null 2>&1; then + compose_cmd=(docker compose) + elif command -v docker-compose >/dev/null 2>&1; then + compose_cmd=(docker-compose) + else + echo "Docker Compose not found (tried 'docker compose' and 'docker-compose')." >&2 + exit 1 + fi + + compose_args=(--project-name "$project_name") + if [[ -n "$env_file" ]]; then + compose_args+=(--env-file "$env_file") + fi + for profile in "''${profiles[@]}"; do + compose_args+=(--profile "$profile") + done + compose_args+=(-f "$compose_file") + + exec "''${compose_cmd[@]}" "''${compose_args[@]}" "$subcommand" "$@" + ''; +} diff --git a/nix/nixos-components-module.nix b/nix/nixos-components-module.nix new file mode 100644 index 0000000..e898fe4 --- /dev/null +++ b/nix/nixos-components-module.nix @@ -0,0 +1,283 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.lab-gateway-components; + inherit (lib) mkEnableOption mkIf mkOption optionalAttrs optionals types; + + gatewayEnvFiles = optionals (cfg.envFile != null) [ cfg.envFile ]; + blockchainEnvFiles = + gatewayEnvFiles ++ + optionals (cfg.blockchainEnvFile != null) [ cfg.blockchainEnvFile ]; + + netOpt = "--network=${cfg.networkName}"; +in +{ + options.services.lab-gateway-components = { + enable = mkEnableOption "DecentraLabs Gateway (componentized OCI containers)"; + + projectDir = mkOption { + type = types.str; + default = "/srv/lab-gateway"; + description = "Project directory where gateway files are located."; + }; + + envFile = mkOption { + type = types.nullOr types.str; + default = "/srv/lab-gateway/.env"; + description = "Main gateway .env file."; + }; + + blockchainEnvFile = mkOption { + type = types.nullOr types.str; + default = "/srv/lab-gateway/blockchain-services/.env"; + description = "Blockchain-specific .env file."; + }; + + networkName = mkOption { + type = types.str; + default = "guacnet"; + description = "Docker network used by gateway containers."; + }; + + openrestyImage = mkOption { + type = types.str; + default = "lab-gateway/openresty:local"; + description = "Docker image tag for OpenResty."; + }; + + guacamoleImage = mkOption { + type = types.str; + default = "lab-gateway/guacamole:local"; + description = "Docker image tag for Guacamole web."; + }; + + blockchainImage = mkOption { + type = types.str; + default = "lab-gateway/blockchain-services:local"; + description = "Docker image tag for blockchain-services."; + }; + + opsWorkerImageName = mkOption { + type = types.str; + default = "lab-gateway-ops-worker:nix"; + description = "Docker tag for the Nix-built ops-worker image."; + }; + + opsWorkerImageFile = mkOption { + type = types.package; + default = pkgs.callPackage ./images/ops-worker-image.nix { }; + description = "Nix-built OCI image tarball for ops-worker."; + }; + + buildLocalImages = mkOption { + type = types.bool; + default = true; + description = '' + Build OpenResty, Guacamole and blockchain-services images from local Dockerfiles + before starting component containers. + ''; + }; + + openrestyBindAddress = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Host bind address for OpenResty ports."; + }; + + openrestyHttpsPort = mkOption { + type = types.int; + default = 443; + description = "Host HTTPS port exposed by OpenResty."; + }; + + openrestyHttpPort = mkOption { + type = types.int; + default = 80; + description = "Host HTTP port exposed by OpenResty."; + }; + + opsConfigPath = mkOption { + type = types.str; + default = "/srv/lab-gateway/ops-worker/hosts.empty.json"; + description = "Path to ops-worker hosts.json source file."; + }; + + opsMysqlDsn = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Optional DSN used by ops-worker for reservation automation. + Example: mysql+pymysql://user:password@mysql:3306/guacamole_db + ''; + }; + }; + + config = mkIf cfg.enable { + virtualisation.docker.enable = lib.mkDefault true; + virtualisation.oci-containers.backend = "docker"; + + systemd.services.lab-gateway-create-network = { + description = "Create Docker network for DecentraLabs Gateway"; + wantedBy = [ "multi-user.target" ]; + wants = [ "docker.service" ]; + after = [ "docker.service" ]; + path = [ pkgs.docker ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if ! docker network inspect ${lib.escapeShellArg cfg.networkName} >/dev/null 2>&1; then + docker network create ${lib.escapeShellArg cfg.networkName} + fi + ''; + }; + + systemd.services.lab-gateway-build-images = mkIf cfg.buildLocalImages { + description = "Build gateway component images from local Dockerfiles"; + wantedBy = [ "multi-user.target" ]; + wants = [ "docker.service" "network-online.target" ]; + after = [ "docker.service" "network-online.target" ]; + path = [ pkgs.docker ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + docker build -t ${lib.escapeShellArg cfg.openrestyImage} ${lib.escapeShellArg "${cfg.projectDir}/openresty"} + docker build -t ${lib.escapeShellArg cfg.guacamoleImage} ${lib.escapeShellArg "${cfg.projectDir}/guacamole"} + docker build -t ${lib.escapeShellArg cfg.blockchainImage} ${lib.escapeShellArg "${cfg.projectDir}/blockchain-services"} + ''; + }; + + virtualisation.oci-containers.containers = { + mysql = { + image = "mysql:8.0.41"; + entrypoint = [ "/bin/bash" "/usr/local/bin/ensure-user-entrypoint.sh" ]; + cmd = [ "mysqld" ]; + environmentFiles = gatewayEnvFiles; + environment = { + BLOCKCHAIN_MYSQL_DATABASE = "blockchain_services"; + }; + volumes = [ + "mysql_data:/var/lib/mysql" + "${cfg.projectDir}/mysql/ensure-user-entrypoint.sh:/usr/local/bin/ensure-user-entrypoint.sh:ro" + "${cfg.projectDir}/mysql/000-ensure-user.sh:/docker-entrypoint-initdb.d/000-ensure-user.sh:ro" + "${cfg.projectDir}/mysql/001-create-schema.sql:/docker-entrypoint-initdb.d/001-create-schema.sql:ro" + "${cfg.projectDir}/mysql/002-labstation-ops.sql:/docker-entrypoint-initdb.d/002-labstation-ops.sql:ro" + ]; + extraOptions = [ netOpt ]; + }; + + guacd = { + image = "guacamole/guacd:1.5.5"; + extraOptions = [ netOpt ]; + }; + + guacamole = { + image = cfg.guacamoleImage; + dependsOn = [ "mysql" "guacd" ]; + environmentFiles = gatewayEnvFiles; + environment = { + GUACD_HOSTNAME = "guacd"; + MYSQL_HOSTNAME = "mysql"; + }; + extraOptions = [ netOpt ]; + }; + + "blockchain-services" = { + image = cfg.blockchainImage; + dependsOn = [ "mysql" ]; + environmentFiles = blockchainEnvFiles; + environment = { + SPRING_DATASOURCE_URL = "jdbc:mysql://mysql:3306/blockchain_services?serverTimezone=UTC&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true"; + PROVIDER_CONFIG_PATH = "/app/data/provider.properties"; + }; + volumes = [ + "${cfg.projectDir}/certs:/app/config/keys" + "${cfg.projectDir}/blockchain-data:/app/data" + ]; + extraOptions = [ netOpt ]; + }; + + "ops-worker" = { + image = cfg.opsWorkerImageName; + imageFile = cfg.opsWorkerImageFile; + dependsOn = [ "mysql" ]; + environmentFiles = gatewayEnvFiles; + environment = { + OPS_BIND = "0.0.0.0"; + OPS_PORT = "8081"; + OPS_CONFIG = "/app/hosts.json"; + OPS_POLL_ENABLED = "true"; + OPS_POLL_INTERVAL = "60"; + OPS_RESERVATION_AUTOMATION = "true"; + OPS_RESERVATION_SCAN_INTERVAL = "30"; + OPS_RESERVATION_START_LEAD = "120"; + OPS_RESERVATION_END_DELAY = "60"; + } // optionalAttrs (cfg.opsMysqlDsn != null) { + MYSQL_DSN = cfg.opsMysqlDsn; + }; + volumes = [ + "${cfg.opsConfigPath}:/app/hosts.json:ro" + ]; + extraOptions = [ netOpt "--read-only" "--tmpfs=/tmp:size=32m,mode=1777" ]; + }; + + openresty = { + image = cfg.openrestyImage; + dependsOn = [ "guacamole" "blockchain-services" "ops-worker" ]; + environmentFiles = gatewayEnvFiles; + volumes = [ + "${cfg.projectDir}/certs:/etc/ssl/private" + "${cfg.projectDir}/certbot/www:/var/www/certbot" + "${cfg.projectDir}/openresty/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro" + "${cfg.projectDir}/openresty/lab_access.conf:/etc/openresty/lab_access.conf:ro" + "${cfg.projectDir}/openresty/lua:/etc/openresty/lua:ro" + "${cfg.projectDir}/web:/var/www/html:ro" + ]; + ports = [ + "${cfg.openrestyBindAddress}:${toString cfg.openrestyHttpsPort}:443" + "${cfg.openrestyBindAddress}:${toString cfg.openrestyHttpPort}:80" + ]; + extraOptions = [ netOpt "--tmpfs=/tmp:size=64m,mode=1777" "--tmpfs=/var/run:size=16m,mode=755" ]; + }; + }; + + systemd.services."docker-mysql" = { + wants = [ "lab-gateway-create-network.service" ]; + after = [ "lab-gateway-create-network.service" ]; + }; + + systemd.services."docker-guacd" = { + wants = [ "lab-gateway-create-network.service" ]; + after = [ "lab-gateway-create-network.service" ]; + }; + + systemd.services."docker-guacamole" = { + wants = [ "lab-gateway-create-network.service" ] ++ + optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; + after = [ "lab-gateway-create-network.service" ] ++ + optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; + }; + + systemd.services."docker-blockchain-services" = { + wants = [ "lab-gateway-create-network.service" ] ++ + optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; + after = [ "lab-gateway-create-network.service" ] ++ + optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; + }; + + systemd.services."docker-ops-worker" = { + wants = [ "lab-gateway-create-network.service" ]; + after = [ "lab-gateway-create-network.service" ]; + }; + + systemd.services."docker-openresty" = { + wants = [ "lab-gateway-create-network.service" ] ++ + optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; + after = [ "lab-gateway-create-network.service" ] ++ + optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; + }; + }; +} diff --git a/nix/nixos-module.nix b/nix/nixos-module.nix new file mode 100644 index 0000000..7505531 --- /dev/null +++ b/nix/nixos-module.nix @@ -0,0 +1,101 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.lab-gateway; + inherit (lib) concatMapStrings mkEnableOption mkIf mkOption optionalString types; + envFileArg = + optionalString (cfg.envFile != null) " --env-file ${lib.escapeShellArg cfg.envFile}"; + profileArgs = + concatMapStrings (profile: " --profile ${lib.escapeShellArg profile}") cfg.profiles; + buildArg = optionalString cfg.buildOnStart " --build"; + removeOrphansArg = optionalString cfg.removeOrphansOnStart " --remove-orphans"; + removeVolumesArg = optionalString cfg.removeVolumesOnStop " --volumes"; + commonArgs = '' + --project-dir ${lib.escapeShellArg cfg.projectDir} + --project-name ${lib.escapeShellArg cfg.projectName}${envFileArg}${profileArgs} + ''; +in +{ + options.services.lab-gateway = { + enable = mkEnableOption "DecentraLabs Gateway (Docker Compose stack)"; + + package = mkOption { + type = types.package; + default = pkgs.callPackage ./lab-gateway-docker.nix { }; + description = "Helper package that wraps docker compose for this stack."; + }; + + projectDir = mkOption { + type = types.str; + default = "/srv/lab-gateway"; + description = "Path where docker-compose.yml and project files are located."; + }; + + projectName = mkOption { + type = types.str; + default = "lab-gateway"; + description = "Compose project name used for container and network naming."; + }; + + envFile = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Optional path to the main .env file to pass to docker compose. + If null, compose uses default environment resolution. + ''; + }; + + profiles = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Optional docker compose profiles to enable (for example: cloudflare)."; + }; + + buildOnStart = mkOption { + type = types.bool; + default = true; + description = "Whether to run compose with --build during service start."; + }; + + removeOrphansOnStart = mkOption { + type = types.bool; + default = true; + description = "Whether to pass --remove-orphans to compose up."; + }; + + removeVolumesOnStop = mkOption { + type = types.bool; + default = false; + description = "Whether to pass --volumes to compose down."; + }; + }; + + config = mkIf cfg.enable { + virtualisation.docker.enable = lib.mkDefault true; + + systemd.services.lab-gateway = { + description = "DecentraLabs Gateway"; + wantedBy = [ "multi-user.target" ]; + wants = [ "docker.service" "network-online.target" ]; + after = [ "docker.service" "network-online.target" ]; + + path = [ pkgs.docker cfg.package ]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + WorkingDirectory = cfg.projectDir; + TimeoutStartSec = "0"; + }; + + script = '' + ${cfg.package}/bin/lab-gateway ${commonArgs} up -d${buildArg}${removeOrphansArg} + ''; + + preStop = '' + ${cfg.package}/bin/lab-gateway ${commonArgs} down${removeVolumesArg} + ''; + }; + }; +} From b8c4534779861f9bfc341e6afa7947d0627ec840 Mon Sep 17 00:00:00 2001 From: Ravenink Date: Fri, 13 Feb 2026 20:32:20 +0100 Subject: [PATCH 02/16] docs(mou): add deliverables pack, pilot templates, and Nix/integration CI --- .github/workflows/nix-integration.yml | 50 +++++++++ README.md | 6 + SUMMARY.md | 13 +++ docs/MOU_DELIVERABLES.md | 35 ++++++ docs/edugain/EDUGAIN_INTEGRATION_EN.md | 52 +++++++++ docs/edugain/INTEGRACION_EDUGAIN_ES.md | 52 +++++++++ docs/install/GUIA_INSTALACION_ES.md | 110 +++++++++++++++++++ docs/install/INSTALLATION_GUIDE_EN.md | 110 +++++++++++++++++++ docs/pilots/PARTNER_PILOT_REPORT_TEMPLATE.md | 47 ++++++++ docs/pilots/PILOT_RUNBOOK.md | 39 +++++++ docs/pilots/UNED_PILOT_REPORT_TEMPLATE.md | 45 ++++++++ docs/tutorials/PROVIDER_TUTORIAL_EN.md | 52 +++++++++ docs/tutorials/TUTORIAL_PROVEEDOR_ES.md | 52 +++++++++ 13 files changed, 663 insertions(+) create mode 100644 .github/workflows/nix-integration.yml create mode 100644 docs/MOU_DELIVERABLES.md create mode 100644 docs/edugain/EDUGAIN_INTEGRATION_EN.md create mode 100644 docs/edugain/INTEGRACION_EDUGAIN_ES.md create mode 100644 docs/install/GUIA_INSTALACION_ES.md create mode 100644 docs/install/INSTALLATION_GUIDE_EN.md create mode 100644 docs/pilots/PARTNER_PILOT_REPORT_TEMPLATE.md create mode 100644 docs/pilots/PILOT_RUNBOOK.md create mode 100644 docs/pilots/UNED_PILOT_REPORT_TEMPLATE.md create mode 100644 docs/tutorials/PROVIDER_TUTORIAL_EN.md create mode 100644 docs/tutorials/TUTORIAL_PROVEEDOR_ES.md diff --git a/.github/workflows/nix-integration.yml b/.github/workflows/nix-integration.yml new file mode 100644 index 0000000..7844ae4 --- /dev/null +++ b/.github/workflows/nix-integration.yml @@ -0,0 +1,50 @@ +name: Nix And Integration + +on: + push: + branches: ["main", "NixOS", "full", "lite"] + pull_request: + branches: ["main", "full", "lite"] + +permissions: + contents: read + +jobs: + nix-flake-check: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v19 + + - name: Show flake outputs + run: nix flake show --all-systems + + - name: Build Nix helper package + run: nix build .#lab-gateway-docker + + - name: Build deterministic ops-worker image + run: nix build .#lab-gateway-ops-worker-image + + - name: Evaluate NixOS configurations + run: | + nix eval .#nixosConfigurations.gateway.config.system.build.toplevel.drvPath + nix eval .#nixosConfigurations.gateway-components.config.system.build.toplevel.drvPath + + integration-tests: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Run integration suite + run: | + chmod +x tests/integration/run-integration.sh + chmod +x tests/integration/certs/generate-certs.sh + tests/integration/run-integration.sh diff --git a/README.md b/README.md index 778ba53..76ed1da 100644 --- a/README.md +++ b/README.md @@ -514,6 +514,7 @@ lab-gateway/ โ”œโ”€โ”€ ๐Ÿ“ tests/ โ”‚ โ”œโ”€โ”€ smoke/ # End-to-end smoke tests โ”‚ โ””โ”€โ”€ integration/ # Integration tests with mocks +โ”œโ”€โ”€ ๐Ÿ“ docs/ # MoU tracking, install guides, eduGAIN and pilot docs โ”œโ”€โ”€ ๐Ÿ“ certs/ # Runtime certificates/keys (not in git) โ”œโ”€โ”€ ๐Ÿ“ blockchain-data/ # Runtime wallet/provider data (not in git) โ””โ”€โ”€ ๐Ÿ“ configuring-lab-connections/ # Guacamole connection setup docs @@ -608,6 +609,11 @@ docker compose logs -f guacamole ## ๐Ÿ“ Documentation - **Main Documentation**: This README (for main branch - full version) +- **MoU Deliverables Tracking**: [docs/MOU_DELIVERABLES.md](docs/MOU_DELIVERABLES.md) +- **Installation Guides**: [docs/install/INSTALLATION_GUIDE_EN.md](docs/install/INSTALLATION_GUIDE_EN.md) and [docs/install/GUIA_INSTALACION_ES.md](docs/install/GUIA_INSTALACION_ES.md) +- **eduGAIN Technical Guide**: [docs/edugain/EDUGAIN_INTEGRATION_EN.md](docs/edugain/EDUGAIN_INTEGRATION_EN.md) and [docs/edugain/INTEGRACION_EDUGAIN_ES.md](docs/edugain/INTEGRACION_EDUGAIN_ES.md) +- **Provider Tutorials**: [docs/tutorials/PROVIDER_TUTORIAL_EN.md](docs/tutorials/PROVIDER_TUTORIAL_EN.md) and [docs/tutorials/TUTORIAL_PROVEEDOR_ES.md](docs/tutorials/TUTORIAL_PROVEEDOR_ES.md) +- **Pilot Runbook and Templates**: [docs/pilots/PILOT_RUNBOOK.md](docs/pilots/PILOT_RUNBOOK.md) - **Logging**: [LOGGING.md](LOGGING.md) - Log configuration and management - **Guacamole Setup**: [configuring-lab-connections/guacamole-connections.md](configuring-lab-connections/guacamole-connections.md) - **Blockchain Services**: Check [blockchain-services/README.md](blockchain-services/README.md) for detailed API documentation diff --git a/SUMMARY.md b/SUMMARY.md index ecd6201..33844c3 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -8,6 +8,19 @@ * [Guacamole Connections](configuring-lab-connections/guacamole-connections.md) * [Logging Configuration](LOGGING.md) +* [Installation Guide (EN)](docs/install/INSTALLATION_GUIDE_EN.md) +* [Guia de Instalacion (ES)](docs/install/GUIA_INSTALACION_ES.md) +* [eduGAIN Integration (EN)](docs/edugain/EDUGAIN_INTEGRATION_EN.md) +* [Integracion eduGAIN (ES)](docs/edugain/INTEGRACION_EDUGAIN_ES.md) +* [Lab Provider Tutorial (EN)](docs/tutorials/PROVIDER_TUTORIAL_EN.md) +* [Tutorial Proveedor (ES)](docs/tutorials/TUTORIAL_PROVEEDOR_ES.md) + +## Project Delivery + +* [MoU Deliverables Status](docs/MOU_DELIVERABLES.md) +* [Pilot Runbook](docs/pilots/PILOT_RUNBOOK.md) +* [UNED Pilot Template](docs/pilots/UNED_PILOT_REPORT_TEMPLATE.md) +* [Partner Pilot Template](docs/pilots/PARTNER_PILOT_REPORT_TEMPLATE.md) ## Services diff --git a/docs/MOU_DELIVERABLES.md b/docs/MOU_DELIVERABLES.md new file mode 100644 index 0000000..30de92d --- /dev/null +++ b/docs/MOU_DELIVERABLES.md @@ -0,0 +1,35 @@ +# MoU Deliverables Status + +Last updated: 2026-02-13 + +This file tracks Annex I deliverables from the Vietsch MoU and maps each one to repository evidence and required external actions. + +## Status Legend + +- `DONE-IN-REPO`: completed with code/docs in this repository. +- `IN-PROGRESS-IN-REPO`: partially implemented in this repository. +- `EXTERNAL-ACTION-REQUIRED`: requires deployment, partner coordination, or federation processes outside git. + +## Deliverables Matrix + +| Deliverable | Status | Evidence in this repo | Remaining work | +| --- | --- | --- | --- | +| D1. NixOS configuration and modules for the gateway | IN-PROGRESS-IN-REPO | `flake.nix`, `nix/nixos-module.nix`, `nix/nixos-components-module.nix`, `nix/hosts/gateway.nix` | Increase component determinism for all images (OpenResty/Guacamole/blockchain-services) from Nix expressions. | +| D2. Deterministic Docker image from same config | IN-PROGRESS-IN-REPO | `nix/images/ops-worker-image.nix`, `flake.nix` package `lab-gateway-ops-worker-image` | Extend deterministic image builds to all gateway components. | +| D3. Installation and usage docs/tutorials (EN/ES) | DONE-IN-REPO | `README.md`, docs under `docs/install`, `docs/edugain`, `docs/tutorials` | Record and publish video tutorial externally. | +| D4. Two pilots and feedback incorporation | EXTERNAL-ACTION-REQUIRED | Pilot runbook and templates under `docs/pilots` | Execute pilots in real institutions and commit resulting reports/findings. | +| D5. Public release package (v1.0) with final artifacts | IN-PROGRESS-IN-REPO | Flake outputs, CI additions, docs pack | Tag/release process and artifact publication. | +| D6. Start eduGAIN registration process via NREN | EXTERNAL-ACTION-REQUIRED | eduGAIN technical guide in `docs/edugain` | Complete institutional coordination and submit federation metadata through RedIRIS/NREN. | +| D7. Release versions after pilots (NixOS + container) | IN-PROGRESS-IN-REPO | Branch work for NixOS and container pathways | Merge pilot feedback and publish tagged v1.0 release artifacts. | + +## Immediate Repository Backlog + +1. Create deterministic Nix-built images for OpenResty, Guacamole, and blockchain-services. +2. Add CI checks that evaluate NixOS configurations and build all Nix image outputs. +3. Add a release checklist and versioned release notes for v1.0. + +## External Backlog (Non-git execution) + +1. Run UNED pilot and fill `docs/pilots/UNED_PILOT_REPORT_TEMPLATE.md`. +2. Run partner pilot and fill `docs/pilots/PARTNER_PILOT_REPORT_TEMPLATE.md`. +3. Submit eduGAIN registration package with RedIRIS/NREN and archive confirmation references in docs. diff --git a/docs/edugain/EDUGAIN_INTEGRATION_EN.md b/docs/edugain/EDUGAIN_INTEGRATION_EN.md new file mode 100644 index 0000000..8a1fec0 --- /dev/null +++ b/docs/edugain/EDUGAIN_INTEGRATION_EN.md @@ -0,0 +1,52 @@ +# eduGAIN Integration Guide (Technical) + +This document covers the technical preparation for federation onboarding with eduGAIN/NREN channels. + +## 1. Scope + +The gateway itself does not register automatically in eduGAIN. Registration is an external federation process that requires: + +- institutional ownership of metadata +- NREN federation workflow (for example RedIRIS in Spain) +- operational contacts and support procedures + +## 2. Required Inputs + +1. Public service URL and TLS certificate chain. +2. EntityID and federation metadata URL decisions. +3. Signing keys and rollover policy. +4. Attribute release policy (nameID, ePPN, mail, scoped affiliation). +5. Incident/security contact and service support contact. + +## 3. Gateway-side Technical Checklist + +1. OpenID/OAuth endpoints reachable through OpenResty: + - `/.well-known/openid-configuration` + - `/auth/jwks` +2. Stable issuer URL derived from `SERVER_NAME` and `HTTPS_PORT`. +3. Token validation and audience checks aligned with federated identity assumptions. +4. CORS and callback URLs aligned with final public domains. +5. Monitoring and logs enabled for auth paths. + +## 4. NREN Submission Checklist + +1. Prepare metadata package requested by the NREN. +2. Submit service endpoints and certificates. +3. Validate test federation login flow. +4. Resolve metadata validation comments. +5. Request propagation to eduGAIN aggregate. + +## 5. Evidence to Store in Repository + +When external steps are completed, add references under `docs/pilots/`: + +- registration ticket IDs +- federation validation reports +- date of publication in federation metadata +- known limitations/open issues + +## 6. Security Notes + +- Keep private signing keys out of git. +- Document rotation procedures and emergency revocation path. +- Keep clear owner contacts for federation trust incidents. diff --git a/docs/edugain/INTEGRACION_EDUGAIN_ES.md b/docs/edugain/INTEGRACION_EDUGAIN_ES.md new file mode 100644 index 0000000..7cfcaf6 --- /dev/null +++ b/docs/edugain/INTEGRACION_EDUGAIN_ES.md @@ -0,0 +1,52 @@ +# Guia Tecnica de Integracion eduGAIN + +Este documento cubre la preparacion tecnica para el onboarding federado mediante eduGAIN/NREN. + +## 1. Alcance + +El gateway no se registra automaticamente en eduGAIN. El registro es un proceso externo de federacion que requiere: + +- titularidad institucional del metadata +- flujo de federacion del NREN (por ejemplo RedIRIS en Espana) +- contactos operativos y de soporte + +## 2. Entradas necesarias + +1. URL publica del servicio y cadena TLS. +2. Definicion de EntityID y URL de metadata. +3. Claves de firma y politica de rotacion. +4. Politica de liberacion de atributos (nameID, ePPN, mail, scoped affiliation). +5. Contacto de seguridad/incidentes y contacto de soporte. + +## 3. Checklist tecnico del gateway + +1. Endpoints OpenID/OAuth accesibles via OpenResty: + - `/.well-known/openid-configuration` + - `/auth/jwks` +2. URL de issuer estable basada en `SERVER_NAME` y `HTTPS_PORT`. +3. Validacion de tokens y audiencias alineada con identidad federada. +4. CORS y callbacks alineados con dominios publicos finales. +5. Monitorizacion y logs habilitados para rutas de autenticacion. + +## 4. Checklist de envio al NREN + +1. Preparar paquete de metadata solicitado por el NREN. +2. Enviar endpoints de servicio y certificados. +3. Validar flujo de login en federacion de pruebas. +4. Resolver observaciones de validacion de metadata. +5. Solicitar propagacion al agregado eduGAIN. + +## 5. Evidencia a guardar en el repositorio + +Cuando se completen los pasos externos, anadir referencias en `docs/pilots/`: + +- IDs de tickets de registro +- informes de validacion federada +- fecha de publicacion en metadata +- limitaciones/incidencias abiertas + +## 6. Notas de seguridad + +- No versionar claves privadas de firma. +- Documentar procedimiento de rotacion y revocacion. +- Mantener contactos propietarios para incidencias de confianza federada. diff --git a/docs/install/GUIA_INSTALACION_ES.md b/docs/install/GUIA_INSTALACION_ES.md new file mode 100644 index 0000000..d3f72f6 --- /dev/null +++ b/docs/install/GUIA_INSTALACION_ES.md @@ -0,0 +1,110 @@ +# Guia de Instalacion (Espanol) + +Esta guia resume las modalidades de despliegue del DecentraLabs Gateway. + +## 1. Elegir modalidad + +1. Script de setup: `setup.sh` / `setup.bat` (recomendado en primera instalacion). +2. Docker Compose manual. +3. Wrapper Nix para compose (`nix run .#lab-gateway-docker`). +4. Host NixOS gestionado con compose (`#gateway`). +5. Host NixOS por componentes OCI (`#gateway-components`). + +## 2. Requisitos previos + +- Git +- Docker Engine +- Docker Compose plugin (`docker compose`) +- Certificados TLS para produccion (`certs/fullchain.pem`, `certs/privkey.pem`) +- Submodulo `blockchain-services` inicializado + +Opcional: + +- Nix (modos 3, 4 y 5) +- Host NixOS (modos 4 y 5) + +## 3. Preparacion comun + +```bash +git clone https://github.com/DecentraLabsCom/lite-lab-gateway.git +cd lite-lab-gateway +git submodule update --init --recursive +cp .env.example .env +cp blockchain-services/.env.example blockchain-services/.env +``` + +Despues edita `.env` y `blockchain-services/.env`. + +## 4. Modo A: Script de setup + +Linux/macOS: + +```bash +chmod +x setup.sh +./setup.sh +``` + +Windows: + +```powershell +.\setup.bat +``` + +## 5. Modo B: Docker Compose manual + +```bash +docker compose up -d --build +docker compose ps +docker compose logs -f openresty +``` + +## 6. Modo C: Wrapper Nix para Compose + +```bash +nix run .#lab-gateway-docker -- --project-dir "$PWD" --env-file "$PWD/.env" up -d --build +``` + +Parar: + +```bash +nix run .#lab-gateway-docker -- --project-dir "$PWD" --env-file "$PWD/.env" down +``` + +## 7. Modo D: Host NixOS gestionado con compose + +```bash +sudo nixos-rebuild switch --flake /srv/lab-gateway#gateway +systemctl status lab-gateway.service +``` + +## 8. Modo E: Host NixOS por componentes OCI + +```bash +sudo nixos-rebuild switch --flake /srv/lab-gateway#gateway-components +systemctl status docker-openresty.service +``` + +Si necesitas automatizacion de reservas en este modo, define: + +- `services.lab-gateway-components.opsMysqlDsn` + +## 9. Validacion post-instalacion + +```bash +curl -k https://127.0.0.1/health +curl -k https://127.0.0.1/gateway/health +``` + +Pruebas opcionales: + +```bash +./tests/integration/run-integration.sh +./tests/smoke/run-smoke.sh +``` + +## 10. Resolucion de problemas + +- Submodulo no inicializado: ejecutar `git submodule update --init --recursive`. +- Faltan certificados: agregar certs o usar fallback local autosignado. +- Permisos en bind mounts: revisar propietario de `certs/` y `blockchain-data/`. +- Servicio no accesible: revisar `docker compose logs -f` o `journalctl -u docker-openresty -f` (modo NixOS por componentes). diff --git a/docs/install/INSTALLATION_GUIDE_EN.md b/docs/install/INSTALLATION_GUIDE_EN.md new file mode 100644 index 0000000..656728e --- /dev/null +++ b/docs/install/INSTALLATION_GUIDE_EN.md @@ -0,0 +1,110 @@ +# Installation Guide (English) + +This guide consolidates installation options for DecentraLabs Gateway. + +## 1. Choose a Deployment Mode + +1. Setup script: `setup.sh` / `setup.bat` (recommended first deployment). +2. Docker Compose manual mode. +3. Nix wrapper for compose (`nix run .#lab-gateway-docker`). +4. NixOS compose-managed host (`#gateway`). +5. NixOS componentized host (`#gateway-components`). + +## 2. Prerequisites + +- Git +- Docker Engine +- Docker Compose plugin (`docker compose`) +- TLS certs for production (`certs/fullchain.pem`, `certs/privkey.pem`) +- `blockchain-services` submodule initialized + +Optional: + +- Nix (for modes 3, 4, 5) +- NixOS host (for modes 4, 5) + +## 3. Common Initial Setup + +```bash +git clone https://github.com/DecentraLabsCom/lite-lab-gateway.git +cd lite-lab-gateway +git submodule update --init --recursive +cp .env.example .env +cp blockchain-services/.env.example blockchain-services/.env +``` + +Then edit `.env` and `blockchain-services/.env`. + +## 4. Mode A: Setup Script + +Linux/macOS: + +```bash +chmod +x setup.sh +./setup.sh +``` + +Windows: + +```powershell +.\setup.bat +``` + +## 5. Mode B: Docker Compose Manual + +```bash +docker compose up -d --build +docker compose ps +docker compose logs -f openresty +``` + +## 6. Mode C: Nix Wrapper for Compose + +```bash +nix run .#lab-gateway-docker -- --project-dir "$PWD" --env-file "$PWD/.env" up -d --build +``` + +Stop: + +```bash +nix run .#lab-gateway-docker -- --project-dir "$PWD" --env-file "$PWD/.env" down +``` + +## 7. Mode D: NixOS Compose-managed Host + +```bash +sudo nixos-rebuild switch --flake /srv/lab-gateway#gateway +systemctl status lab-gateway.service +``` + +## 8. Mode E: NixOS Componentized Host + +```bash +sudo nixos-rebuild switch --flake /srv/lab-gateway#gateway-components +systemctl status docker-openresty.service +``` + +If reservation automation is needed in this mode, set: + +- `services.lab-gateway-components.opsMysqlDsn` + +## 9. Post-install Validation + +```bash +curl -k https://127.0.0.1/health +curl -k https://127.0.0.1/gateway/health +``` + +Optional tests: + +```bash +./tests/integration/run-integration.sh +./tests/smoke/run-smoke.sh +``` + +## 10. Troubleshooting + +- Submodule not initialized: run `git submodule update --init --recursive`. +- Missing cert files: add certs or use self-signed local fallback. +- Permission issues on bind mounts: verify ownership for `certs/` and `blockchain-data/`. +- Service not reachable: inspect `docker compose logs -f` or `journalctl -u docker-openresty -f` (NixOS componentized mode). diff --git a/docs/pilots/PARTNER_PILOT_REPORT_TEMPLATE.md b/docs/pilots/PARTNER_PILOT_REPORT_TEMPLATE.md new file mode 100644 index 0000000..bbb2265 --- /dev/null +++ b/docs/pilots/PARTNER_PILOT_REPORT_TEMPLATE.md @@ -0,0 +1,47 @@ +# Partner Institution Pilot Report Template + +## Metadata + +- Institution: +- NREN/Federation context: +- Date window: +- Environment owner: +- Deployment mode: +- Gateway commit: +- Submodule commit (`blockchain-services`): + +## Infrastructure + +- Host model / VM profile: +- Public URLs: +- TLS mode: +- Authentication mode: + +## Interoperability Results + +| Test | Result | Notes | +| --- | --- | --- | +| Health endpoints | | | +| OpenID/JWKS | | | +| Cross-institution auth flow | | | +| Guacamole access flow | | | +| Ops protection | | | +| Reservation automation | | | + +## Federation/Identity Notes + +- Attribute mapping: +- Metadata issues: +- Policy constraints: + +## Issues Found + +| ID | Severity | Description | Fix / workaround | Status | +| --- | --- | --- | --- | --- | +| | | | | | + +## Final Assessment + +- Production readiness: +- Required follow-up actions: +- Recommended defaults/changes for v1.0: diff --git a/docs/pilots/PILOT_RUNBOOK.md b/docs/pilots/PILOT_RUNBOOK.md new file mode 100644 index 0000000..842bbda --- /dev/null +++ b/docs/pilots/PILOT_RUNBOOK.md @@ -0,0 +1,39 @@ +# Pilot Runbook + +Use this runbook to execute institutional pilots and capture evidence for MoU deliverables. + +## 1. Pilot Scope + +- Validate end-to-end reservation + auth + remote session flow. +- Validate operational reliability (health checks, logs, recoverability). +- Capture user/admin feedback and map issues to remediation. + +## 2. Minimum Test Matrix + +1. Gateway health (`/health`, `/gateway/health`). +2. Auth metadata (`/.well-known/openid-configuration`, `/auth/jwks`). +3. Guacamole session launch after authenticated flow. +4. Ops endpoints with valid/invalid token. +5. Reservation automation behavior (if enabled). +6. TLS and CORS behavior from external clients. + +## 3. Evidence Collection + +- Deployment commit/branch references. +- Configuration fingerprint (non-secret values only). +- Logs for success and failure scenarios. +- Screenshots of user journeys. +- Performance notes (latency, startup times). +- Known issues and mitigations. + +## 4. Completion Criteria + +- All critical tests pass. +- At least one real lab station session validated. +- Institutional operators confirm maintainability and onboarding clarity. +- Findings documented in pilot report template and linked in repo. + +## 5. Report Templates + +- `docs/pilots/UNED_PILOT_REPORT_TEMPLATE.md` +- `docs/pilots/PARTNER_PILOT_REPORT_TEMPLATE.md` diff --git a/docs/pilots/UNED_PILOT_REPORT_TEMPLATE.md b/docs/pilots/UNED_PILOT_REPORT_TEMPLATE.md new file mode 100644 index 0000000..955fedb --- /dev/null +++ b/docs/pilots/UNED_PILOT_REPORT_TEMPLATE.md @@ -0,0 +1,45 @@ +# UNED Pilot Report Template + +## Metadata + +- Date window: +- Environment owner: +- Deployment mode: +- Gateway commit: +- Submodule commit (`blockchain-services`): + +## Infrastructure + +- Host model / VM profile: +- Public URLs: +- TLS mode: +- Authentication mode: + +## Test Results + +| Test | Result | Notes | +| --- | --- | --- | +| Health endpoints | | | +| OpenID/JWKS | | | +| Guacamole access flow | | | +| Ops protection | | | +| Reservation automation | | | +| CORS/TLS from user networks | | | + +## User Feedback + +- Students: +- Teachers: +- Lab operators: + +## Issues Found + +| ID | Severity | Description | Fix / workaround | Status | +| --- | --- | --- | --- | --- | +| | | | | | + +## Final Assessment + +- Production readiness: +- Required follow-up actions: +- Recommended defaults/changes for v1.0: diff --git a/docs/tutorials/PROVIDER_TUTORIAL_EN.md b/docs/tutorials/PROVIDER_TUTORIAL_EN.md new file mode 100644 index 0000000..c089d75 --- /dev/null +++ b/docs/tutorials/PROVIDER_TUTORIAL_EN.md @@ -0,0 +1,52 @@ +# Lab Provider Tutorial (English) + +This tutorial explains how a lab provider can publish and operate a remote lab with DecentraLabs Gateway. + +## 1. Prerequisites + +- Gateway deployed and healthy (`/health` and `/gateway/health`). +- Access to Guacamole admin credentials. +- Valid token for protected routes (`SECURITY_ACCESS_TOKEN` and optional `LAB_MANAGER_TOKEN`). +- Lab station host data configured for ops-worker if remote power/session control is required. + +## 2. Configure Guacamole Connections + +1. Open `https:///guacamole`. +2. Sign in with `GUAC_ADMIN_USER` and `GUAC_ADMIN_PASS`. +3. Create required protocols (RDP/VNC/SSH) for each lab station. +4. Verify test login to each connection. + +Reference: `configuring-lab-connections/guacamole-connections.md`. + +## 3. Prepare Authentication/Wallet Layer + +1. Open `https:///wallet-dashboard`. +2. Configure/import the institutional wallet when needed. +3. Validate auth endpoints: + - `/.well-known/openid-configuration` + - `/auth/jwks` +4. Confirm reservation-aware access is enabled in your policy. + +## 4. Configure Ops Worker (Optional but Recommended) + +1. Edit hosts file for lab station inventory. +2. Provide WinRM credentials through environment variables. +3. Set `MYSQL_DSN` and enable reservation automation if required. +4. Verify: + - `GET /ops/health` + - `POST /ops/api/wol` + - `POST /ops/api/winrm` + +## 5. Publish and Validate End-to-End Flow + +1. Simulate or create a reservation. +2. Authenticate with wallet/SSO path. +3. Confirm access to Guacamole session. +4. Verify logs in OpenResty, blockchain-services, and ops-worker. + +## 6. Operational Checklist + +- Rotate admin and database credentials. +- Monitor cert expiration and renewals. +- Keep submodule and container versions updated. +- Run integration/smoke tests before production updates. diff --git a/docs/tutorials/TUTORIAL_PROVEEDOR_ES.md b/docs/tutorials/TUTORIAL_PROVEEDOR_ES.md new file mode 100644 index 0000000..0516501 --- /dev/null +++ b/docs/tutorials/TUTORIAL_PROVEEDOR_ES.md @@ -0,0 +1,52 @@ +# Tutorial para Proveedores de Laboratorio (Espanol) + +Este tutorial explica como publicar y operar un laboratorio remoto con DecentraLabs Gateway. + +## 1. Requisitos previos + +- Gateway desplegado y saludable (`/health` y `/gateway/health`). +- Acceso a credenciales admin de Guacamole. +- Token valido para rutas protegidas (`SECURITY_ACCESS_TOKEN` y opcional `LAB_MANAGER_TOKEN`). +- Inventario de hosts configurado en ops-worker si se requiere control remoto de energia/sesion. + +## 2. Configurar conexiones Guacamole + +1. Abrir `https:///guacamole`. +2. Iniciar sesion con `GUAC_ADMIN_USER` y `GUAC_ADMIN_PASS`. +3. Crear conexiones RDP/VNC/SSH para cada laboratorio. +4. Verificar login de prueba en cada conexion. + +Referencia: `configuring-lab-connections/guacamole-connections.md`. + +## 3. Preparar capa de autenticacion/wallet + +1. Abrir `https:///wallet-dashboard`. +2. Configurar o importar wallet institucional cuando aplique. +3. Validar endpoints de autenticacion: + - `/.well-known/openid-configuration` + - `/auth/jwks` +4. Confirmar que el acceso por reserva esta habilitado en tu politica. + +## 4. Configurar Ops Worker (opcional, recomendado) + +1. Editar archivo de hosts con el inventario de laboratorios. +2. Definir credenciales WinRM por variables de entorno. +3. Configurar `MYSQL_DSN` y automatizacion de reservas si aplica. +4. Verificar: + - `GET /ops/health` + - `POST /ops/api/wol` + - `POST /ops/api/winrm` + +## 5. Publicar y validar flujo extremo a extremo + +1. Simular o crear una reserva. +2. Autenticarse por wallet/SSO. +3. Confirmar acceso a la sesion Guacamole. +4. Revisar logs de OpenResty, blockchain-services y ops-worker. + +## 6. Checklist operativo + +- Rotar credenciales admin y base de datos. +- Monitorizar expiracion y renovacion de certificados. +- Mantener actualizado submodulo y versiones de contenedores. +- Ejecutar pruebas de integracion/smoke antes de cambios productivos. From b12cc31f3a7e7e336524b26065349b26096b1112 Mon Sep 17 00:00:00 2001 From: Ravenink Date: Fri, 13 Feb 2026 20:43:50 +0100 Subject: [PATCH 03/16] feat(nix): close D1/D2 with component modules and deterministic images --- .github/workflows/nix-integration.yml | 6 + README.md | 29 +++- docs/MOU_DELIVERABLES.md | 10 +- docs/install/GUIA_INSTALACION_ES.md | 18 +++ docs/install/INSTALLATION_GUIDE_EN.md | 18 +++ flake.nix | 10 ++ nix/components/blockchain-services.nix | 35 +++++ nix/components/guacamole.nix | 29 ++++ nix/components/guacd.nix | 20 +++ nix/components/mysql.nix | 34 +++++ nix/components/openresty.nix | 36 +++++ nix/components/ops-worker.nix | 40 ++++++ nix/images/gateway-bundle-image.nix | 63 +++++++++ nix/images/openresty-image.nix | 113 +++++++++++++++ nix/nixos-components-module.nix | 181 ++++--------------------- 15 files changed, 482 insertions(+), 160 deletions(-) create mode 100644 nix/components/blockchain-services.nix create mode 100644 nix/components/guacamole.nix create mode 100644 nix/components/guacd.nix create mode 100644 nix/components/mysql.nix create mode 100644 nix/components/openresty.nix create mode 100644 nix/components/ops-worker.nix create mode 100644 nix/images/gateway-bundle-image.nix create mode 100644 nix/images/openresty-image.nix diff --git a/.github/workflows/nix-integration.yml b/.github/workflows/nix-integration.yml index 7844ae4..96ed957 100644 --- a/.github/workflows/nix-integration.yml +++ b/.github/workflows/nix-integration.yml @@ -30,6 +30,12 @@ jobs: - name: Build deterministic ops-worker image run: nix build .#lab-gateway-ops-worker-image + - name: Build deterministic OpenResty image + run: nix build .#lab-gateway-openresty-image + + - name: Build deterministic deployment bundle image + run: nix build .#lab-gateway-bundle-image + - name: Evaluate NixOS configurations run: | nix eval .#nixosConfigurations.gateway.config.system.build.toplevel.drvPath diff --git a/README.md b/README.md index 76ed1da..6dd077c 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,11 @@ This repository also includes a `flake.nix` with: - `packages..lab-gateway-docker`: helper CLI for the current Docker Compose stack - `packages..lab-gateway-ops-worker-image`: deterministic OCI image tarball built from Nix +- `packages..lab-gateway-openresty-image`: deterministic OpenResty OCI image from Nix +- `packages..lab-gateway-bundle-image`: deterministic deployment bundle image for non-NixOS hosts - `nixosModules.default`: NixOS module to manage the stack through systemd - `nixosModules.components`: componentized NixOS module (OCI containers, no compose) +- `nixosModules.components-*`: per-component NixOS modules (mysql, guacd, guacamole, blockchain-services, ops-worker, openresty) - `nixosModules.gateway-host`: host defaults for a dedicated NixOS gateway machine - `nixosConfigurations.gateway`: complete host config ready for `nixos-rebuild` - `nixosConfigurations.gateway-components`: host config using the componentized module @@ -190,10 +193,27 @@ sudo nixos-rebuild switch --flake /srv/lab-gateway#gateway-components ``` Notes: -- OpenResty, Guacamole and blockchain-services images are built from local Dockerfiles by a systemd build step. -- `ops-worker` image is produced deterministically by Nix (`packages..lab-gateway-ops-worker-image`). +- OpenResty and ops-worker images are produced deterministically by Nix. +- Guacamole and blockchain-services images are built from local Dockerfiles by a systemd build step. - Set `services.lab-gateway-components.opsMysqlDsn` if you want ops-worker reservation automation backed by MySQL. +#### D) Deterministic deployment bundle image (non-NixOS) + +Build bundle image from flake: + +```bash +nix build .#lab-gateway-bundle-image +``` + +Load and run (Docker socket bind required): + +```bash +docker load < result +docker run --rm -it \ + -v /var/run/docker.sock:/var/run/docker.sock \ + lab-gateway-bundle:nix up -d --build +``` + ### Manual Deployment If you prefer manual configuration: @@ -498,7 +518,10 @@ lab-gateway/ โ”‚ โ”œโ”€โ”€ lab-gateway-docker.nix # Compose wrapper package โ”‚ โ”œโ”€โ”€ nixos-module.nix # services.lab-gateway (compose-managed) module โ”‚ โ”œโ”€โ”€ nixos-components-module.nix # services.lab-gateway-components module -โ”‚ โ”œโ”€โ”€ images/ops-worker-image.nix # Nix-built OCI image +โ”‚ โ”œโ”€โ”€ components/ # Per-component NixOS modules +โ”‚ โ”œโ”€โ”€ images/ops-worker-image.nix # Nix-built ops-worker OCI image +โ”‚ โ”œโ”€โ”€ images/openresty-image.nix # Nix-built OpenResty OCI image +โ”‚ โ”œโ”€โ”€ images/gateway-bundle-image.nix # Deterministic deployment bundle image โ”‚ โ””โ”€โ”€ hosts/gateway.nix # Host defaults for nixosConfigurations.gateway โ”œโ”€โ”€ ๐Ÿ“ blockchain-services/ # Blockchain auth/wallet service (submodule) โ”œโ”€โ”€ ๐Ÿ“ openresty/ # Reverse proxy (Nginx + Lua) diff --git a/docs/MOU_DELIVERABLES.md b/docs/MOU_DELIVERABLES.md index 30de92d..a05b60e 100644 --- a/docs/MOU_DELIVERABLES.md +++ b/docs/MOU_DELIVERABLES.md @@ -14,8 +14,8 @@ This file tracks Annex I deliverables from the Vietsch MoU and maps each one to | Deliverable | Status | Evidence in this repo | Remaining work | | --- | --- | --- | --- | -| D1. NixOS configuration and modules for the gateway | IN-PROGRESS-IN-REPO | `flake.nix`, `nix/nixos-module.nix`, `nix/nixos-components-module.nix`, `nix/hosts/gateway.nix` | Increase component determinism for all images (OpenResty/Guacamole/blockchain-services) from Nix expressions. | -| D2. Deterministic Docker image from same config | IN-PROGRESS-IN-REPO | `nix/images/ops-worker-image.nix`, `flake.nix` package `lab-gateway-ops-worker-image` | Extend deterministic image builds to all gateway components. | +| D1. NixOS configuration and modules for the gateway | DONE-IN-REPO | `flake.nix`, `nix/nixos-module.nix`, `nix/nixos-components-module.nix`, per-component modules under `nix/components/`, `nix/hosts/gateway.nix` | Validate in target pilot environments and tune defaults per institution. | +| D2. Deterministic Docker image from same config | DONE-IN-REPO | `nix/images/ops-worker-image.nix`, `nix/images/openresty-image.nix`, `nix/images/gateway-bundle-image.nix`, flake packages for deterministic OCI/bundle images | Add release publication workflow for image artifacts and signed provenance. | | D3. Installation and usage docs/tutorials (EN/ES) | DONE-IN-REPO | `README.md`, docs under `docs/install`, `docs/edugain`, `docs/tutorials` | Record and publish video tutorial externally. | | D4. Two pilots and feedback incorporation | EXTERNAL-ACTION-REQUIRED | Pilot runbook and templates under `docs/pilots` | Execute pilots in real institutions and commit resulting reports/findings. | | D5. Public release package (v1.0) with final artifacts | IN-PROGRESS-IN-REPO | Flake outputs, CI additions, docs pack | Tag/release process and artifact publication. | @@ -24,9 +24,9 @@ This file tracks Annex I deliverables from the Vietsch MoU and maps each one to ## Immediate Repository Backlog -1. Create deterministic Nix-built images for OpenResty, Guacamole, and blockchain-services. -2. Add CI checks that evaluate NixOS configurations and build all Nix image outputs. -3. Add a release checklist and versioned release notes for v1.0. +1. Add deterministic Nix-built images for Guacamole and blockchain-services (current module still supports Dockerfile builds for those components). +2. Add release checklist and signed provenance for deterministic image artifacts. +3. Finalize v1.0 release notes and publication process. ## External Backlog (Non-git execution) diff --git a/docs/install/GUIA_INSTALACION_ES.md b/docs/install/GUIA_INSTALACION_ES.md index d3f72f6..155f441 100644 --- a/docs/install/GUIA_INSTALACION_ES.md +++ b/docs/install/GUIA_INSTALACION_ES.md @@ -9,6 +9,7 @@ Esta guia resume las modalidades de despliegue del DecentraLabs Gateway. 3. Wrapper Nix para compose (`nix run .#lab-gateway-docker`). 4. Host NixOS gestionado con compose (`#gateway`). 5. Host NixOS por componentes OCI (`#gateway-components`). +6. Imagen bundle determinista de despliegue (`.#lab-gateway-bundle-image`). ## 2. Requisitos previos @@ -108,3 +109,20 @@ Pruebas opcionales: - Faltan certificados: agregar certs o usar fallback local autosignado. - Permisos en bind mounts: revisar propietario de `certs/` y `blockchain-data/`. - Servicio no accesible: revisar `docker compose logs -f` o `journalctl -u docker-openresty -f` (modo NixOS por componentes). + +## 11. Imagen Bundle Determinista (No-NixOS) + +Construir: + +```bash +nix build .#lab-gateway-bundle-image +``` + +Cargar y ejecutar usando el socket Docker del host: + +```bash +docker load < result +docker run --rm -it \ + -v /var/run/docker.sock:/var/run/docker.sock \ + lab-gateway-bundle:nix up -d --build +``` diff --git a/docs/install/INSTALLATION_GUIDE_EN.md b/docs/install/INSTALLATION_GUIDE_EN.md index 656728e..8b4b581 100644 --- a/docs/install/INSTALLATION_GUIDE_EN.md +++ b/docs/install/INSTALLATION_GUIDE_EN.md @@ -9,6 +9,7 @@ This guide consolidates installation options for DecentraLabs Gateway. 3. Nix wrapper for compose (`nix run .#lab-gateway-docker`). 4. NixOS compose-managed host (`#gateway`). 5. NixOS componentized host (`#gateway-components`). +6. Deterministic deployment bundle image (`.#lab-gateway-bundle-image`). ## 2. Prerequisites @@ -108,3 +109,20 @@ Optional tests: - Missing cert files: add certs or use self-signed local fallback. - Permission issues on bind mounts: verify ownership for `certs/` and `blockchain-data/`. - Service not reachable: inspect `docker compose logs -f` or `journalctl -u docker-openresty -f` (NixOS componentized mode). + +## 11. Deterministic Bundle Image (Non-NixOS) + +Build: + +```bash +nix build .#lab-gateway-bundle-image +``` + +Load and run with host Docker socket: + +```bash +docker load < result +docker run --rm -it \ + -v /var/run/docker.sock:/var/run/docker.sock \ + lab-gateway-bundle:nix up -d --build +``` diff --git a/flake.nix b/flake.nix index 840eccb..50bf979 100644 --- a/flake.nix +++ b/flake.nix @@ -16,11 +16,15 @@ pkgs = import nixpkgs { inherit system; }; labGatewayDocker = pkgs.callPackage ./nix/lab-gateway-docker.nix { }; labGatewayOpsWorkerImage = pkgs.callPackage ./nix/images/ops-worker-image.nix { }; + labGatewayOpenrestyImage = pkgs.callPackage ./nix/images/openresty-image.nix { }; + labGatewayBundleImage = pkgs.callPackage ./nix/images/gateway-bundle-image.nix { }; in { default = labGatewayDocker; lab-gateway-docker = labGatewayDocker; lab-gateway-ops-worker-image = labGatewayOpsWorkerImage; + lab-gateway-openresty-image = labGatewayOpenrestyImage; + lab-gateway-bundle-image = labGatewayBundleImage; }); apps = forAllSystems (system: { @@ -42,6 +46,12 @@ default = import ./nix/nixos-module.nix; lab-gateway = self.nixosModules.default; components = import ./nix/nixos-components-module.nix; + components-mysql = import ./nix/components/mysql.nix; + components-guacd = import ./nix/components/guacd.nix; + components-guacamole = import ./nix/components/guacamole.nix; + components-blockchain-services = import ./nix/components/blockchain-services.nix; + components-ops-worker = import ./nix/components/ops-worker.nix; + components-openresty = import ./nix/components/openresty.nix; gateway-host = import ./nix/hosts/gateway.nix; }; diff --git a/nix/components/blockchain-services.nix b/nix/components/blockchain-services.nix new file mode 100644 index 0000000..aee63e0 --- /dev/null +++ b/nix/components/blockchain-services.nix @@ -0,0 +1,35 @@ +{ config, lib, ... }: + +let + cfg = config.services.lab-gateway-components; + inherit (lib) mkIf optionals; + gatewayEnvFiles = optionals (cfg.envFile != null) [ cfg.envFile ]; + blockchainEnvFiles = + gatewayEnvFiles ++ optionals (cfg.blockchainEnvFile != null) [ cfg.blockchainEnvFile ]; + netOpt = "--network=${cfg.networkName}"; +in +{ + config = mkIf cfg.enable { + virtualisation.oci-containers.containers."blockchain-services" = { + image = cfg.blockchainImage; + dependsOn = [ "mysql" ]; + environmentFiles = blockchainEnvFiles; + environment = { + SPRING_DATASOURCE_URL = "jdbc:mysql://mysql:3306/blockchain_services?serverTimezone=UTC&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true"; + PROVIDER_CONFIG_PATH = "/app/data/provider.properties"; + }; + volumes = [ + "${cfg.projectDir}/certs:/app/config/keys" + "${cfg.projectDir}/blockchain-data:/app/data" + ]; + extraOptions = [ netOpt ]; + }; + + systemd.services."docker-blockchain-services" = { + wants = [ "lab-gateway-create-network.service" ] ++ + optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; + after = [ "lab-gateway-create-network.service" ] ++ + optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; + }; + }; +} diff --git a/nix/components/guacamole.nix b/nix/components/guacamole.nix new file mode 100644 index 0000000..f33073b --- /dev/null +++ b/nix/components/guacamole.nix @@ -0,0 +1,29 @@ +{ config, lib, ... }: + +let + cfg = config.services.lab-gateway-components; + inherit (lib) mkIf optionals; + gatewayEnvFiles = optionals (cfg.envFile != null) [ cfg.envFile ]; + netOpt = "--network=${cfg.networkName}"; +in +{ + config = mkIf cfg.enable { + virtualisation.oci-containers.containers.guacamole = { + image = cfg.guacamoleImage; + dependsOn = [ "mysql" "guacd" ]; + environmentFiles = gatewayEnvFiles; + environment = { + GUACD_HOSTNAME = "guacd"; + MYSQL_HOSTNAME = "mysql"; + }; + extraOptions = [ netOpt ]; + }; + + systemd.services."docker-guacamole" = { + wants = [ "lab-gateway-create-network.service" ] ++ + optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; + after = [ "lab-gateway-create-network.service" ] ++ + optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; + }; + }; +} diff --git a/nix/components/guacd.nix b/nix/components/guacd.nix new file mode 100644 index 0000000..a4e0a11 --- /dev/null +++ b/nix/components/guacd.nix @@ -0,0 +1,20 @@ +{ config, lib, ... }: + +let + cfg = config.services.lab-gateway-components; + inherit (lib) mkIf; + netOpt = "--network=${cfg.networkName}"; +in +{ + config = mkIf cfg.enable { + virtualisation.oci-containers.containers.guacd = { + image = "guacamole/guacd:1.5.5"; + extraOptions = [ netOpt ]; + }; + + systemd.services."docker-guacd" = { + wants = [ "lab-gateway-create-network.service" ]; + after = [ "lab-gateway-create-network.service" ]; + }; + }; +} diff --git a/nix/components/mysql.nix b/nix/components/mysql.nix new file mode 100644 index 0000000..889aadc --- /dev/null +++ b/nix/components/mysql.nix @@ -0,0 +1,34 @@ +{ config, lib, ... }: + +let + cfg = config.services.lab-gateway-components; + inherit (lib) mkIf optionals; + gatewayEnvFiles = optionals (cfg.envFile != null) [ cfg.envFile ]; + netOpt = "--network=${cfg.networkName}"; +in +{ + config = mkIf cfg.enable { + virtualisation.oci-containers.containers.mysql = { + image = "mysql:8.0.41"; + entrypoint = [ "/bin/bash" "/usr/local/bin/ensure-user-entrypoint.sh" ]; + cmd = [ "mysqld" ]; + environmentFiles = gatewayEnvFiles; + environment = { + BLOCKCHAIN_MYSQL_DATABASE = "blockchain_services"; + }; + volumes = [ + "mysql_data:/var/lib/mysql" + "${cfg.projectDir}/mysql/ensure-user-entrypoint.sh:/usr/local/bin/ensure-user-entrypoint.sh:ro" + "${cfg.projectDir}/mysql/000-ensure-user.sh:/docker-entrypoint-initdb.d/000-ensure-user.sh:ro" + "${cfg.projectDir}/mysql/001-create-schema.sql:/docker-entrypoint-initdb.d/001-create-schema.sql:ro" + "${cfg.projectDir}/mysql/002-labstation-ops.sql:/docker-entrypoint-initdb.d/002-labstation-ops.sql:ro" + ]; + extraOptions = [ netOpt ]; + }; + + systemd.services."docker-mysql" = { + wants = [ "lab-gateway-create-network.service" ]; + after = [ "lab-gateway-create-network.service" ]; + }; + }; +} diff --git a/nix/components/openresty.nix b/nix/components/openresty.nix new file mode 100644 index 0000000..91437d2 --- /dev/null +++ b/nix/components/openresty.nix @@ -0,0 +1,36 @@ +{ config, lib, ... }: + +let + cfg = config.services.lab-gateway-components; + inherit (lib) mkIf optionals; + gatewayEnvFiles = optionals (cfg.envFile != null) [ cfg.envFile ]; + netOpt = "--network=${cfg.networkName}"; +in +{ + config = mkIf cfg.enable { + virtualisation.oci-containers.containers.openresty = { + image = cfg.openrestyImage; + imageFile = cfg.openrestyImageFile; + dependsOn = [ "guacamole" "blockchain-services" "ops-worker" ]; + environmentFiles = gatewayEnvFiles; + volumes = [ + "${cfg.projectDir}/certs:/etc/ssl/private" + "${cfg.projectDir}/certbot/www:/var/www/certbot" + "${cfg.projectDir}/openresty/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro" + "${cfg.projectDir}/openresty/lab_access.conf:/etc/openresty/lab_access.conf:ro" + "${cfg.projectDir}/openresty/lua:/etc/openresty/lua:ro" + "${cfg.projectDir}/web:/var/www/html:ro" + ]; + ports = [ + "${cfg.openrestyBindAddress}:${toString cfg.openrestyHttpsPort}:443" + "${cfg.openrestyBindAddress}:${toString cfg.openrestyHttpPort}:80" + ]; + extraOptions = [ netOpt "--tmpfs=/tmp:size=64m,mode=1777" "--tmpfs=/var/run:size=16m,mode=755" ]; + }; + + systemd.services."docker-openresty" = { + wants = [ "lab-gateway-create-network.service" ]; + after = [ "lab-gateway-create-network.service" ]; + }; + }; +} diff --git a/nix/components/ops-worker.nix b/nix/components/ops-worker.nix new file mode 100644 index 0000000..4cff10c --- /dev/null +++ b/nix/components/ops-worker.nix @@ -0,0 +1,40 @@ +{ config, lib, ... }: + +let + cfg = config.services.lab-gateway-components; + inherit (lib) mkIf optionalAttrs optionals; + gatewayEnvFiles = optionals (cfg.envFile != null) [ cfg.envFile ]; + netOpt = "--network=${cfg.networkName}"; +in +{ + config = mkIf cfg.enable { + virtualisation.oci-containers.containers."ops-worker" = { + image = cfg.opsWorkerImageName; + imageFile = cfg.opsWorkerImageFile; + dependsOn = [ "mysql" ]; + environmentFiles = gatewayEnvFiles; + environment = { + OPS_BIND = "0.0.0.0"; + OPS_PORT = "8081"; + OPS_CONFIG = "/app/hosts.json"; + OPS_POLL_ENABLED = "true"; + OPS_POLL_INTERVAL = "60"; + OPS_RESERVATION_AUTOMATION = "true"; + OPS_RESERVATION_SCAN_INTERVAL = "30"; + OPS_RESERVATION_START_LEAD = "120"; + OPS_RESERVATION_END_DELAY = "60"; + } // optionalAttrs (cfg.opsMysqlDsn != null) { + MYSQL_DSN = cfg.opsMysqlDsn; + }; + volumes = [ + "${cfg.opsConfigPath}:/app/hosts.json:ro" + ]; + extraOptions = [ netOpt "--read-only" "--tmpfs=/tmp:size=32m,mode=1777" ]; + }; + + systemd.services."docker-ops-worker" = { + wants = [ "lab-gateway-create-network.service" ]; + after = [ "lab-gateway-create-network.service" ]; + }; + }; +} diff --git a/nix/images/gateway-bundle-image.nix b/nix/images/gateway-bundle-image.nix new file mode 100644 index 0000000..8464fd9 --- /dev/null +++ b/nix/images/gateway-bundle-image.nix @@ -0,0 +1,63 @@ +{ dockerTools +, stdenvNoCC +, lib +, bash +, coreutils +, docker +, docker-compose +, callPackage +}: + +let + gatewayCli = callPackage ../lab-gateway-docker.nix { }; + + bundleSource = lib.cleanSourceWith { + src = ../../.; + filter = path: type: + let + rel = lib.removePrefix (toString ../../.) (toString path); + in + !(lib.hasInfix "/.git" rel || lib.hasInfix "/dist" rel || lib.hasInfix "/result" rel); + }; + + bundleFiles = stdenvNoCC.mkDerivation { + pname = "lab-gateway-bundle-files"; + version = "1.0"; + src = bundleSource; + dontConfigure = true; + dontBuild = true; + installPhase = '' + mkdir -p $out/opt/lab-gateway + cp -r . $out/opt/lab-gateway/ + ''; + }; +in +dockerTools.buildLayeredImage { + name = "lab-gateway-bundle"; + tag = "nix"; + + contents = [ + bash + coreutils + docker + docker-compose + gatewayCli + bundleFiles + ]; + + extraCommands = '' + mkdir -p usr/local/bin + cat > usr/local/bin/lab-gateway-bundle <<'EOF' + #!/bin/sh + set -eu + project_dir="${LAB_GATEWAY_PROJECT_DIR:-/opt/lab-gateway}" + exec ${gatewayCli}/bin/lab-gateway --project-dir "$project_dir" "$@" + EOF + chmod +x usr/local/bin/lab-gateway-bundle + ''; + + config = { + WorkingDir = "/opt/lab-gateway"; + Entrypoint = [ "/usr/local/bin/lab-gateway-bundle" ]; + }; +} diff --git a/nix/images/openresty-image.nix b/nix/images/openresty-image.nix new file mode 100644 index 0000000..b4eb189 --- /dev/null +++ b/nix/images/openresty-image.nix @@ -0,0 +1,113 @@ +{ dockerTools +, fetchFromGitHub +, bash +, coreutils +, curl +, findutils +, gawk +, gnugrep +, gnused +, nginx +, openresty +, openssl +, certbot +}: + +let + luaRestyHttp = fetchFromGitHub { + owner = "ledgetech"; + repo = "lua-resty-http"; + rev = "v0.17.2"; + hash = "sha256-PaGMqFgiQ+/ygwJZHjZlHcf6sEbnczaqSm+nGLzM5KI="; + }; + + luaRestyJwt = fetchFromGitHub { + owner = "SkyLothar"; + repo = "lua-resty-jwt"; + rev = "v0.1.11"; + hash = "sha256-58KwuO3xTu11ac1WhPwAxvl6ir9tbA5GLNSbSyZuM5A="; + }; + + luaRestyOpenssl = fetchFromGitHub { + owner = "fffonion"; + repo = "lua-resty-openssl"; + rev = "1.6.4"; + hash = "sha256-UfYnv/bjmhoH7KyS3tDu/H2mbxuX20DkiGELDTA55fs="; + }; + + luaRestyMysql = fetchFromGitHub { + owner = "openresty"; + repo = "lua-resty-mysql"; + rev = "v0.27"; + hash = "sha256-7ort7///WpJMEhcsoCep2xX7ADTTi3GZF5XenNwLyXU="; + }; + + luaRestyString = fetchFromGitHub { + owner = "openresty"; + repo = "lua-resty-string"; + rev = "v0.16"; + hash = "sha256-d/AGqX/Uo75Kgtzy1fFILjmbcPs1RUxbWtS5f/Hd7Q0="; + }; +in +dockerTools.buildLayeredImage { + name = "lab-gateway-openresty"; + tag = "nix"; + + contents = [ + bash + coreutils + curl + findutils + gawk + gnugrep + gnused + nginx + openresty + openssl + certbot + ]; + + extraCommands = '' + mkdir -p usr/local/bin + mkdir -p usr/local/openresty/bin + mkdir -p usr/local/openresty/nginx/conf + mkdir -p usr/local/openresty/site/lualib/resty + mkdir -p etc/openresty + mkdir -p etc/ssl/private + mkdir -p var/www/html + mkdir -p var/www/certbot + + cat > usr/local/openresty/bin/openresty <<'EOF' + #!/bin/sh + exec ${openresty}/bin/openresty -p /usr/local/openresty/nginx/ -c conf/nginx.conf "$@" + EOF + chmod +x usr/local/openresty/bin/openresty + + cp ${nginx}/conf/mime.types usr/local/openresty/nginx/conf/mime.types + cp ${../../openresty/nginx.conf} usr/local/openresty/nginx/conf/nginx.conf + cp ${../../openresty/lab_access.conf} etc/openresty/lab_access.conf + cp ${../../openresty/init-ssl.sh} usr/local/bin/init-ssl.sh + chmod +x usr/local/bin/init-ssl.sh + + cp -r ${../../openresty/lua} etc/openresty/lua + cp -r ${../../web}/. var/www/html/ + + cp -r ${luaRestyHttp}/lib/resty/. usr/local/openresty/site/lualib/resty/ + cp -r ${luaRestyJwt}/lib/resty/. usr/local/openresty/site/lualib/resty/ + cp -r ${luaRestyMysql}/lib/resty/. usr/local/openresty/site/lualib/resty/ + cp -r ${luaRestyString}/lib/resty/. usr/local/openresty/site/lualib/resty/ + cp -r ${luaRestyOpenssl}/lib/resty/. usr/local/openresty/site/lualib/resty/ + ''; + + config = { + WorkingDir = "/"; + Env = [ + "PATH=/usr/local/openresty/bin:/usr/local/bin:/bin" + ]; + ExposedPorts = { + "80/tcp" = { }; + "443/tcp" = { }; + }; + Cmd = [ "/usr/local/bin/init-ssl.sh" ]; + }; +} diff --git a/nix/nixos-components-module.nix b/nix/nixos-components-module.nix index e898fe4..174dc17 100644 --- a/nix/nixos-components-module.nix +++ b/nix/nixos-components-module.nix @@ -2,16 +2,18 @@ let cfg = config.services.lab-gateway-components; - inherit (lib) mkEnableOption mkIf mkOption optionalAttrs optionals types; - - gatewayEnvFiles = optionals (cfg.envFile != null) [ cfg.envFile ]; - blockchainEnvFiles = - gatewayEnvFiles ++ - optionals (cfg.blockchainEnvFile != null) [ cfg.blockchainEnvFile ]; - - netOpt = "--network=${cfg.networkName}"; + inherit (lib) mkEnableOption mkIf mkOption types; in { + imports = [ + ./components/mysql.nix + ./components/guacd.nix + ./components/guacamole.nix + ./components/blockchain-services.nix + ./components/ops-worker.nix + ./components/openresty.nix + ]; + options.services.lab-gateway-components = { enable = mkEnableOption "DecentraLabs Gateway (componentized OCI containers)"; @@ -39,10 +41,13 @@ in description = "Docker network used by gateway containers."; }; - openrestyImage = mkOption { - type = types.str; - default = "lab-gateway/openresty:local"; - description = "Docker image tag for OpenResty."; + buildLocalImages = mkOption { + type = types.bool; + default = true; + description = '' + Build Guacamole and blockchain-services images from local Dockerfiles + before starting component containers. + ''; }; guacamoleImage = mkOption { @@ -57,6 +62,18 @@ in description = "Docker image tag for blockchain-services."; }; + openrestyImage = mkOption { + type = types.str; + default = "lab-gateway-openresty:nix"; + description = "Docker image tag for OpenResty."; + }; + + openrestyImageFile = mkOption { + type = types.package; + default = pkgs.callPackage ./images/openresty-image.nix { }; + description = "Nix-built OCI image tarball for OpenResty."; + }; + opsWorkerImageName = mkOption { type = types.str; default = "lab-gateway-ops-worker:nix"; @@ -69,15 +86,6 @@ in description = "Nix-built OCI image tarball for ops-worker."; }; - buildLocalImages = mkOption { - type = types.bool; - default = true; - description = '' - Build OpenResty, Guacamole and blockchain-services images from local Dockerfiles - before starting component containers. - ''; - }; - openrestyBindAddress = mkOption { type = types.str; default = "127.0.0.1"; @@ -144,140 +152,9 @@ in RemainAfterExit = true; }; script = '' - docker build -t ${lib.escapeShellArg cfg.openrestyImage} ${lib.escapeShellArg "${cfg.projectDir}/openresty"} docker build -t ${lib.escapeShellArg cfg.guacamoleImage} ${lib.escapeShellArg "${cfg.projectDir}/guacamole"} docker build -t ${lib.escapeShellArg cfg.blockchainImage} ${lib.escapeShellArg "${cfg.projectDir}/blockchain-services"} ''; }; - - virtualisation.oci-containers.containers = { - mysql = { - image = "mysql:8.0.41"; - entrypoint = [ "/bin/bash" "/usr/local/bin/ensure-user-entrypoint.sh" ]; - cmd = [ "mysqld" ]; - environmentFiles = gatewayEnvFiles; - environment = { - BLOCKCHAIN_MYSQL_DATABASE = "blockchain_services"; - }; - volumes = [ - "mysql_data:/var/lib/mysql" - "${cfg.projectDir}/mysql/ensure-user-entrypoint.sh:/usr/local/bin/ensure-user-entrypoint.sh:ro" - "${cfg.projectDir}/mysql/000-ensure-user.sh:/docker-entrypoint-initdb.d/000-ensure-user.sh:ro" - "${cfg.projectDir}/mysql/001-create-schema.sql:/docker-entrypoint-initdb.d/001-create-schema.sql:ro" - "${cfg.projectDir}/mysql/002-labstation-ops.sql:/docker-entrypoint-initdb.d/002-labstation-ops.sql:ro" - ]; - extraOptions = [ netOpt ]; - }; - - guacd = { - image = "guacamole/guacd:1.5.5"; - extraOptions = [ netOpt ]; - }; - - guacamole = { - image = cfg.guacamoleImage; - dependsOn = [ "mysql" "guacd" ]; - environmentFiles = gatewayEnvFiles; - environment = { - GUACD_HOSTNAME = "guacd"; - MYSQL_HOSTNAME = "mysql"; - }; - extraOptions = [ netOpt ]; - }; - - "blockchain-services" = { - image = cfg.blockchainImage; - dependsOn = [ "mysql" ]; - environmentFiles = blockchainEnvFiles; - environment = { - SPRING_DATASOURCE_URL = "jdbc:mysql://mysql:3306/blockchain_services?serverTimezone=UTC&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true"; - PROVIDER_CONFIG_PATH = "/app/data/provider.properties"; - }; - volumes = [ - "${cfg.projectDir}/certs:/app/config/keys" - "${cfg.projectDir}/blockchain-data:/app/data" - ]; - extraOptions = [ netOpt ]; - }; - - "ops-worker" = { - image = cfg.opsWorkerImageName; - imageFile = cfg.opsWorkerImageFile; - dependsOn = [ "mysql" ]; - environmentFiles = gatewayEnvFiles; - environment = { - OPS_BIND = "0.0.0.0"; - OPS_PORT = "8081"; - OPS_CONFIG = "/app/hosts.json"; - OPS_POLL_ENABLED = "true"; - OPS_POLL_INTERVAL = "60"; - OPS_RESERVATION_AUTOMATION = "true"; - OPS_RESERVATION_SCAN_INTERVAL = "30"; - OPS_RESERVATION_START_LEAD = "120"; - OPS_RESERVATION_END_DELAY = "60"; - } // optionalAttrs (cfg.opsMysqlDsn != null) { - MYSQL_DSN = cfg.opsMysqlDsn; - }; - volumes = [ - "${cfg.opsConfigPath}:/app/hosts.json:ro" - ]; - extraOptions = [ netOpt "--read-only" "--tmpfs=/tmp:size=32m,mode=1777" ]; - }; - - openresty = { - image = cfg.openrestyImage; - dependsOn = [ "guacamole" "blockchain-services" "ops-worker" ]; - environmentFiles = gatewayEnvFiles; - volumes = [ - "${cfg.projectDir}/certs:/etc/ssl/private" - "${cfg.projectDir}/certbot/www:/var/www/certbot" - "${cfg.projectDir}/openresty/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro" - "${cfg.projectDir}/openresty/lab_access.conf:/etc/openresty/lab_access.conf:ro" - "${cfg.projectDir}/openresty/lua:/etc/openresty/lua:ro" - "${cfg.projectDir}/web:/var/www/html:ro" - ]; - ports = [ - "${cfg.openrestyBindAddress}:${toString cfg.openrestyHttpsPort}:443" - "${cfg.openrestyBindAddress}:${toString cfg.openrestyHttpPort}:80" - ]; - extraOptions = [ netOpt "--tmpfs=/tmp:size=64m,mode=1777" "--tmpfs=/var/run:size=16m,mode=755" ]; - }; - }; - - systemd.services."docker-mysql" = { - wants = [ "lab-gateway-create-network.service" ]; - after = [ "lab-gateway-create-network.service" ]; - }; - - systemd.services."docker-guacd" = { - wants = [ "lab-gateway-create-network.service" ]; - after = [ "lab-gateway-create-network.service" ]; - }; - - systemd.services."docker-guacamole" = { - wants = [ "lab-gateway-create-network.service" ] ++ - optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; - after = [ "lab-gateway-create-network.service" ] ++ - optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; - }; - - systemd.services."docker-blockchain-services" = { - wants = [ "lab-gateway-create-network.service" ] ++ - optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; - after = [ "lab-gateway-create-network.service" ] ++ - optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; - }; - - systemd.services."docker-ops-worker" = { - wants = [ "lab-gateway-create-network.service" ]; - after = [ "lab-gateway-create-network.service" ]; - }; - - systemd.services."docker-openresty" = { - wants = [ "lab-gateway-create-network.service" ] ++ - optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; - after = [ "lab-gateway-create-network.service" ] ++ - optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; - }; }; } From daf431a2168d64216fd0ce34667b5d7aba2444a7 Mon Sep 17 00:00:00 2001 From: Ravenink Date: Sat, 14 Feb 2026 12:27:47 +0100 Subject: [PATCH 04/16] Updates docs --- .gitignore | 5 --- SUMMARY.md | 7 --- docs/MOU_DELIVERABLES.md | 35 --------------- docs/pilots/PARTNER_PILOT_REPORT_TEMPLATE.md | 47 -------------------- docs/pilots/PILOT_RUNBOOK.md | 39 ---------------- docs/pilots/UNED_PILOT_REPORT_TEMPLATE.md | 45 ------------------- 6 files changed, 178 deletions(-) delete mode 100644 docs/MOU_DELIVERABLES.md delete mode 100644 docs/pilots/PARTNER_PILOT_REPORT_TEMPLATE.md delete mode 100644 docs/pilots/PILOT_RUNBOOK.md delete mode 100644 docs/pilots/UNED_PILOT_REPORT_TEMPLATE.md diff --git a/.gitignore b/.gitignore index 5558e69..afa95a2 100644 --- a/.gitignore +++ b/.gitignore @@ -57,12 +57,7 @@ Thumbs.db # Development notes and planning (keep local only) dev/ -docs-lab-station/ ops-worker/hosts.json -# Dev scripts and tools -update-blockchain-services.sh -update-blockchain-services.bat - # Smart contracts contracts/ diff --git a/SUMMARY.md b/SUMMARY.md index 33844c3..d384efe 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -15,13 +15,6 @@ * [Lab Provider Tutorial (EN)](docs/tutorials/PROVIDER_TUTORIAL_EN.md) * [Tutorial Proveedor (ES)](docs/tutorials/TUTORIAL_PROVEEDOR_ES.md) -## Project Delivery - -* [MoU Deliverables Status](docs/MOU_DELIVERABLES.md) -* [Pilot Runbook](docs/pilots/PILOT_RUNBOOK.md) -* [UNED Pilot Template](docs/pilots/UNED_PILOT_REPORT_TEMPLATE.md) -* [Partner Pilot Template](docs/pilots/PARTNER_PILOT_REPORT_TEMPLATE.md) - ## Services * [Blockchain Services](blockchain-services/README.md) diff --git a/docs/MOU_DELIVERABLES.md b/docs/MOU_DELIVERABLES.md deleted file mode 100644 index a05b60e..0000000 --- a/docs/MOU_DELIVERABLES.md +++ /dev/null @@ -1,35 +0,0 @@ -# MoU Deliverables Status - -Last updated: 2026-02-13 - -This file tracks Annex I deliverables from the Vietsch MoU and maps each one to repository evidence and required external actions. - -## Status Legend - -- `DONE-IN-REPO`: completed with code/docs in this repository. -- `IN-PROGRESS-IN-REPO`: partially implemented in this repository. -- `EXTERNAL-ACTION-REQUIRED`: requires deployment, partner coordination, or federation processes outside git. - -## Deliverables Matrix - -| Deliverable | Status | Evidence in this repo | Remaining work | -| --- | --- | --- | --- | -| D1. NixOS configuration and modules for the gateway | DONE-IN-REPO | `flake.nix`, `nix/nixos-module.nix`, `nix/nixos-components-module.nix`, per-component modules under `nix/components/`, `nix/hosts/gateway.nix` | Validate in target pilot environments and tune defaults per institution. | -| D2. Deterministic Docker image from same config | DONE-IN-REPO | `nix/images/ops-worker-image.nix`, `nix/images/openresty-image.nix`, `nix/images/gateway-bundle-image.nix`, flake packages for deterministic OCI/bundle images | Add release publication workflow for image artifacts and signed provenance. | -| D3. Installation and usage docs/tutorials (EN/ES) | DONE-IN-REPO | `README.md`, docs under `docs/install`, `docs/edugain`, `docs/tutorials` | Record and publish video tutorial externally. | -| D4. Two pilots and feedback incorporation | EXTERNAL-ACTION-REQUIRED | Pilot runbook and templates under `docs/pilots` | Execute pilots in real institutions and commit resulting reports/findings. | -| D5. Public release package (v1.0) with final artifacts | IN-PROGRESS-IN-REPO | Flake outputs, CI additions, docs pack | Tag/release process and artifact publication. | -| D6. Start eduGAIN registration process via NREN | EXTERNAL-ACTION-REQUIRED | eduGAIN technical guide in `docs/edugain` | Complete institutional coordination and submit federation metadata through RedIRIS/NREN. | -| D7. Release versions after pilots (NixOS + container) | IN-PROGRESS-IN-REPO | Branch work for NixOS and container pathways | Merge pilot feedback and publish tagged v1.0 release artifacts. | - -## Immediate Repository Backlog - -1. Add deterministic Nix-built images for Guacamole and blockchain-services (current module still supports Dockerfile builds for those components). -2. Add release checklist and signed provenance for deterministic image artifacts. -3. Finalize v1.0 release notes and publication process. - -## External Backlog (Non-git execution) - -1. Run UNED pilot and fill `docs/pilots/UNED_PILOT_REPORT_TEMPLATE.md`. -2. Run partner pilot and fill `docs/pilots/PARTNER_PILOT_REPORT_TEMPLATE.md`. -3. Submit eduGAIN registration package with RedIRIS/NREN and archive confirmation references in docs. diff --git a/docs/pilots/PARTNER_PILOT_REPORT_TEMPLATE.md b/docs/pilots/PARTNER_PILOT_REPORT_TEMPLATE.md deleted file mode 100644 index bbb2265..0000000 --- a/docs/pilots/PARTNER_PILOT_REPORT_TEMPLATE.md +++ /dev/null @@ -1,47 +0,0 @@ -# Partner Institution Pilot Report Template - -## Metadata - -- Institution: -- NREN/Federation context: -- Date window: -- Environment owner: -- Deployment mode: -- Gateway commit: -- Submodule commit (`blockchain-services`): - -## Infrastructure - -- Host model / VM profile: -- Public URLs: -- TLS mode: -- Authentication mode: - -## Interoperability Results - -| Test | Result | Notes | -| --- | --- | --- | -| Health endpoints | | | -| OpenID/JWKS | | | -| Cross-institution auth flow | | | -| Guacamole access flow | | | -| Ops protection | | | -| Reservation automation | | | - -## Federation/Identity Notes - -- Attribute mapping: -- Metadata issues: -- Policy constraints: - -## Issues Found - -| ID | Severity | Description | Fix / workaround | Status | -| --- | --- | --- | --- | --- | -| | | | | | - -## Final Assessment - -- Production readiness: -- Required follow-up actions: -- Recommended defaults/changes for v1.0: diff --git a/docs/pilots/PILOT_RUNBOOK.md b/docs/pilots/PILOT_RUNBOOK.md deleted file mode 100644 index 842bbda..0000000 --- a/docs/pilots/PILOT_RUNBOOK.md +++ /dev/null @@ -1,39 +0,0 @@ -# Pilot Runbook - -Use this runbook to execute institutional pilots and capture evidence for MoU deliverables. - -## 1. Pilot Scope - -- Validate end-to-end reservation + auth + remote session flow. -- Validate operational reliability (health checks, logs, recoverability). -- Capture user/admin feedback and map issues to remediation. - -## 2. Minimum Test Matrix - -1. Gateway health (`/health`, `/gateway/health`). -2. Auth metadata (`/.well-known/openid-configuration`, `/auth/jwks`). -3. Guacamole session launch after authenticated flow. -4. Ops endpoints with valid/invalid token. -5. Reservation automation behavior (if enabled). -6. TLS and CORS behavior from external clients. - -## 3. Evidence Collection - -- Deployment commit/branch references. -- Configuration fingerprint (non-secret values only). -- Logs for success and failure scenarios. -- Screenshots of user journeys. -- Performance notes (latency, startup times). -- Known issues and mitigations. - -## 4. Completion Criteria - -- All critical tests pass. -- At least one real lab station session validated. -- Institutional operators confirm maintainability and onboarding clarity. -- Findings documented in pilot report template and linked in repo. - -## 5. Report Templates - -- `docs/pilots/UNED_PILOT_REPORT_TEMPLATE.md` -- `docs/pilots/PARTNER_PILOT_REPORT_TEMPLATE.md` diff --git a/docs/pilots/UNED_PILOT_REPORT_TEMPLATE.md b/docs/pilots/UNED_PILOT_REPORT_TEMPLATE.md deleted file mode 100644 index 955fedb..0000000 --- a/docs/pilots/UNED_PILOT_REPORT_TEMPLATE.md +++ /dev/null @@ -1,45 +0,0 @@ -# UNED Pilot Report Template - -## Metadata - -- Date window: -- Environment owner: -- Deployment mode: -- Gateway commit: -- Submodule commit (`blockchain-services`): - -## Infrastructure - -- Host model / VM profile: -- Public URLs: -- TLS mode: -- Authentication mode: - -## Test Results - -| Test | Result | Notes | -| --- | --- | --- | -| Health endpoints | | | -| OpenID/JWKS | | | -| Guacamole access flow | | | -| Ops protection | | | -| Reservation automation | | | -| CORS/TLS from user networks | | | - -## User Feedback - -- Students: -- Teachers: -- Lab operators: - -## Issues Found - -| ID | Severity | Description | Fix / workaround | Status | -| --- | --- | --- | --- | --- | -| | | | | | - -## Final Assessment - -- Production readiness: -- Required follow-up actions: -- Recommended defaults/changes for v1.0: From 0ec8ffe2295e26e59e32c5ed6a1c6151a84f01a1 Mon Sep 17 00:00:00 2001 From: Ravenink Date: Sat, 14 Feb 2026 12:28:34 +0100 Subject: [PATCH 05/16] Updates ref --- blockchain-services | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blockchain-services b/blockchain-services index 9f3dcf2..bd2d5b9 160000 --- a/blockchain-services +++ b/blockchain-services @@ -1 +1 @@ -Subproject commit 9f3dcf286ded937f7420cda5b97f6ad6ee1a79dd +Subproject commit bd2d5b9e5566f359c38ae0816529943076089378 From 49c566aed19d28b8e0098608b8fce2882f6b9ac8 Mon Sep 17 00:00:00 2001 From: Ravenink Date: Sat, 14 Feb 2026 12:35:09 +0100 Subject: [PATCH 06/16] Updated docs --- README.md | 4 +--- SUMMARY.md | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6dd077c..da94b83 100644 --- a/README.md +++ b/README.md @@ -537,7 +537,7 @@ lab-gateway/ โ”œโ”€โ”€ ๐Ÿ“ tests/ โ”‚ โ”œโ”€โ”€ smoke/ # End-to-end smoke tests โ”‚ โ””โ”€โ”€ integration/ # Integration tests with mocks -โ”œโ”€โ”€ ๐Ÿ“ docs/ # MoU tracking, install guides, eduGAIN and pilot docs +โ”œโ”€โ”€ ๐Ÿ“ docs/ # Install guides, eduGAIN integration, and provider tutorials โ”œโ”€โ”€ ๐Ÿ“ certs/ # Runtime certificates/keys (not in git) โ”œโ”€โ”€ ๐Ÿ“ blockchain-data/ # Runtime wallet/provider data (not in git) โ””โ”€โ”€ ๐Ÿ“ configuring-lab-connections/ # Guacamole connection setup docs @@ -632,11 +632,9 @@ docker compose logs -f guacamole ## ๐Ÿ“ Documentation - **Main Documentation**: This README (for main branch - full version) -- **MoU Deliverables Tracking**: [docs/MOU_DELIVERABLES.md](docs/MOU_DELIVERABLES.md) - **Installation Guides**: [docs/install/INSTALLATION_GUIDE_EN.md](docs/install/INSTALLATION_GUIDE_EN.md) and [docs/install/GUIA_INSTALACION_ES.md](docs/install/GUIA_INSTALACION_ES.md) - **eduGAIN Technical Guide**: [docs/edugain/EDUGAIN_INTEGRATION_EN.md](docs/edugain/EDUGAIN_INTEGRATION_EN.md) and [docs/edugain/INTEGRACION_EDUGAIN_ES.md](docs/edugain/INTEGRACION_EDUGAIN_ES.md) - **Provider Tutorials**: [docs/tutorials/PROVIDER_TUTORIAL_EN.md](docs/tutorials/PROVIDER_TUTORIAL_EN.md) and [docs/tutorials/TUTORIAL_PROVEEDOR_ES.md](docs/tutorials/TUTORIAL_PROVEEDOR_ES.md) -- **Pilot Runbook and Templates**: [docs/pilots/PILOT_RUNBOOK.md](docs/pilots/PILOT_RUNBOOK.md) - **Logging**: [LOGGING.md](LOGGING.md) - Log configuration and management - **Guacamole Setup**: [configuring-lab-connections/guacamole-connections.md](configuring-lab-connections/guacamole-connections.md) - **Blockchain Services**: Check [blockchain-services/README.md](blockchain-services/README.md) for detailed API documentation diff --git a/SUMMARY.md b/SUMMARY.md index d384efe..ac06add 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -17,6 +17,6 @@ ## Services -* [Blockchain Services](blockchain-services/README.md) +* [Blockchain Services](blockchain-services/SUMMARY.md) * [Ops Worker](ops-worker/README.md) * [Certbot Setup](certbot/README.md) From b8a20f351ef794f52b0c2dc4a5581c3b66ef7c20 Mon Sep 17 00:00:00 2001 From: Ravenink Date: Sat, 14 Feb 2026 12:56:35 +0100 Subject: [PATCH 07/16] Simplified NixOS installation options --- .github/workflows/nix-integration.yml | 15 +-- README.md | 66 +--------- docs/install/GUIA_INSTALACION_ES.md | 59 ++------- docs/install/INSTALLATION_GUIDE_EN.md | 59 ++------- flake.nix | 57 +-------- nix/components/blockchain-services.nix | 35 ------ nix/components/guacamole.nix | 29 ----- nix/components/guacd.nix | 20 ---- nix/components/mysql.nix | 34 ------ nix/components/openresty.nix | 36 ------ nix/components/ops-worker.nix | 40 ------- nix/images/gateway-bundle-image.nix | 63 ---------- nix/images/openresty-image.nix | 113 ----------------- nix/images/ops-worker-image.nix | 56 --------- nix/nixos-components-module.nix | 160 ------------------------- 15 files changed, 22 insertions(+), 820 deletions(-) delete mode 100644 nix/components/blockchain-services.nix delete mode 100644 nix/components/guacamole.nix delete mode 100644 nix/components/guacd.nix delete mode 100644 nix/components/mysql.nix delete mode 100644 nix/components/openresty.nix delete mode 100644 nix/components/ops-worker.nix delete mode 100644 nix/images/gateway-bundle-image.nix delete mode 100644 nix/images/openresty-image.nix delete mode 100644 nix/images/ops-worker-image.nix delete mode 100644 nix/nixos-components-module.nix diff --git a/.github/workflows/nix-integration.yml b/.github/workflows/nix-integration.yml index 96ed957..238e221 100644 --- a/.github/workflows/nix-integration.yml +++ b/.github/workflows/nix-integration.yml @@ -24,22 +24,9 @@ jobs: - name: Show flake outputs run: nix flake show --all-systems - - name: Build Nix helper package - run: nix build .#lab-gateway-docker - - - name: Build deterministic ops-worker image - run: nix build .#lab-gateway-ops-worker-image - - - name: Build deterministic OpenResty image - run: nix build .#lab-gateway-openresty-image - - - name: Build deterministic deployment bundle image - run: nix build .#lab-gateway-bundle-image - - - name: Evaluate NixOS configurations + - name: Evaluate NixOS configuration run: | nix eval .#nixosConfigurations.gateway.config.system.build.toplevel.drvPath - nix eval .#nixosConfigurations.gateway-components.config.system.build.toplevel.drvPath integration-tests: runs-on: ubuntu-latest diff --git a/README.md b/README.md index da94b83..cf3b15c 100644 --- a/README.md +++ b/README.md @@ -61,15 +61,9 @@ Use one of these modes depending on your target: 2. **Manual Docker Compose** Best if you want full control over compose commands and deployment flow. -3. **Nix Wrapper for Compose (`nix run .#lab-gateway-docker`)** - Same runtime stack as Docker Compose; Nix only provides a packaged CLI wrapper. - -4. **NixOS Compose-managed Host (`nixos-rebuild --flake ...#gateway`)** +3. **NixOS Compose-managed Host (`nixos-rebuild --flake ...#gateway`)** Best for dedicated NixOS hosts where you want declarative system + service management. -5. **NixOS Componentized Host (`nixos-rebuild --flake ...#gateway-components`)** - Runs each service as an OCI container managed by NixOS modules (no `docker-compose up`). - ### Using Setup Scripts (Recommended) The setup scripts will automatically: @@ -97,31 +91,15 @@ chmod +x setup.sh That's it! The script will guide you through the setup and start all services automatically. -### Nix / NixOS Deployment +### NixOS Deployment This repository also includes a `flake.nix` with: -- `packages..lab-gateway-docker`: helper CLI for the current Docker Compose stack -- `packages..lab-gateway-ops-worker-image`: deterministic OCI image tarball built from Nix -- `packages..lab-gateway-openresty-image`: deterministic OpenResty OCI image from Nix -- `packages..lab-gateway-bundle-image`: deterministic deployment bundle image for non-NixOS hosts - `nixosModules.default`: NixOS module to manage the stack through systemd -- `nixosModules.components`: componentized NixOS module (OCI containers, no compose) -- `nixosModules.components-*`: per-component NixOS modules (mysql, guacd, guacamole, blockchain-services, ops-worker, openresty) - `nixosModules.gateway-host`: host defaults for a dedicated NixOS gateway machine - `nixosConfigurations.gateway`: complete host config ready for `nixos-rebuild` -- `nixosConfigurations.gateway-components`: host config using the componentized module - -#### A) Nix wrapper for the existing Docker stack -This mode still requires Docker Engine + Docker Compose on the host. -It runs the same `docker-compose.yml` stack; it does not replace Compose with a different runtime. - -```bash -nix run .#lab-gateway-docker -- --project-dir "$PWD" --env-file "$PWD/.env" up -d --build -``` - -#### B) NixOS host configuration (compose-managed) +#### NixOS host configuration (compose-managed) This mode is only for NixOS machines. @@ -184,36 +162,6 @@ systemctl status lab-gateway.service `nixosConfigurations.gateway` imports your existing `/etc/nixos/configuration.nix` and layers the gateway module on top, so host-specific settings (bootloader, users, disks, hardware) are preserved. Host-level values (hostname, timezone, firewall, profiles, SSH hardening) are installation-specific and should be overridden per environment. -#### C) NixOS host configuration (componentized OCI containers) - -This mode avoids `docker compose up` and manages each component as a NixOS-defined OCI container. - -```bash -sudo nixos-rebuild switch --flake /srv/lab-gateway#gateway-components -``` - -Notes: -- OpenResty and ops-worker images are produced deterministically by Nix. -- Guacamole and blockchain-services images are built from local Dockerfiles by a systemd build step. -- Set `services.lab-gateway-components.opsMysqlDsn` if you want ops-worker reservation automation backed by MySQL. - -#### D) Deterministic deployment bundle image (non-NixOS) - -Build bundle image from flake: - -```bash -nix build .#lab-gateway-bundle-image -``` - -Load and run (Docker socket bind required): - -```bash -docker load < result -docker run --rm -it \ - -v /var/run/docker.sock:/var/run/docker.sock \ - lab-gateway-bundle:nix up -d --build -``` - ### Manual Deployment If you prefer manual configuration: @@ -509,19 +457,13 @@ Internet โ”€โ”€> [NIC with VLAN tagging] Lab Gateway โ”€โ”€> VLAN 10 / VLAN 20 ``` lab-gateway/ -โ”œโ”€โ”€ ๐Ÿ“„ flake.nix # Nix flake outputs (packages + NixOS config/module) +โ”œโ”€โ”€ ๐Ÿ“„ flake.nix # Nix flake outputs (NixOS config/module) โ”œโ”€โ”€ ๐Ÿ“„ docker-compose.yml # Main service orchestration โ”œโ”€โ”€ ๐Ÿ“„ .env.example # Gateway configuration template โ”œโ”€โ”€ ๐Ÿ“„ setup.sh / setup.bat # Guided setup scripts โ”œโ”€โ”€ ๐Ÿ“„ selfsigned-refresh.sh # Self-signed cert helper โ”œโ”€โ”€ ๐Ÿ“ nix/ -โ”‚ โ”œโ”€โ”€ lab-gateway-docker.nix # Compose wrapper package โ”‚ โ”œโ”€โ”€ nixos-module.nix # services.lab-gateway (compose-managed) module -โ”‚ โ”œโ”€โ”€ nixos-components-module.nix # services.lab-gateway-components module -โ”‚ โ”œโ”€โ”€ components/ # Per-component NixOS modules -โ”‚ โ”œโ”€โ”€ images/ops-worker-image.nix # Nix-built ops-worker OCI image -โ”‚ โ”œโ”€โ”€ images/openresty-image.nix # Nix-built OpenResty OCI image -โ”‚ โ”œโ”€โ”€ images/gateway-bundle-image.nix # Deterministic deployment bundle image โ”‚ โ””โ”€โ”€ hosts/gateway.nix # Host defaults for nixosConfigurations.gateway โ”œโ”€โ”€ ๐Ÿ“ blockchain-services/ # Blockchain auth/wallet service (submodule) โ”œโ”€โ”€ ๐Ÿ“ openresty/ # Reverse proxy (Nginx + Lua) diff --git a/docs/install/GUIA_INSTALACION_ES.md b/docs/install/GUIA_INSTALACION_ES.md index 155f441..a175b26 100644 --- a/docs/install/GUIA_INSTALACION_ES.md +++ b/docs/install/GUIA_INSTALACION_ES.md @@ -1,15 +1,12 @@ # Guia de Instalacion (Espanol) -Esta guia resume las modalidades de despliegue del DecentraLabs Gateway. +Esta guia resume las modalidades soportadas de despliegue del DecentraLabs Gateway. ## 1. Elegir modalidad 1. Script de setup: `setup.sh` / `setup.bat` (recomendado en primera instalacion). 2. Docker Compose manual. -3. Wrapper Nix para compose (`nix run .#lab-gateway-docker`). -4. Host NixOS gestionado con compose (`#gateway`). -5. Host NixOS por componentes OCI (`#gateway-components`). -6. Imagen bundle determinista de despliegue (`.#lab-gateway-bundle-image`). +3. Host NixOS gestionado con compose (`#gateway`). ## 2. Requisitos previos @@ -21,8 +18,8 @@ Esta guia resume las modalidades de despliegue del DecentraLabs Gateway. Opcional: -- Nix (modos 3, 4 y 5) -- Host NixOS (modos 4 y 5) +- Nix (modo 3) +- Host NixOS (modo 3) ## 3. Preparacion comun @@ -59,37 +56,14 @@ docker compose ps docker compose logs -f openresty ``` -## 6. Modo C: Wrapper Nix para Compose - -```bash -nix run .#lab-gateway-docker -- --project-dir "$PWD" --env-file "$PWD/.env" up -d --build -``` - -Parar: - -```bash -nix run .#lab-gateway-docker -- --project-dir "$PWD" --env-file "$PWD/.env" down -``` - -## 7. Modo D: Host NixOS gestionado con compose +## 6. Modo C: Host NixOS gestionado con compose ```bash sudo nixos-rebuild switch --flake /srv/lab-gateway#gateway systemctl status lab-gateway.service ``` -## 8. Modo E: Host NixOS por componentes OCI - -```bash -sudo nixos-rebuild switch --flake /srv/lab-gateway#gateway-components -systemctl status docker-openresty.service -``` - -Si necesitas automatizacion de reservas en este modo, define: - -- `services.lab-gateway-components.opsMysqlDsn` - -## 9. Validacion post-instalacion +## 7. Validacion post-instalacion ```bash curl -k https://127.0.0.1/health @@ -103,26 +77,9 @@ Pruebas opcionales: ./tests/smoke/run-smoke.sh ``` -## 10. Resolucion de problemas +## 8. Resolucion de problemas - Submodulo no inicializado: ejecutar `git submodule update --init --recursive`. - Faltan certificados: agregar certs o usar fallback local autosignado. - Permisos en bind mounts: revisar propietario de `certs/` y `blockchain-data/`. -- Servicio no accesible: revisar `docker compose logs -f` o `journalctl -u docker-openresty -f` (modo NixOS por componentes). - -## 11. Imagen Bundle Determinista (No-NixOS) - -Construir: - -```bash -nix build .#lab-gateway-bundle-image -``` - -Cargar y ejecutar usando el socket Docker del host: - -```bash -docker load < result -docker run --rm -it \ - -v /var/run/docker.sock:/var/run/docker.sock \ - lab-gateway-bundle:nix up -d --build -``` +- Servicio no accesible: revisar `docker compose logs -f` o `journalctl -u lab-gateway.service -f` (modo NixOS). diff --git a/docs/install/INSTALLATION_GUIDE_EN.md b/docs/install/INSTALLATION_GUIDE_EN.md index 8b4b581..63d8929 100644 --- a/docs/install/INSTALLATION_GUIDE_EN.md +++ b/docs/install/INSTALLATION_GUIDE_EN.md @@ -1,15 +1,12 @@ # Installation Guide (English) -This guide consolidates installation options for DecentraLabs Gateway. +This guide consolidates the supported installation options for DecentraLabs Gateway. ## 1. Choose a Deployment Mode 1. Setup script: `setup.sh` / `setup.bat` (recommended first deployment). 2. Docker Compose manual mode. -3. Nix wrapper for compose (`nix run .#lab-gateway-docker`). -4. NixOS compose-managed host (`#gateway`). -5. NixOS componentized host (`#gateway-components`). -6. Deterministic deployment bundle image (`.#lab-gateway-bundle-image`). +3. NixOS compose-managed host (`#gateway`). ## 2. Prerequisites @@ -21,8 +18,8 @@ This guide consolidates installation options for DecentraLabs Gateway. Optional: -- Nix (for modes 3, 4, 5) -- NixOS host (for modes 4, 5) +- Nix (for mode 3) +- NixOS host (for mode 3) ## 3. Common Initial Setup @@ -59,37 +56,14 @@ docker compose ps docker compose logs -f openresty ``` -## 6. Mode C: Nix Wrapper for Compose - -```bash -nix run .#lab-gateway-docker -- --project-dir "$PWD" --env-file "$PWD/.env" up -d --build -``` - -Stop: - -```bash -nix run .#lab-gateway-docker -- --project-dir "$PWD" --env-file "$PWD/.env" down -``` - -## 7. Mode D: NixOS Compose-managed Host +## 6. Mode C: NixOS Compose-managed Host ```bash sudo nixos-rebuild switch --flake /srv/lab-gateway#gateway systemctl status lab-gateway.service ``` -## 8. Mode E: NixOS Componentized Host - -```bash -sudo nixos-rebuild switch --flake /srv/lab-gateway#gateway-components -systemctl status docker-openresty.service -``` - -If reservation automation is needed in this mode, set: - -- `services.lab-gateway-components.opsMysqlDsn` - -## 9. Post-install Validation +## 7. Post-install Validation ```bash curl -k https://127.0.0.1/health @@ -103,26 +77,9 @@ Optional tests: ./tests/smoke/run-smoke.sh ``` -## 10. Troubleshooting +## 8. Troubleshooting - Submodule not initialized: run `git submodule update --init --recursive`. - Missing cert files: add certs or use self-signed local fallback. - Permission issues on bind mounts: verify ownership for `certs/` and `blockchain-data/`. -- Service not reachable: inspect `docker compose logs -f` or `journalctl -u docker-openresty -f` (NixOS componentized mode). - -## 11. Deterministic Bundle Image (Non-NixOS) - -Build: - -```bash -nix build .#lab-gateway-bundle-image -``` - -Load and run with host Docker socket: - -```bash -docker load < result -docker run --rm -it \ - -v /var/run/docker.sock:/var/run/docker.sock \ - lab-gateway-bundle:nix up -d --build -``` +- Service not reachable: inspect `docker compose logs -f` or `journalctl -u lab-gateway.service -f` (NixOS mode). diff --git a/flake.nix b/flake.nix index 50bf979..5d105bf 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "DecentraLabs Gateway flake (Docker helpers, OCI images, and NixOS modules)"; + description = "DecentraLabs Gateway flake (NixOS module and host configuration)"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; @@ -11,33 +11,6 @@ forAllSystems = nixpkgs.lib.genAttrs systems; in { - packages = forAllSystems (system: - let - pkgs = import nixpkgs { inherit system; }; - labGatewayDocker = pkgs.callPackage ./nix/lab-gateway-docker.nix { }; - labGatewayOpsWorkerImage = pkgs.callPackage ./nix/images/ops-worker-image.nix { }; - labGatewayOpenrestyImage = pkgs.callPackage ./nix/images/openresty-image.nix { }; - labGatewayBundleImage = pkgs.callPackage ./nix/images/gateway-bundle-image.nix { }; - in - { - default = labGatewayDocker; - lab-gateway-docker = labGatewayDocker; - lab-gateway-ops-worker-image = labGatewayOpsWorkerImage; - lab-gateway-openresty-image = labGatewayOpenrestyImage; - lab-gateway-bundle-image = labGatewayBundleImage; - }); - - apps = forAllSystems (system: { - default = { - type = "app"; - program = "${self.packages.${system}.lab-gateway-docker}/bin/lab-gateway"; - }; - lab-gateway-docker = { - type = "app"; - program = "${self.packages.${system}.lab-gateway-docker}/bin/lab-gateway"; - }; - }); - formatter = forAllSystems (system: (import nixpkgs { inherit system; }).nixfmt-rfc-style ); @@ -45,13 +18,6 @@ nixosModules = { default = import ./nix/nixos-module.nix; lab-gateway = self.nixosModules.default; - components = import ./nix/nixos-components-module.nix; - components-mysql = import ./nix/components/mysql.nix; - components-guacd = import ./nix/components/guacd.nix; - components-guacamole = import ./nix/components/guacamole.nix; - components-blockchain-services = import ./nix/components/blockchain-services.nix; - components-ops-worker = import ./nix/components/ops-worker.nix; - components-openresty = import ./nix/components/openresty.nix; gateway-host = import ./nix/hosts/gateway.nix; }; @@ -74,27 +40,6 @@ ]; }; - gateway-components = nixpkgs.lib.nixosSystem { - system = "x86_64-linux"; - modules = [ - (if builtins.pathExists "/etc/nixos/configuration.nix" - then /etc/nixos/configuration.nix - else ({ ... }: { - boot.isContainer = true; - fileSystems."/" = { - device = "none"; - fsType = "tmpfs"; - }; - })) - self.nixosModules.default - self.nixosModules.components - self.nixosModules.gateway-host - ({ lib, ... }: { - services.lab-gateway.enable = lib.mkForce false; - services.lab-gateway-components.enable = true; - }) - ]; - }; }; }; } diff --git a/nix/components/blockchain-services.nix b/nix/components/blockchain-services.nix deleted file mode 100644 index aee63e0..0000000 --- a/nix/components/blockchain-services.nix +++ /dev/null @@ -1,35 +0,0 @@ -{ config, lib, ... }: - -let - cfg = config.services.lab-gateway-components; - inherit (lib) mkIf optionals; - gatewayEnvFiles = optionals (cfg.envFile != null) [ cfg.envFile ]; - blockchainEnvFiles = - gatewayEnvFiles ++ optionals (cfg.blockchainEnvFile != null) [ cfg.blockchainEnvFile ]; - netOpt = "--network=${cfg.networkName}"; -in -{ - config = mkIf cfg.enable { - virtualisation.oci-containers.containers."blockchain-services" = { - image = cfg.blockchainImage; - dependsOn = [ "mysql" ]; - environmentFiles = blockchainEnvFiles; - environment = { - SPRING_DATASOURCE_URL = "jdbc:mysql://mysql:3306/blockchain_services?serverTimezone=UTC&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true"; - PROVIDER_CONFIG_PATH = "/app/data/provider.properties"; - }; - volumes = [ - "${cfg.projectDir}/certs:/app/config/keys" - "${cfg.projectDir}/blockchain-data:/app/data" - ]; - extraOptions = [ netOpt ]; - }; - - systemd.services."docker-blockchain-services" = { - wants = [ "lab-gateway-create-network.service" ] ++ - optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; - after = [ "lab-gateway-create-network.service" ] ++ - optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; - }; - }; -} diff --git a/nix/components/guacamole.nix b/nix/components/guacamole.nix deleted file mode 100644 index f33073b..0000000 --- a/nix/components/guacamole.nix +++ /dev/null @@ -1,29 +0,0 @@ -{ config, lib, ... }: - -let - cfg = config.services.lab-gateway-components; - inherit (lib) mkIf optionals; - gatewayEnvFiles = optionals (cfg.envFile != null) [ cfg.envFile ]; - netOpt = "--network=${cfg.networkName}"; -in -{ - config = mkIf cfg.enable { - virtualisation.oci-containers.containers.guacamole = { - image = cfg.guacamoleImage; - dependsOn = [ "mysql" "guacd" ]; - environmentFiles = gatewayEnvFiles; - environment = { - GUACD_HOSTNAME = "guacd"; - MYSQL_HOSTNAME = "mysql"; - }; - extraOptions = [ netOpt ]; - }; - - systemd.services."docker-guacamole" = { - wants = [ "lab-gateway-create-network.service" ] ++ - optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; - after = [ "lab-gateway-create-network.service" ] ++ - optionals cfg.buildLocalImages [ "lab-gateway-build-images.service" ]; - }; - }; -} diff --git a/nix/components/guacd.nix b/nix/components/guacd.nix deleted file mode 100644 index a4e0a11..0000000 --- a/nix/components/guacd.nix +++ /dev/null @@ -1,20 +0,0 @@ -{ config, lib, ... }: - -let - cfg = config.services.lab-gateway-components; - inherit (lib) mkIf; - netOpt = "--network=${cfg.networkName}"; -in -{ - config = mkIf cfg.enable { - virtualisation.oci-containers.containers.guacd = { - image = "guacamole/guacd:1.5.5"; - extraOptions = [ netOpt ]; - }; - - systemd.services."docker-guacd" = { - wants = [ "lab-gateway-create-network.service" ]; - after = [ "lab-gateway-create-network.service" ]; - }; - }; -} diff --git a/nix/components/mysql.nix b/nix/components/mysql.nix deleted file mode 100644 index 889aadc..0000000 --- a/nix/components/mysql.nix +++ /dev/null @@ -1,34 +0,0 @@ -{ config, lib, ... }: - -let - cfg = config.services.lab-gateway-components; - inherit (lib) mkIf optionals; - gatewayEnvFiles = optionals (cfg.envFile != null) [ cfg.envFile ]; - netOpt = "--network=${cfg.networkName}"; -in -{ - config = mkIf cfg.enable { - virtualisation.oci-containers.containers.mysql = { - image = "mysql:8.0.41"; - entrypoint = [ "/bin/bash" "/usr/local/bin/ensure-user-entrypoint.sh" ]; - cmd = [ "mysqld" ]; - environmentFiles = gatewayEnvFiles; - environment = { - BLOCKCHAIN_MYSQL_DATABASE = "blockchain_services"; - }; - volumes = [ - "mysql_data:/var/lib/mysql" - "${cfg.projectDir}/mysql/ensure-user-entrypoint.sh:/usr/local/bin/ensure-user-entrypoint.sh:ro" - "${cfg.projectDir}/mysql/000-ensure-user.sh:/docker-entrypoint-initdb.d/000-ensure-user.sh:ro" - "${cfg.projectDir}/mysql/001-create-schema.sql:/docker-entrypoint-initdb.d/001-create-schema.sql:ro" - "${cfg.projectDir}/mysql/002-labstation-ops.sql:/docker-entrypoint-initdb.d/002-labstation-ops.sql:ro" - ]; - extraOptions = [ netOpt ]; - }; - - systemd.services."docker-mysql" = { - wants = [ "lab-gateway-create-network.service" ]; - after = [ "lab-gateway-create-network.service" ]; - }; - }; -} diff --git a/nix/components/openresty.nix b/nix/components/openresty.nix deleted file mode 100644 index 91437d2..0000000 --- a/nix/components/openresty.nix +++ /dev/null @@ -1,36 +0,0 @@ -{ config, lib, ... }: - -let - cfg = config.services.lab-gateway-components; - inherit (lib) mkIf optionals; - gatewayEnvFiles = optionals (cfg.envFile != null) [ cfg.envFile ]; - netOpt = "--network=${cfg.networkName}"; -in -{ - config = mkIf cfg.enable { - virtualisation.oci-containers.containers.openresty = { - image = cfg.openrestyImage; - imageFile = cfg.openrestyImageFile; - dependsOn = [ "guacamole" "blockchain-services" "ops-worker" ]; - environmentFiles = gatewayEnvFiles; - volumes = [ - "${cfg.projectDir}/certs:/etc/ssl/private" - "${cfg.projectDir}/certbot/www:/var/www/certbot" - "${cfg.projectDir}/openresty/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro" - "${cfg.projectDir}/openresty/lab_access.conf:/etc/openresty/lab_access.conf:ro" - "${cfg.projectDir}/openresty/lua:/etc/openresty/lua:ro" - "${cfg.projectDir}/web:/var/www/html:ro" - ]; - ports = [ - "${cfg.openrestyBindAddress}:${toString cfg.openrestyHttpsPort}:443" - "${cfg.openrestyBindAddress}:${toString cfg.openrestyHttpPort}:80" - ]; - extraOptions = [ netOpt "--tmpfs=/tmp:size=64m,mode=1777" "--tmpfs=/var/run:size=16m,mode=755" ]; - }; - - systemd.services."docker-openresty" = { - wants = [ "lab-gateway-create-network.service" ]; - after = [ "lab-gateway-create-network.service" ]; - }; - }; -} diff --git a/nix/components/ops-worker.nix b/nix/components/ops-worker.nix deleted file mode 100644 index 4cff10c..0000000 --- a/nix/components/ops-worker.nix +++ /dev/null @@ -1,40 +0,0 @@ -{ config, lib, ... }: - -let - cfg = config.services.lab-gateway-components; - inherit (lib) mkIf optionalAttrs optionals; - gatewayEnvFiles = optionals (cfg.envFile != null) [ cfg.envFile ]; - netOpt = "--network=${cfg.networkName}"; -in -{ - config = mkIf cfg.enable { - virtualisation.oci-containers.containers."ops-worker" = { - image = cfg.opsWorkerImageName; - imageFile = cfg.opsWorkerImageFile; - dependsOn = [ "mysql" ]; - environmentFiles = gatewayEnvFiles; - environment = { - OPS_BIND = "0.0.0.0"; - OPS_PORT = "8081"; - OPS_CONFIG = "/app/hosts.json"; - OPS_POLL_ENABLED = "true"; - OPS_POLL_INTERVAL = "60"; - OPS_RESERVATION_AUTOMATION = "true"; - OPS_RESERVATION_SCAN_INTERVAL = "30"; - OPS_RESERVATION_START_LEAD = "120"; - OPS_RESERVATION_END_DELAY = "60"; - } // optionalAttrs (cfg.opsMysqlDsn != null) { - MYSQL_DSN = cfg.opsMysqlDsn; - }; - volumes = [ - "${cfg.opsConfigPath}:/app/hosts.json:ro" - ]; - extraOptions = [ netOpt "--read-only" "--tmpfs=/tmp:size=32m,mode=1777" ]; - }; - - systemd.services."docker-ops-worker" = { - wants = [ "lab-gateway-create-network.service" ]; - after = [ "lab-gateway-create-network.service" ]; - }; - }; -} diff --git a/nix/images/gateway-bundle-image.nix b/nix/images/gateway-bundle-image.nix deleted file mode 100644 index 8464fd9..0000000 --- a/nix/images/gateway-bundle-image.nix +++ /dev/null @@ -1,63 +0,0 @@ -{ dockerTools -, stdenvNoCC -, lib -, bash -, coreutils -, docker -, docker-compose -, callPackage -}: - -let - gatewayCli = callPackage ../lab-gateway-docker.nix { }; - - bundleSource = lib.cleanSourceWith { - src = ../../.; - filter = path: type: - let - rel = lib.removePrefix (toString ../../.) (toString path); - in - !(lib.hasInfix "/.git" rel || lib.hasInfix "/dist" rel || lib.hasInfix "/result" rel); - }; - - bundleFiles = stdenvNoCC.mkDerivation { - pname = "lab-gateway-bundle-files"; - version = "1.0"; - src = bundleSource; - dontConfigure = true; - dontBuild = true; - installPhase = '' - mkdir -p $out/opt/lab-gateway - cp -r . $out/opt/lab-gateway/ - ''; - }; -in -dockerTools.buildLayeredImage { - name = "lab-gateway-bundle"; - tag = "nix"; - - contents = [ - bash - coreutils - docker - docker-compose - gatewayCli - bundleFiles - ]; - - extraCommands = '' - mkdir -p usr/local/bin - cat > usr/local/bin/lab-gateway-bundle <<'EOF' - #!/bin/sh - set -eu - project_dir="${LAB_GATEWAY_PROJECT_DIR:-/opt/lab-gateway}" - exec ${gatewayCli}/bin/lab-gateway --project-dir "$project_dir" "$@" - EOF - chmod +x usr/local/bin/lab-gateway-bundle - ''; - - config = { - WorkingDir = "/opt/lab-gateway"; - Entrypoint = [ "/usr/local/bin/lab-gateway-bundle" ]; - }; -} diff --git a/nix/images/openresty-image.nix b/nix/images/openresty-image.nix deleted file mode 100644 index b4eb189..0000000 --- a/nix/images/openresty-image.nix +++ /dev/null @@ -1,113 +0,0 @@ -{ dockerTools -, fetchFromGitHub -, bash -, coreutils -, curl -, findutils -, gawk -, gnugrep -, gnused -, nginx -, openresty -, openssl -, certbot -}: - -let - luaRestyHttp = fetchFromGitHub { - owner = "ledgetech"; - repo = "lua-resty-http"; - rev = "v0.17.2"; - hash = "sha256-PaGMqFgiQ+/ygwJZHjZlHcf6sEbnczaqSm+nGLzM5KI="; - }; - - luaRestyJwt = fetchFromGitHub { - owner = "SkyLothar"; - repo = "lua-resty-jwt"; - rev = "v0.1.11"; - hash = "sha256-58KwuO3xTu11ac1WhPwAxvl6ir9tbA5GLNSbSyZuM5A="; - }; - - luaRestyOpenssl = fetchFromGitHub { - owner = "fffonion"; - repo = "lua-resty-openssl"; - rev = "1.6.4"; - hash = "sha256-UfYnv/bjmhoH7KyS3tDu/H2mbxuX20DkiGELDTA55fs="; - }; - - luaRestyMysql = fetchFromGitHub { - owner = "openresty"; - repo = "lua-resty-mysql"; - rev = "v0.27"; - hash = "sha256-7ort7///WpJMEhcsoCep2xX7ADTTi3GZF5XenNwLyXU="; - }; - - luaRestyString = fetchFromGitHub { - owner = "openresty"; - repo = "lua-resty-string"; - rev = "v0.16"; - hash = "sha256-d/AGqX/Uo75Kgtzy1fFILjmbcPs1RUxbWtS5f/Hd7Q0="; - }; -in -dockerTools.buildLayeredImage { - name = "lab-gateway-openresty"; - tag = "nix"; - - contents = [ - bash - coreutils - curl - findutils - gawk - gnugrep - gnused - nginx - openresty - openssl - certbot - ]; - - extraCommands = '' - mkdir -p usr/local/bin - mkdir -p usr/local/openresty/bin - mkdir -p usr/local/openresty/nginx/conf - mkdir -p usr/local/openresty/site/lualib/resty - mkdir -p etc/openresty - mkdir -p etc/ssl/private - mkdir -p var/www/html - mkdir -p var/www/certbot - - cat > usr/local/openresty/bin/openresty <<'EOF' - #!/bin/sh - exec ${openresty}/bin/openresty -p /usr/local/openresty/nginx/ -c conf/nginx.conf "$@" - EOF - chmod +x usr/local/openresty/bin/openresty - - cp ${nginx}/conf/mime.types usr/local/openresty/nginx/conf/mime.types - cp ${../../openresty/nginx.conf} usr/local/openresty/nginx/conf/nginx.conf - cp ${../../openresty/lab_access.conf} etc/openresty/lab_access.conf - cp ${../../openresty/init-ssl.sh} usr/local/bin/init-ssl.sh - chmod +x usr/local/bin/init-ssl.sh - - cp -r ${../../openresty/lua} etc/openresty/lua - cp -r ${../../web}/. var/www/html/ - - cp -r ${luaRestyHttp}/lib/resty/. usr/local/openresty/site/lualib/resty/ - cp -r ${luaRestyJwt}/lib/resty/. usr/local/openresty/site/lualib/resty/ - cp -r ${luaRestyMysql}/lib/resty/. usr/local/openresty/site/lualib/resty/ - cp -r ${luaRestyString}/lib/resty/. usr/local/openresty/site/lualib/resty/ - cp -r ${luaRestyOpenssl}/lib/resty/. usr/local/openresty/site/lualib/resty/ - ''; - - config = { - WorkingDir = "/"; - Env = [ - "PATH=/usr/local/openresty/bin:/usr/local/bin:/bin" - ]; - ExposedPorts = { - "80/tcp" = { }; - "443/tcp" = { }; - }; - Cmd = [ "/usr/local/bin/init-ssl.sh" ]; - }; -} diff --git a/nix/images/ops-worker-image.nix b/nix/images/ops-worker-image.nix deleted file mode 100644 index 32def0f..0000000 --- a/nix/images/ops-worker-image.nix +++ /dev/null @@ -1,56 +0,0 @@ -{ dockerTools -, python3 -, bash -, coreutils -, iputils -, cacert -}: - -let - pythonEnv = python3.withPackages (ps: with ps; [ - flask - pywinrm - requests - requests-ntlm - wakeonlan - apscheduler - sqlalchemy - pymysql - cryptography - pytz - tzlocal - urllib3 - xmltodict - ntlm-auth - ]); -in -dockerTools.buildLayeredImage { - name = "lab-gateway-ops-worker"; - tag = "nix"; - - contents = [ - pythonEnv - bash - coreutils - iputils - cacert - ]; - - extraCommands = '' - mkdir -p app - cp ${../../ops-worker/worker.py} app/worker.py - ''; - - config = { - WorkingDir = "/app"; - Env = [ - "PYTHONDONTWRITEBYTECODE=1" - "PYTHONUNBUFFERED=1" - "OPS_BIND=0.0.0.0" - "OPS_PORT=8081" - "OPS_CONFIG=/app/hosts.json" - "SSL_CERT_FILE=${cacert}/etc/ssl/certs/ca-bundle.crt" - ]; - Cmd = [ "${pythonEnv}/bin/python" "/app/worker.py" ]; - }; -} diff --git a/nix/nixos-components-module.nix b/nix/nixos-components-module.nix deleted file mode 100644 index 174dc17..0000000 --- a/nix/nixos-components-module.nix +++ /dev/null @@ -1,160 +0,0 @@ -{ config, lib, pkgs, ... }: - -let - cfg = config.services.lab-gateway-components; - inherit (lib) mkEnableOption mkIf mkOption types; -in -{ - imports = [ - ./components/mysql.nix - ./components/guacd.nix - ./components/guacamole.nix - ./components/blockchain-services.nix - ./components/ops-worker.nix - ./components/openresty.nix - ]; - - options.services.lab-gateway-components = { - enable = mkEnableOption "DecentraLabs Gateway (componentized OCI containers)"; - - projectDir = mkOption { - type = types.str; - default = "/srv/lab-gateway"; - description = "Project directory where gateway files are located."; - }; - - envFile = mkOption { - type = types.nullOr types.str; - default = "/srv/lab-gateway/.env"; - description = "Main gateway .env file."; - }; - - blockchainEnvFile = mkOption { - type = types.nullOr types.str; - default = "/srv/lab-gateway/blockchain-services/.env"; - description = "Blockchain-specific .env file."; - }; - - networkName = mkOption { - type = types.str; - default = "guacnet"; - description = "Docker network used by gateway containers."; - }; - - buildLocalImages = mkOption { - type = types.bool; - default = true; - description = '' - Build Guacamole and blockchain-services images from local Dockerfiles - before starting component containers. - ''; - }; - - guacamoleImage = mkOption { - type = types.str; - default = "lab-gateway/guacamole:local"; - description = "Docker image tag for Guacamole web."; - }; - - blockchainImage = mkOption { - type = types.str; - default = "lab-gateway/blockchain-services:local"; - description = "Docker image tag for blockchain-services."; - }; - - openrestyImage = mkOption { - type = types.str; - default = "lab-gateway-openresty:nix"; - description = "Docker image tag for OpenResty."; - }; - - openrestyImageFile = mkOption { - type = types.package; - default = pkgs.callPackage ./images/openresty-image.nix { }; - description = "Nix-built OCI image tarball for OpenResty."; - }; - - opsWorkerImageName = mkOption { - type = types.str; - default = "lab-gateway-ops-worker:nix"; - description = "Docker tag for the Nix-built ops-worker image."; - }; - - opsWorkerImageFile = mkOption { - type = types.package; - default = pkgs.callPackage ./images/ops-worker-image.nix { }; - description = "Nix-built OCI image tarball for ops-worker."; - }; - - openrestyBindAddress = mkOption { - type = types.str; - default = "127.0.0.1"; - description = "Host bind address for OpenResty ports."; - }; - - openrestyHttpsPort = mkOption { - type = types.int; - default = 443; - description = "Host HTTPS port exposed by OpenResty."; - }; - - openrestyHttpPort = mkOption { - type = types.int; - default = 80; - description = "Host HTTP port exposed by OpenResty."; - }; - - opsConfigPath = mkOption { - type = types.str; - default = "/srv/lab-gateway/ops-worker/hosts.empty.json"; - description = "Path to ops-worker hosts.json source file."; - }; - - opsMysqlDsn = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - Optional DSN used by ops-worker for reservation automation. - Example: mysql+pymysql://user:password@mysql:3306/guacamole_db - ''; - }; - }; - - config = mkIf cfg.enable { - virtualisation.docker.enable = lib.mkDefault true; - virtualisation.oci-containers.backend = "docker"; - - systemd.services.lab-gateway-create-network = { - description = "Create Docker network for DecentraLabs Gateway"; - wantedBy = [ "multi-user.target" ]; - wants = [ "docker.service" ]; - after = [ "docker.service" ]; - path = [ pkgs.docker ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - script = '' - if ! docker network inspect ${lib.escapeShellArg cfg.networkName} >/dev/null 2>&1; then - docker network create ${lib.escapeShellArg cfg.networkName} - fi - ''; - }; - - systemd.services.lab-gateway-build-images = mkIf cfg.buildLocalImages { - description = "Build gateway component images from local Dockerfiles"; - wantedBy = [ "multi-user.target" ]; - wants = [ "docker.service" "network-online.target" ]; - after = [ "docker.service" "network-online.target" ]; - path = [ pkgs.docker ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - script = '' - docker build -t ${lib.escapeShellArg cfg.guacamoleImage} ${lib.escapeShellArg "${cfg.projectDir}/guacamole"} - docker build -t ${lib.escapeShellArg cfg.blockchainImage} ${lib.escapeShellArg "${cfg.projectDir}/blockchain-services"} - ''; - }; - }; -} From 2071d11181e244d552b6b719a50d196da95920d5 Mon Sep 17 00:00:00 2001 From: Ravenink Date: Sat, 14 Feb 2026 13:07:57 +0100 Subject: [PATCH 08/16] Updates ref --- blockchain-services | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blockchain-services b/blockchain-services index bd2d5b9..b5142ff 160000 --- a/blockchain-services +++ b/blockchain-services @@ -1 +1 @@ -Subproject commit bd2d5b9e5566f359c38ae0816529943076089378 +Subproject commit b5142ff6710a31c19eed2726955c222154b522b5 From 5070b78c8af5c48c18786ead286fcf2f637734d2 Mon Sep 17 00:00:00 2001 From: Ravenink Date: Sat, 14 Feb 2026 15:09:25 +0100 Subject: [PATCH 09/16] Renamed access tokens to unify naming. ops_token and lab_manager_token have been combined into just one. Updated docs --- .env.example | 17 +- LOGGING.md | 152 ++++----------- README.md | 126 ++----------- blockchain-services | 2 +- docker-compose.yml | 15 +- docs/DOCUMENTATION_AUDIT.md | 66 +++++++ docs/install/GUIA_INSTALACION_ES.md | 5 +- docs/install/INSTALLATION_GUIDE_EN.md | 5 +- docs/tutorials/PROVIDER_TUTORIAL_EN.md | 4 +- docs/tutorials/TUTORIAL_PROVEEDOR_ES.md | 4 +- openresty/init-ssl.sh | 2 +- openresty/lab_access.conf | 44 ++--- openresty/lua/admin_access.lua | 37 ++-- openresty/lua/gateway_health.lua | 2 +- openresty/lua/init.lua | 8 +- ...nternal_access.lua => treasury_access.lua} | 16 +- openresty/nginx.conf | 7 +- openresty/tests/README.md | 175 ++++-------------- openresty/tests/run.lua | 3 +- openresty/tests/unit/admin_access_spec.lua | 161 ++++++++++++++++ ...cess_spec.lua => treasury_access_spec.lua} | 42 ++--- ops-worker/README.md | 136 ++++++-------- setup.bat | 69 ++----- setup.sh | 55 ++---- tests/integration/README.md | 132 ++++--------- .../docker-compose.integration.yml | 2 +- tests/integration/run-integration.sh | 2 +- tests/smoke/docker-compose.smoke.yml | 4 +- tests/smoke/run-smoke.sh | 2 +- web/assets/js/auth-token-fetch.js | 25 ++- web/assets/js/auth-token-handler.js | 63 ++++--- web/assets/js/lab-manager.js | 75 +++++++- web/lab-manager/index.html | 21 ++- 33 files changed, 683 insertions(+), 796 deletions(-) create mode 100644 docs/DOCUMENTATION_AUDIT.md rename openresty/lua/{internal_access.lua => treasury_access.lua} (86%) create mode 100644 openresty/tests/unit/admin_access_spec.lua rename openresty/tests/unit/{internal_access_spec.lua => treasury_access_spec.lua} (77%) diff --git a/.env.example b/.env.example index 6539a1a..87b4780 100644 --- a/.env.example +++ b/.env.example @@ -64,10 +64,8 @@ CLOUDFLARE_TUNNEL_TOKEN= # SECRETS AND KEYS # ============================================================================= -# OPS worker -# Set to empty to disable /ops endpoints. -OPS_SECRET=CHANGE_ME -# Lab Manager access token (separate from SECURITY_ACCESS_TOKEN). +# Lab Manager access token (used by /lab-manager and /ops). +# Set to empty to disable /ops endpoints (and keep /lab-manager private-network-only). LAB_MANAGER_TOKEN=CHANGE_ME LAB_MANAGER_TOKEN_HEADER=X-Lab-Manager-Token LAB_MANAGER_TOKEN_COOKIE=lab_manager_token @@ -83,11 +81,12 @@ ETHEREUM_SEPOLIA_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com,https://0xr # BLOCKCHAIN-SERVICES REMOTE ACCESS # ============================================================================= -# Access token for /wallet, /treasury, /wallet-dashboard (sent by OpenResty). -SECURITY_ACCESS_TOKEN=CHANGE_ME -SECURITY_ACCESS_TOKEN_HEADER=X-Access-Token -SECURITY_ACCESS_TOKEN_COOKIE=access_token -SECURITY_ACCESS_TOKEN_REQUIRED=true +# Wallet/Treasury access token +# Used by /wallet, /treasury, /wallet-dashboard and /treasury/admin/** (sent by OpenResty). +TREASURY_TOKEN=CHANGE_ME +TREASURY_TOKEN_HEADER=X-Access-Token +TREASURY_TOKEN_COOKIE=access_token +TREASURY_TOKEN_REQUIRED=true SECURITY_ALLOW_PRIVATE_NETWORKS=true ADMIN_DASHBOARD_ALLOW_PRIVATE=true diff --git a/LOGGING.md b/LOGGING.md index 2374667..664cc1d 100644 --- a/LOGGING.md +++ b/LOGGING.md @@ -1,142 +1,68 @@ -# Logging Configuration +# Logging Guide -## ๐Ÿ“‹ Log Configuration Summary +This project uses Docker `json-file` logging with per-service rotation in `docker-compose.yml`. -| Service | Max Size | Max Files | Total Storage | Description | -| ------------- | -------- | --------- | ------------- | ------------------------------- | -| **MySQL** | 10MB | 3 | \~30MB | Database logs and queries | -| **Guacd** | 5MB | 3 | \~15MB | Protocol daemon logs | -| **Guacamole** | 20MB | 5 | \~100MB | Application logs (most verbose) | -| **OpenResty** | 10MB | 5 | \~50MB | Access logs and proxy logs | +## Rotation limits -**Total Maximum Log Storage**: \~195MB +| Service | max-size | max-file | Approx max | +| --- | --- | --- | --- | +| `blockchain-services` | `20m` | `5` | ~100 MB | +| `openresty` | `10m` | `5` | ~50 MB | +| `mysql` | `10m` | `3` | ~30 MB | +| `guacamole` | `20m` | `5` | ~100 MB | +| `guacd` | `5m` | `3` | ~15 MB | +| `ops-worker` | `10m` | `3` | ~30 MB | -## ๐Ÿ” Useful Logging Commands +Estimated capped total (configured services): ~325 MB. -### View Live Logs +## Common commands ```bash -# All services -docker-compose logs -f +# All services (follow) +docker compose logs -f -# Specific service -docker-compose logs -f openresty -docker-compose logs -f guacamole -docker-compose logs -f mysql -docker-compose logs -f guacd +# One service +docker compose logs -f openresty -# Last N lines -docker-compose logs --tail=50 openresty -``` - -### Filter Logs by Time - -```bash -# Since timestamp -docker-compose logs mysql --since="2024-01-01T10:00:00" +# Last lines +docker compose logs --tail=100 guacamole -# Last 10 minutes -docker-compose logs guacamole --since="10m" - -# Last hour -docker-compose logs --since="1h" +# Time window +docker compose logs --since=10m ``` -### Search in Logs +## Search examples -```bash -# PowerShell - Search for errors -docker-compose logs | Select-String -Pattern "error|failed|exception" -CaseSensitive:$false +PowerShell: -# PowerShell - Search for specific patterns -docker-compose logs openresty | Select-String -Pattern "JWT|auth|token" -docker-compose logs mysql | Select-String -Pattern "connection|query" +```powershell +docker compose logs | Select-String -Pattern "error|failed|exception" -CaseSensitive:$false +docker compose logs openresty | Select-String -Pattern "jwt|token|auth" -CaseSensitive:$false ``` -### Log File Locations - -Log files are stored in Docker's default location: - -* **Windows**: `C:\ProgramData\docker\containers\\-json.log` -* **Linux**: `/var/lib/docker/containers//-json.log` - -### Export Logs +Bash: ```bash -# Export all logs to file -docker-compose logs > gateway-logs-$(Get-Date -Format "yyyy-MM-dd").log - -# Export specific service logs -docker-compose logs openresty > openresty-logs-$(Get-Date -Format "yyyy-MM-dd").log +docker compose logs | grep -Ei "error|failed|exception" +docker compose logs openresty | grep -Ei "jwt|token|auth" ``` -## โš ๏ธ Log Rotation - -The logging configuration automatically rotates logs when: - -* File size exceeds the `max-size` limit -* Number of files exceeds `max-file` limit - -Oldest logs are automatically deleted to maintain storage limits. - -## ๐Ÿ”ง Advanced Logging Options - -### Enable Debug Logging (Development) +## Export logs -Add to specific service in docker-compose.yml: +PowerShell: -```yaml -environment: - - LOG_LEVEL=DEBUG +```powershell +docker compose logs > gateway-logs-$(Get-Date -Format "yyyy-MM-dd").log ``` -### Send Logs to External System - -For production, consider: - -* **Fluentd**: For centralized logging -* **ELK Stack**: Elasticsearch, Logstash, Kibana -* **Splunk**: Enterprise logging solution - -Example with Fluentd: - -```yaml -logging: - driver: "fluentd" - options: - fluentd-address: "localhost:24224" - tag: "gateway.{{.Name}}" -``` - -## ๐Ÿšจ Log Monitoring - -### Critical Patterns to Monitor - -* `ERROR`, `FATAL`, `CRITICAL` -* `Authentication failed` -* `Connection refused` -* `Out of memory` -* `Database connection lost` -* `SSL/TLS errors` - -### Health Check via Logs +Bash: ```bash -# Check for recent errors (last 5 minutes) -docker-compose logs --since="5m" | Select-String -Pattern "error|failed|fatal" -CaseSensitive:$false +docker compose logs > gateway-logs-$(date +%F).log ``` -## ๐Ÿ“ˆ Log Analysis +## Notes -### Common Log Queries - -```bash -# Count error occurrences -docker-compose logs | Select-String -Pattern "error" -CaseSensitive:$false | Measure-Object - -# Find authentication attempts -docker-compose logs openresty | Select-String -Pattern "JWT|auth" -CaseSensitive:$false - -# Monitor MySQL performance -docker-compose logs mysql | Select-String -Pattern "slow query|performance|timeout" -``` +- Rotation deletes older files automatically after the limits above. +- Host log file locations are Docker defaults (`/var/lib/docker/containers/...` on Linux). +- For production centralization, use a logging driver (for example `fluentd`) in compose. diff --git a/README.md b/README.md index cf3b15c..5bd7234 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,9 @@ If you prefer manual configuration: ``` 2. **Edit `.env` and `blockchain-services/.env`** with your configuration (see Configuration section below) - - Make sure you set `OPS_SECRET`, `SECURITY_ACCESS_TOKEN`, and `LAB_MANAGER_TOKEN` for production if needed. `OPS_SECRET` protects `/ops`, `SECURITY_ACCESS_TOKEN` protects `/wallet`, `/treasury`, and `/wallet-dashboard`, and `LAB_MANAGER_TOKEN` protects `/lab-manager` when accessed from public networks. + - Configure the two gateway access tokens for production: + - `TREASURY_TOKEN`: protects wallet/treasury routes (`/wallet`, `/treasury`, `/wallet-dashboard`, `/treasury/admin/**`) + - `LAB_MANAGER_TOKEN`: protects `/lab-manager` and `/ops` from public networks 3. **Set host UID/GID for bind mounts (Linux/macOS)** so containers can write to `certs/` and `blockchain-data/`: ```bash @@ -196,9 +198,12 @@ If you prefer manual configuration: certs/ โ”œโ”€โ”€ fullchain.pem # SSL certificate chain โ”œโ”€โ”€ privkey.pem # SSL private key - โ””โ”€โ”€ public_key.pem # JWT public key (from auth provider) + โ””โ”€โ”€ public_key.pem # JWT public key (optional if blockchain-services generates it) ``` + `public_key.pem` is generated automatically by `blockchain-services` on first start + when missing. You only need to provide it manually if you use an external auth signer. + **Database schema:** When `blockchain-services` has a MySQL datasource configured, it runs Flyway migrations on startup to create the auth, WebAuthn, and intents tables automatically. @@ -255,17 +260,16 @@ CORS_ALLOWED_ORIGINS=https://your-frontend.com,https://marketplace.com # Wallet/Treasury CORS allowlist (blockchain-services) WALLET_ALLOWED_ORIGINS=https://your-domain -# Ops Worker -OPS_SECRET=your_ops_secret +# Lab Manager + Ops Worker LAB_MANAGER_TOKEN=your_lab_manager_token LAB_MANAGER_TOKEN_HEADER=X-Lab-Manager-Token LAB_MANAGER_TOKEN_COOKIE=lab_manager_token # Blockchain Services remote access -SECURITY_ACCESS_TOKEN=your_access_token -SECURITY_ACCESS_TOKEN_HEADER=X-Access-Token -SECURITY_ACCESS_TOKEN_COOKIE=access_token -SECURITY_ACCESS_TOKEN_REQUIRED=true +TREASURY_TOKEN=your_treasury_token +TREASURY_TOKEN_HEADER=X-Access-Token +TREASURY_TOKEN_COOKIE=access_token +TREASURY_TOKEN_REQUIRED=true SECURITY_ALLOW_PRIVATE_NETWORKS=true ADMIN_DASHBOARD_ALLOW_PRIVATE=true @@ -281,7 +285,7 @@ CERTBOT_EMAIL=you@example.com CERTBOT_STAGING=0 ``` -Use a strong `GUAC_ADMIN_PASS`. Common defaults are rejected at startup to avoid insecure deployments. The same check applies to `MYSQL_ROOT_PASSWORD` and `MYSQL_PASSWORD` (defaults like `CHANGE_ME` will stop MySQL from initializing). Set a strong `OPS_SECRET` (or leave it empty to disable `/ops`). Set `SECURITY_ACCESS_TOKEN` to secure blockchain-services endpoints exposed through OpenResty for remote access. +Use a strong `GUAC_ADMIN_PASS`. Common defaults are rejected at startup to avoid insecure deployments. The same check applies to `MYSQL_ROOT_PASSWORD` and `MYSQL_PASSWORD` (defaults like `CHANGE_ME` will stop MySQL from initializing). Set a strong `LAB_MANAGER_TOKEN` (or leave it empty to keep `/ops` disabled and `/lab-manager` private-network-only). Set `TREASURY_TOKEN` to protect wallet/treasury endpoints exposed through OpenResty for remote access. `blockchain-services` uses a dedicated schema named `blockchain_services` by default. If you want a different name, set `BLOCKCHAIN_MYSQL_DATABASE` in `.env`. @@ -335,11 +339,12 @@ MARKETPLACE_PUBLIC_KEY_URL=https://marketplace.com/.well-known/public-key.pem #### Access Controls (Important) -- `/wallet-dashboard`, `/wallet`, `/treasury`: require `SECURITY_ACCESS_TOKEN` for non-private clients. If the token is unset, access is limited to loopback/Docker networks. The token is provided automatically via the authentication modal on the gateway's homepage, which stores it locally and adds it as the `X-Access-Token` header on all requests. -- `/treasury/admin/**`: always requires `SECURITY_ACCESS_TOKEN` (no private-network bypass). `/treasury/admin/execute` additionally requires an EIP-712 signature from the institutional wallet, including a fresh timestamp. -- **Initial setup**: Click "Wallet & Treasuryโ†’" from the homepage, enter your `SECURITY_ACCESS_TOKEN` when prompted. The token will be stored in your browser and automatically included in all requests. +- `/wallet-dashboard`, `/wallet`, `/treasury`: require `TREASURY_TOKEN` for non-private clients. If the token is unset, access is limited to loopback/Docker networks. The token is provided automatically via the authentication modal on the gateway's homepage, which stores it locally and adds it as the `X-Access-Token` header on all requests. +- `/treasury/admin/**`: uses `TREASURY_TOKEN` only (header/cookie/query parameter). If `TREASURY_TOKEN` is unset, access is limited to loopback/Docker ranges. +- `/treasury/admin/execute`: additionally requires an EIP-712 signature from the institutional wallet, including a fresh timestamp. +- **Initial setup**: Click "Wallet & Treasuryโ†’" from the homepage, enter your `TREASURY_TOKEN` when prompted. The token will be stored in your browser and automatically included in all requests. - `/lab-manager`: allows private networks by default; requires `LAB_MANAGER_TOKEN` for non-private clients. Click "Lab Managerโ†’" from the homepage and enter your token when prompted. -- `/ops`: restricted to private networks and also requires `OPS_SECRET` via `X-Ops-Token` or `ops_token` cookie. +- `/ops`: **network-restricted** to `127.0.0.1` and `172.16.0.0/12` only, plus requires `LAB_MANAGER_TOKEN`. Lab Manager UI works remotely, but ops features (WoL, WinRM, heartbeat) require access from the gateway server or institution network. - If wallet actions return `JSON.parse` errors in the browser, ensure both `CORS_ALLOWED_ORIGINS` and `WALLET_ALLOWED_ORIGINS` include your gateway origin. ## Institutional Wallet Setup @@ -488,81 +493,6 @@ lab-gateway/ `certs/` and `blockchain-data/` are runtime directories and may not exist until first setup. `blockchain-services/` is a Git submodule and must be initialized/updated before running the stack. -## ๐Ÿงช Testing - -### Gateway Tests - -Unit tests cover the OpenResty gateway logic (Lua handlers and session guard). They run via the OpenResty container so you do not need a local Lua installation: - -```bash -# Windows (PowerShell) -docker run --rm -v "${PWD}:/workspace" -w /workspace openresty/openresty:alpine-fat luajit openresty/tests/run.lua - -# Linux/macOS -docker run --rm -v "$(pwd):/workspace" -w /workspace openresty/openresty:alpine-fat luajit openresty/tests/run.lua -``` - -The command executes every spec under `openresty/tests/unit/` through a lightweight Lua test runner. - -### Smoke Tests - -For an end-to-end smoke check (OpenResty โ†” Guacamole proxy logic): - -```bash -cd tests/smoke -./run-smoke.sh -``` - -The script spins up a miniature docker-compose environment with mock services, validates that JWT cookies are issued, and ensures Guacamole receives the propagated `Authorization` header. - -### Coverage Reports - -To collect LuaCov coverage metrics: - -```bash -# Windows (PowerShell) -docker run --rm -v "${PWD}:/workspace" -w /workspace openresty/openresty:alpine-fat sh -c "luarocks install luacov >/dev/null && luajit -lluacov openresty/tests/run.lua && luacov" - -# Linux/macOS -docker run --rm -v "$(pwd):/workspace" -w /workspace openresty/openresty:alpine-fat sh -c "luarocks install luacov >/dev/null && luajit -lluacov openresty/tests/run.lua && luacov" -``` - -Coverage data will be written to `luacov.report.out` and `luacov.stats.out`. - -## ๐Ÿ› ๏ธ Development - -### Local Development Setup - -1. **Start services in development mode:** - ```bash - docker compose up -d - ``` - -2. **Access services:** - - Blockchain Services: http://localhost:8080/wallet (or configured port) - - Guacamole: https://localhost:8443/guacamole - - MySQL: localhost:3306 - -### Debugging - -Enable debug logging in `.env` or `blockchain-services/.env`: -```env -LOG_LEVEL_AUTH=DEBUG -LOG_LEVEL_SECURITY=DEBUG -LOG_LEVEL_WEB=DEBUG -``` - -View logs: -```bash -# All services -docker compose logs -f - -# Specific service -docker compose logs -f openresty -docker compose logs -f blockchain-services -docker compose logs -f guacamole -``` - ## ๐Ÿค Contributing 1. **Fork** the project @@ -570,23 +500,3 @@ docker compose logs -f guacamole 3. **Commit** your changes (`git commit -m 'Add amazing feature'`) 4. **Push** to the branch (`git push origin feature/amazing-feature`) 5. **Open** a Pull Request - -## ๐Ÿ“ Documentation - -- **Main Documentation**: This README (for main branch - full version) -- **Installation Guides**: [docs/install/INSTALLATION_GUIDE_EN.md](docs/install/INSTALLATION_GUIDE_EN.md) and [docs/install/GUIA_INSTALACION_ES.md](docs/install/GUIA_INSTALACION_ES.md) -- **eduGAIN Technical Guide**: [docs/edugain/EDUGAIN_INTEGRATION_EN.md](docs/edugain/EDUGAIN_INTEGRATION_EN.md) and [docs/edugain/INTEGRACION_EDUGAIN_ES.md](docs/edugain/INTEGRACION_EDUGAIN_ES.md) -- **Provider Tutorials**: [docs/tutorials/PROVIDER_TUTORIAL_EN.md](docs/tutorials/PROVIDER_TUTORIAL_EN.md) and [docs/tutorials/TUTORIAL_PROVEEDOR_ES.md](docs/tutorials/TUTORIAL_PROVEEDOR_ES.md) -- **Logging**: [LOGGING.md](LOGGING.md) - Log configuration and management -- **Guacamole Setup**: [configuring-lab-connections/guacamole-connections.md](configuring-lab-connections/guacamole-connections.md) -- **Blockchain Services**: Check [blockchain-services/README.md](blockchain-services/README.md) for detailed API documentation - -## ๐Ÿ“ž Support - -* **Issues**: [GitHub Issues](https://github.com/DecentraLabsCom/lite-lab-gateway/issues) -* **Logs**: Use `docker compose logs [service]` for troubleshooting -* **Configuration**: Review `.env.example` and `blockchain-services/.env.example` for all options - ---- - -*DecentraLabs Gateway provides a complete, production-ready blockchain authentication system for decentralized laboratory access.* diff --git a/blockchain-services b/blockchain-services index b5142ff..9039142 160000 --- a/blockchain-services +++ b/blockchain-services @@ -1 +1 @@ -Subproject commit b5142ff6710a31c19eed2726955c222154b522b5 +Subproject commit 90391427525568911d261ab87ddce1400798b91a diff --git a/docker-compose.yml b/docker-compose.yml index 5dfd7c9..2b6c863 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,10 +17,10 @@ services: - SPRING_DATASOURCE_USERNAME=${MYSQL_USER} - SPRING_DATASOURCE_PASSWORD=${MYSQL_PASSWORD} - PROVIDER_CONFIG_PATH=/app/data/provider.properties - - SECURITY_ACCESS_TOKEN=${SECURITY_ACCESS_TOKEN} - - SECURITY_ACCESS_TOKEN_HEADER=${SECURITY_ACCESS_TOKEN_HEADER} - - SECURITY_ACCESS_TOKEN_COOKIE=${SECURITY_ACCESS_TOKEN_COOKIE} - - SECURITY_ACCESS_TOKEN_REQUIRED=${SECURITY_ACCESS_TOKEN_REQUIRED} + - TREASURY_TOKEN=${TREASURY_TOKEN} + - TREASURY_TOKEN_HEADER=${TREASURY_TOKEN_HEADER} + - TREASURY_TOKEN_COOKIE=${TREASURY_TOKEN_COOKIE} + - TREASURY_TOKEN_REQUIRED=${TREASURY_TOKEN_REQUIRED} - SECURITY_ALLOW_PRIVATE_NETWORKS=${SECURITY_ALLOW_PRIVATE_NETWORKS} - ADMIN_DASHBOARD_ALLOW_PRIVATE=${ADMIN_DASHBOARD_ALLOW_PRIVATE} volumes: @@ -82,15 +82,14 @@ services: - SERVER_NAME=${SERVER_NAME} - HTTPS_PORT=${HTTPS_PORT} - HTTP_PORT=${HTTP_PORT} - - OPS_SECRET=${OPS_SECRET} - MYSQL_DATABASE=${MYSQL_DATABASE} - MYSQL_USER=${MYSQL_USER} - MYSQL_PASSWORD=${MYSQL_PASSWORD} - CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS} - MARKETPLACE_URL=${MARKETPLACE_URL:-https://marketplace-decentralabs.vercel.app} - - SECURITY_ACCESS_TOKEN=${SECURITY_ACCESS_TOKEN} - - SECURITY_ACCESS_TOKEN_HEADER=${SECURITY_ACCESS_TOKEN_HEADER} - - SECURITY_ACCESS_TOKEN_COOKIE=${SECURITY_ACCESS_TOKEN_COOKIE} + - TREASURY_TOKEN=${TREASURY_TOKEN} + - TREASURY_TOKEN_HEADER=${TREASURY_TOKEN_HEADER} + - TREASURY_TOKEN_COOKIE=${TREASURY_TOKEN_COOKIE} - LAB_MANAGER_TOKEN=${LAB_MANAGER_TOKEN} - LAB_MANAGER_TOKEN_HEADER=${LAB_MANAGER_TOKEN_HEADER} - LAB_MANAGER_TOKEN_COOKIE=${LAB_MANAGER_TOKEN_COOKIE} diff --git a/docs/DOCUMENTATION_AUDIT.md b/docs/DOCUMENTATION_AUDIT.md new file mode 100644 index 0000000..a1d18d7 --- /dev/null +++ b/docs/DOCUMENTATION_AUDIT.md @@ -0,0 +1,66 @@ +# Documentation Audit (Lab Gateway) + +Audit date: 2026-02-14 +Scope: all project docs except `blockchain-services/*`. + +## Result + +Status: mostly aligned after this pass. + +Main fixes applied: + +1. `README.md` +- Corrected local access endpoints (everything enters through OpenResty by default). +- Clarified `public_key.pem` behavior (auto-generated by `blockchain-services` if missing). +- Corrected `/treasury/admin/**` access model (strict `TREASURY_TOKEN`). +- Corrected `/ops` network restriction (`127.0.0.1` and `172.16.0.0/12`). + +2. `LOGGING.md` +- Rewritten to match current `docker-compose.yml` logging config. +- Migrated commands to `docker compose`. +- Added `blockchain-services` and `ops-worker` rotation limits. + +3. `docs/install/INSTALLATION_GUIDE_EN.md` +- Post-install checks now explicitly support non-443 bind ports. + +4. `docs/install/GUIA_INSTALACION_ES.md` +- Same post-install validation fix as English guide. + +5. `ops-worker/README.md` +- Simplified and aligned with implemented API routes. +- Added missing endpoints (`/api/reservations/timeline`, `/api/hosts/reload`, `/api/hosts/quarantine`). + +6. `openresty/tests/README.md` +- Updated suite structure to include all active specs (`treasury_access`, `lab_manager_access`, `jwt_handler`). +- Simplified run instructions and coverage scope. + +7. `tests/integration/README.md` +- Rewritten for concise, command-focused usage aligned with `run-integration.sh` and compose paths. + +## Verified docs + +- `README.md` +- `SUMMARY.md` +- `LOGGING.md` +- `configuring-lab-connections/guacamole-connections.md` +- `certbot/README.md` +- `ops-worker/README.md` +- `docs/install/INSTALLATION_GUIDE_EN.md` +- `docs/install/GUIA_INSTALACION_ES.md` +- `docs/edugain/EDUGAIN_INTEGRATION_EN.md` +- `docs/edugain/INTEGRACION_EDUGAIN_ES.md` +- `docs/tutorials/PROVIDER_TUTORIAL_EN.md` +- `docs/tutorials/TUTORIAL_PROVEEDOR_ES.md` +- `openresty/tests/README.md` +- `tests/integration/README.md` + +## Remaining recommendation (optional) + +Create one short "Docs map" page that defines: + +- install path (`README.md` + `docs/install/*`) +- operations path (`LOGGING.md` + `ops-worker/README.md`) +- federation path (`docs/edugain/*`) +- provider path (`docs/tutorials/*` + `configuring-lab-connections/*`) + +This reduces repeated instructions and keeps one source of truth per topic. diff --git a/docs/install/GUIA_INSTALACION_ES.md b/docs/install/GUIA_INSTALACION_ES.md index a175b26..9e234d7 100644 --- a/docs/install/GUIA_INSTALACION_ES.md +++ b/docs/install/GUIA_INSTALACION_ES.md @@ -66,8 +66,9 @@ systemctl status lab-gateway.service ## 7. Validacion post-instalacion ```bash -curl -k https://127.0.0.1/health -curl -k https://127.0.0.1/gateway/health +HTTPS_BIND_PORT=443 # usa 8443 si elegiste modo localhost en setup.sh/setup.bat +curl -k "https://127.0.0.1:${HTTPS_BIND_PORT}/health" +curl -k "https://127.0.0.1:${HTTPS_BIND_PORT}/gateway/health" ``` Pruebas opcionales: diff --git a/docs/install/INSTALLATION_GUIDE_EN.md b/docs/install/INSTALLATION_GUIDE_EN.md index 63d8929..45cea8b 100644 --- a/docs/install/INSTALLATION_GUIDE_EN.md +++ b/docs/install/INSTALLATION_GUIDE_EN.md @@ -66,8 +66,9 @@ systemctl status lab-gateway.service ## 7. Post-install Validation ```bash -curl -k https://127.0.0.1/health -curl -k https://127.0.0.1/gateway/health +HTTPS_BIND_PORT=443 # use 8443 if you selected localhost mode in setup.sh/setup.bat +curl -k "https://127.0.0.1:${HTTPS_BIND_PORT}/health" +curl -k "https://127.0.0.1:${HTTPS_BIND_PORT}/gateway/health" ``` Optional tests: diff --git a/docs/tutorials/PROVIDER_TUTORIAL_EN.md b/docs/tutorials/PROVIDER_TUTORIAL_EN.md index c089d75..9e9a066 100644 --- a/docs/tutorials/PROVIDER_TUTORIAL_EN.md +++ b/docs/tutorials/PROVIDER_TUTORIAL_EN.md @@ -6,7 +6,9 @@ This tutorial explains how a lab provider can publish and operate a remote lab w - Gateway deployed and healthy (`/health` and `/gateway/health`). - Access to Guacamole admin credentials. -- Valid token for protected routes (`SECURITY_ACCESS_TOKEN` and optional `LAB_MANAGER_TOKEN`). +- Access tokens configured per area: + - `TREASURY_TOKEN` for wallet/treasury routes. + - `LAB_MANAGER_TOKEN` for `/lab-manager` and `/ops`. - Lab station host data configured for ops-worker if remote power/session control is required. ## 2. Configure Guacamole Connections diff --git a/docs/tutorials/TUTORIAL_PROVEEDOR_ES.md b/docs/tutorials/TUTORIAL_PROVEEDOR_ES.md index 0516501..f1937b3 100644 --- a/docs/tutorials/TUTORIAL_PROVEEDOR_ES.md +++ b/docs/tutorials/TUTORIAL_PROVEEDOR_ES.md @@ -6,7 +6,9 @@ Este tutorial explica como publicar y operar un laboratorio remoto con DecentraL - Gateway desplegado y saludable (`/health` y `/gateway/health`). - Acceso a credenciales admin de Guacamole. -- Token valido para rutas protegidas (`SECURITY_ACCESS_TOKEN` y opcional `LAB_MANAGER_TOKEN`). +- Tokens de acceso configurados por area: + - `TREASURY_TOKEN` para rutas de wallet/treasury. + - `LAB_MANAGER_TOKEN` para `/lab-manager` y `/ops`. - Inventario de hosts configurado en ops-worker si se requiere control remoto de energia/sesion. ## 2. Configurar conexiones Guacamole diff --git a/openresty/init-ssl.sh b/openresty/init-ssl.sh index 887053e..01dfac9 100644 --- a/openresty/init-ssl.sh +++ b/openresty/init-ssl.sh @@ -208,7 +208,7 @@ fi echo "=== Starting OpenResty ===" # Export environment variables that nginx needs to access -export OPS_SECRET="${OPS_SECRET:-}" +export LAB_MANAGER_TOKEN="${LAB_MANAGER_TOKEN:-}" # Background watcher: reload OpenResty if cert/key change on disk watch_certs() { diff --git a/openresty/lab_access.conf b/openresty/lab_access.conf index d202a50..57feac1 100644 --- a/openresty/lab_access.conf +++ b/openresty/lab_access.conf @@ -139,12 +139,8 @@ server { try_files $uri $uri/ =404; } - # Ensure /lab-manager sets cookie even without trailing slash + # Ensure /lab-manager works even without trailing slash location = /lab-manager { - set_by_lua_block $ops_secret { return os.getenv("OPS_SECRET") or "" } - if ($ops_secret != "") { - add_header Set-Cookie "ops_token=$ops_secret; Path=/ops/; HttpOnly; Secure; SameSite=Lax"; - } content_by_lua_block { local token = ngx.var.arg_token if token and token ~= "" then @@ -169,13 +165,9 @@ server { } } - # Set ops_token cookie for Lab Manager (admin UI). Uses OPS_SECRET; scoped to /ops/. + # Lab Manager admin UI location /lab-manager/ { access_by_lua_file /etc/openresty/lua/lab_manager_access.lua; - set_by_lua_block $ops_secret { return os.getenv("OPS_SECRET") or "" } - if ($ops_secret != "") { - add_header Set-Cookie "ops_token=$ops_secret; Path=/ops/; HttpOnly; Secure; SameSite=Lax"; - } root /var/www/html; index index.html; try_files $uri $uri/ /lab-manager/index.html; @@ -278,9 +270,9 @@ server { content_by_lua_block { local token = ngx.var.arg_token if token and token ~= "" then - local expected = os.getenv("SECURITY_ACCESS_TOKEN") or "" + local expected = os.getenv("TREASURY_TOKEN") or "" if expected ~= "" and token == expected then - local cookie_name = os.getenv("SECURITY_ACCESS_TOKEN_COOKIE") or "access_token" + local cookie_name = os.getenv("TREASURY_TOKEN_COOKIE") or "access_token" ngx.header["Set-Cookie"] = cookie_name .. "=" .. token .. "; Path=/; HttpOnly; Secure; SameSite=Lax" return ngx.redirect("/wallet-dashboard/", 302) end @@ -288,9 +280,9 @@ server { ngx.status = ngx.HTTP_UNAUTHORIZED ngx.header["Content-Type"] = "text/plain" if expected == "" then - ngx.say("Forbidden: Remote access is disabled. To enable external access, set SECURITY_ACCESS_TOKEN in your .env file and restart the service.") + ngx.say("Forbidden: Remote access is disabled. To enable external access, set TREASURY_TOKEN in your .env file and restart the service.") else - ngx.say("Unauthorized: Invalid access token. Use /wallet-dashboard?token=YOUR_ACCESS_TOKEN.") + ngx.say("Unauthorized: Invalid treasury token. Use /wallet-dashboard?token=YOUR_TREASURY_TOKEN.") end return ngx.exit(ngx.HTTP_UNAUTHORIZED) end @@ -299,7 +291,7 @@ server { } } location /wallet-dashboard/ { - access_by_lua_file /etc/openresty/lua/internal_access.lua; + access_by_lua_file /etc/openresty/lua/treasury_access.lua; # Proxy configuration for blockchain-services static dashboard proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -320,7 +312,7 @@ server { } location /institution-config/ { - access_by_lua_file /etc/openresty/lua/internal_access.lua; + access_by_lua_file /etc/openresty/lua/treasury_access.lua; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -356,7 +348,7 @@ server { } location /treasury/ { - access_by_lua_file /etc/openresty/lua/internal_access.lua; + access_by_lua_file /etc/openresty/lua/treasury_access.lua; # Enable CORS for API endpoints if ($cors_allow_origin = "DENY") { return 403; @@ -398,7 +390,7 @@ server { # Wallet API endpoints location /wallet/ { - access_by_lua_file /etc/openresty/lua/internal_access.lua; + access_by_lua_file /etc/openresty/lua/treasury_access.lua; # Enable CORS for API endpoints if ($cors_allow_origin = "DENY") { return 403; @@ -534,11 +526,11 @@ server { allow 127.0.0.1; allow 172.16.0.0/12; deny all; - # ACL: requires OPS_SECRET env and matching X-Ops-Token header OR ops_token cookie - set_by_lua_block $ops_expected { return os.getenv("OPS_SECRET") or "" } - set $ops_token $http_x_ops_token; - if ($ops_token = "") { - set $ops_token $cookie_ops_token; + # ACL: requires LAB_MANAGER_TOKEN env and matching X-Lab-Manager-Token header OR lab_manager_token cookie + set_by_lua_block $lab_manager_expected { return os.getenv("LAB_MANAGER_TOKEN") or "" } + set $lab_manager_token $http_x_lab_manager_token; + if ($lab_manager_token = "") { + set $lab_manager_token $cookie_lab_manager_token; } # CORS reducido al propio host; sin credenciales @@ -546,17 +538,17 @@ server { add_header 'Access-Control-Allow-Origin' $ops_origin always; add_header 'Access-Control-Allow-Credentials' 'false' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'Content-Type, X-Ops-Token' always; + add_header 'Access-Control-Allow-Headers' 'Content-Type, X-Lab-Manager-Token' always; add_header 'Access-Control-Max-Age' 1728000 always; if ($request_method = 'OPTIONS') { return 204; } - if ($ops_expected = "") { + if ($lab_manager_expected = "") { return 503; } - if ($ops_token != $ops_expected) { + if ($lab_manager_token != $lab_manager_expected) { return 401; } diff --git a/openresty/lua/admin_access.lua b/openresty/lua/admin_access.lua index 102934e..01cac8b 100644 --- a/openresty/lua/admin_access.lua +++ b/openresty/lua/admin_access.lua @@ -1,14 +1,10 @@ -- Strict access guard for treasury admin endpoints. --- Requires a valid access token when configured; falls back to loopback/Docker only when missing. +-- Uses TREASURY_TOKEN only. If unset, allows loopback/Docker ranges only. -local security_token = os.getenv("SECURITY_ACCESS_TOKEN") or "" -local token = security_token -local lab_manager_token = os.getenv("LAB_MANAGER_TOKEN") or "" +local token = os.getenv("TREASURY_TOKEN") or "" -local header_name = os.getenv("SECURITY_ACCESS_TOKEN_HEADER") or "X-Access-Token" -local cookie_name = os.getenv("SECURITY_ACCESS_TOKEN_COOKIE") or "access_token" -local lab_manager_header = os.getenv("LAB_MANAGER_TOKEN_HEADER") or "X-Lab-Manager-Token" -local lab_manager_cookie = os.getenv("LAB_MANAGER_TOKEN_COOKIE") or "lab_manager_token" +local header_name = os.getenv("TREASURY_TOKEN_HEADER") or "X-Access-Token" +local cookie_name = os.getenv("TREASURY_TOKEN_COOKIE") or "access_token" local function deny(message) ngx.status = ngx.HTTP_UNAUTHORIZED @@ -37,9 +33,9 @@ local function is_loopback_or_docker(ip) return false end -if token == "" and lab_manager_token == "" then +if token == "" then if not is_loopback_or_docker(ngx.var.remote_addr or "") then - return deny("Forbidden: Remote access is disabled. To enable external access, set SECURITY_ACCESS_TOKEN or LAB_MANAGER_TOKEN in your .env file and restart the service.") + return deny("Forbidden: Remote treasury admin access is disabled. Set TREASURY_TOKEN in your .env file and restart the service.") end return end @@ -62,17 +58,6 @@ end local headers = ngx.req.get_headers() local provided = headers[header_name] --- Also check lab manager token header -if not provided or provided == "" then - provided = headers[lab_manager_header] - if provided and provided ~= "" then - -- Switch to lab manager token validation - token = lab_manager_token - header_name = lab_manager_header - cookie_name = lab_manager_cookie - end -end - if not provided or provided == "" then local cookie_var = "cookie_" .. cookie_name provided = ngx.var[cookie_var] @@ -92,17 +77,17 @@ if not provided or provided == "" then end if not provided or provided == "" then - return deny("Unauthorized: Access token required. Provide " .. header_name .. " header, " .. cookie_name .. " cookie, or ?token=... query parameter.") + return deny("Unauthorized: Treasury token required. Provide " .. header_name .. " header, " .. cookie_name .. " cookie, or ?token=... query parameter.") end if provided ~= token then - return deny("Unauthorized: Invalid access token. Provide " .. header_name .. " header, " .. cookie_name .. " cookie, or ?token=... query parameter.") + return deny("Unauthorized: Invalid treasury token. Provide " .. header_name .. " header, " .. cookie_name .. " cookie, or ?token=... query parameter.") end -- Always forward the selected token header ngx.req.set_header(header_name, token) --- Additionally, forward the security token under X-Access-Token so downstream filters accept either -if security_token and security_token ~= "" then - ngx.req.set_header("X-Access-Token", security_token) +-- Ensure downstream always receives X-Access-Token. +if header_name ~= "X-Access-Token" then + ngx.req.set_header("X-Access-Token", token) end diff --git a/openresty/lua/gateway_health.lua b/openresty/lua/gateway_health.lua index 7957acc..4cd92ed 100644 --- a/openresty/lua/gateway_health.lua +++ b/openresty/lua/gateway_health.lua @@ -163,7 +163,7 @@ end local cert_days = cert_days_remaining("/etc/ssl/private/fullchain.pem") -- Env/config sanity -local required_env = { "SERVER_NAME", "OPS_SECRET" } +local required_env = { "SERVER_NAME", "LAB_MANAGER_TOKEN" } local env_ok = {} for _, k in ipairs(required_env) do env_ok[k] = (os.getenv(k) ~= nil and os.getenv(k) ~= "") diff --git a/openresty/lua/init.lua b/openresty/lua/init.lua index 23bb710..01dc520 100644 --- a/openresty/lua/init.lua +++ b/openresty/lua/init.lua @@ -38,11 +38,11 @@ local guac_api_url = os.getenv("GUAC_API_URL") refuse_default_secret("GUAC_ADMIN_PASS", admin_pass, { "guacadmin", "changeme", "change_me", "password", "test" }) -local ops_secret = os.getenv("OPS_SECRET") -if not ops_secret or ops_secret == "" then - ngx.log(ngx.WARN, "OPS_SECRET not set; /ops endpoints will remain disabled") +local lab_manager_token = os.getenv("LAB_MANAGER_TOKEN") +if not lab_manager_token or lab_manager_token == "" then + ngx.log(ngx.WARN, "LAB_MANAGER_TOKEN not set; /ops endpoints will remain disabled and /lab-manager will be private-network-only") else - refuse_default_secret("OPS_SECRET", ops_secret, { "supersecretvalue", "changeme", "change_me", "password", "test" }) + refuse_default_secret("LAB_MANAGER_TOKEN", lab_manager_token, { "supersecretvalue", "changeme", "change_me", "password", "test" }) end local function trim(value) diff --git a/openresty/lua/internal_access.lua b/openresty/lua/treasury_access.lua similarity index 86% rename from openresty/lua/internal_access.lua rename to openresty/lua/treasury_access.lua index 21fa9db..9850ba0 100644 --- a/openresty/lua/internal_access.lua +++ b/openresty/lua/treasury_access.lua @@ -1,7 +1,7 @@ --- Access guard for wallet/treasury endpoints. --- Enforces an access token for non-local clients when configured. +-- Access guard for treasury-facing endpoints. +-- Enforces the treasury token for non-local clients when configured. -local token = os.getenv("SECURITY_ACCESS_TOKEN") or "" +local token = os.getenv("TREASURY_TOKEN") or "" local function deny(message) ngx.status = ngx.HTTP_UNAUTHORIZED @@ -87,13 +87,13 @@ end if token == "" then if not is_loopback_or_docker(client_ip) then - return deny("Forbidden: Remote access is disabled. To enable external access, set SECURITY_ACCESS_TOKEN in your .env file and restart the service.") + return deny("Forbidden: Remote access is disabled. To enable external access, set TREASURY_TOKEN in your .env file and restart the service.") end return end -local header_name = os.getenv("SECURITY_ACCESS_TOKEN_HEADER") or "X-Access-Token" -local cookie_name = os.getenv("SECURITY_ACCESS_TOKEN_COOKIE") or "access_token" +local header_name = os.getenv("TREASURY_TOKEN_HEADER") or "X-Access-Token" +local cookie_name = os.getenv("TREASURY_TOKEN_COOKIE") or "access_token" local function is_tokenized_path(value) if not value or value == "" then @@ -156,10 +156,10 @@ end if provided and provided ~= "" then if provided ~= token then - return deny("Unauthorized: Invalid access token. " .. token_hint()) + return deny("Unauthorized: Invalid treasury token. " .. token_hint()) end elseif not is_private(client_ip) then - return deny("Unauthorized: Access token required for remote access. " .. token_hint()) + return deny("Unauthorized: Treasury token required for remote access. " .. token_hint()) end ngx.req.set_header(header_name, token) diff --git a/openresty/nginx.conf b/openresty/nginx.conf index 1eb054f..7db7805 100644 --- a/openresty/nginx.conf +++ b/openresty/nginx.conf @@ -8,15 +8,14 @@ env GUAC_ADMIN_PASS; env AUTO_LOGOUT_ON_DISCONNECT; env GUAC_API_URL; env OPS_API_URL; -env OPS_SECRET; env MYSQL_DATABASE; env MYSQL_USER; env MYSQL_PASSWORD; env CORS_ALLOWED_ORIGINS; env MARKETPLACE_URL; -env SECURITY_ACCESS_TOKEN; -env SECURITY_ACCESS_TOKEN_HEADER; -env SECURITY_ACCESS_TOKEN_COOKIE; +env TREASURY_TOKEN; +env TREASURY_TOKEN_HEADER; +env TREASURY_TOKEN_COOKIE; env LAB_MANAGER_TOKEN; env LAB_MANAGER_TOKEN_HEADER; env LAB_MANAGER_TOKEN_COOKIE; diff --git a/openresty/tests/README.md b/openresty/tests/README.md index ec28f64..f2ebc7f 100644 --- a/openresty/tests/README.md +++ b/openresty/tests/README.md @@ -1,159 +1,64 @@ # OpenResty Lua Unit Tests -This directory contains unit tests for the OpenResty Lua modules in the Lab Gateway project. +This folder contains unit tests for the Lua modules used by OpenResty. -## Test Structure +## Test structure -``` +```text openresty/tests/ -โ”œโ”€โ”€ run.lua # Main test runner -โ”œโ”€โ”€ run-lua-tests.sh # Shell script runner (Linux/Mac) -โ”œโ”€โ”€ run-lua-tests.ps1 # PowerShell script runner (Windows) -โ”œโ”€โ”€ helpers/ -โ”‚ โ”œโ”€โ”€ runner.lua # Custom test framework -โ”‚ โ”œโ”€โ”€ ngx_stub.lua # Mock ngx object -โ”‚ โ””โ”€โ”€ http_client_stub.lua # Mock HTTP client -โ””โ”€โ”€ unit/ - โ”œโ”€โ”€ access_handler_spec.lua - โ”œโ”€โ”€ access_handler_extended_spec.lua - โ”œโ”€โ”€ body_filter_handler_spec.lua - โ”œโ”€โ”€ body_filter_handler_extended_spec.lua - โ”œโ”€โ”€ header_filter_handler_spec.lua - โ”œโ”€โ”€ header_filter_handler_extended_spec.lua - โ”œโ”€โ”€ log_handler_spec.lua - โ”œโ”€โ”€ log_handler_extended_spec.lua - โ”œโ”€โ”€ session_guard_spec.lua - โ””โ”€โ”€ session_guard_extended_spec.lua +|- run.lua +|- run-lua-tests.sh +|- run-lua-tests.ps1 +|- helpers/ +| |- runner.lua +| |- ngx_stub.lua +| `- http_client_stub.lua +`- unit/ + |- access_handler_spec.lua + |- access_handler_extended_spec.lua + |- body_filter_handler_spec.lua + |- body_filter_handler_extended_spec.lua + |- header_filter_handler_spec.lua + |- header_filter_handler_extended_spec.lua + |- admin_access_spec.lua + |- treasury_access_spec.lua + |- jwt_handler_spec.lua + |- lab_manager_access_spec.lua + |- log_handler_spec.lua + |- log_handler_extended_spec.lua + |- session_guard_spec.lua + `- session_guard_extended_spec.lua ``` -## Running Tests - -### Using Docker (Recommended) +## Run tests -The tests can be run in a Docker container with all dependencies: +Linux/macOS: -**Linux/Mac:** ```bash ./openresty/tests/run-lua-tests.sh ``` -**Windows (PowerShell):** +Windows (PowerShell): + ```powershell .\openresty\tests\run-lua-tests.ps1 ``` -### Direct Execution (requires LuaJIT + lua-cjson) +Direct run (if LuaJIT + lua-cjson are installed): -From the project root: ```bash -cd openresty -luajit tests/run.lua +luajit openresty/tests/run.lua ``` -## Test Coverage - -### Modules Tested - -| Module | Test File | Coverage | -|--------|-----------|----------| -| `access_handler.lua` | `access_handler_spec.lua`, `access_handler_extended_spec.lua` | Cookie parsing, JTI validation, session expiration | -| `header_filter_handler.lua` | `header_filter_handler_spec.lua`, `header_filter_handler_extended_spec.lua` | JWT validation, cookie setting, redirect handling | -| `body_filter_handler.lua` | `body_filter_handler_spec.lua`, `body_filter_handler_extended_spec.lua` | JSON parsing, token storage, chunked responses | -| `log_handler.lua` | `log_handler_spec.lua`, `log_handler_extended_spec.lua` | WebSocket tunnel detection, session cleanup | -| `session_guard.lua` | `session_guard_spec.lua`, `session_guard_extended_spec.lua` | Session expiration, token revocation | - -### Key Test Scenarios - -#### Access Handler -- Requests without cookies -- Invalid/missing JTI -- Expired sessions -- Valid session propagation -- Edge cases: empty values, special characters - -#### Header Filter Handler -- JWT validation (signature, issuer, audience) -- Cookie creation with proper flags (Secure, HttpOnly, SameSite) -- Redirect URL rewriting (301, 302, 303, 307, 308) -- Replay attack prevention (JTI tracking) -- Username normalization - -#### Body Filter Handler -- JSON response detection -- Token mapping storage -- Chunked response handling -- Unicode and special characters - -#### Log Handler -- WebSocket tunnel closure detection -- Admin user filtering -- JWT vs manual session differentiation -- Pending closure flagging - -#### Session Guard -- Expired session termination -- Guacamole API integration -- Token revocation -- Error handling (timeouts, malformed responses) - -## Test Framework - -Tests use a custom lightweight framework (`helpers/runner.lua`) with: -- `runner.describe(name, fn)` - Define test suite -- `runner.it(name, fn)` - Define test case -- `runner.assert.equals(expected, actual)` - Assert equality -- `runner.assert.truthy(value)` - Assert truthy value -- `runner.assert.contains(list, item)` - Assert list contains item +## Coverage focus -### Mock Objects +- Access/session propagation (`access_handler`, `treasury_access`, `lab_manager_access`) +- JWT validation and JWKS fetch (`jwt_handler`) +- Header/body filters for Guacamole auth flow +- Session cleanup and revocation (`log_handler`, `session_guard`) -#### ngx_stub.lua -Provides a mock `ngx` object with: -- Shared dictionaries (`ngx.shared.cache`, `ngx.shared.config`) -- Request/response handling -- Time simulation -- Logging capture +## Add a new spec -#### http_client_stub.lua -Simulates HTTP client responses for testing Guacamole API calls. - -## Adding New Tests - -1. Create a new spec file in `tests/unit/`: -```lua -local runner = require "tests.helpers.runner" -local ngx_factory = require "tests.helpers.ngx_stub" -local module = require "modules.your_module" - -runner.describe("Your module", function() - runner.it("does something", function() - local ngx = ngx_factory.new({ ... }) - module.run(ngx) - runner.assert.equals(expected, ngx.some_value) - end) -end) - -return runner -``` - -2. Add the spec to `run.lua`: -```lua -local specs = { - -- existing specs... - "tests.unit.your_module_spec" -} -``` - -3. Run the tests to verify. - -## CI/CD Integration - -The tests can be integrated into CI pipelines: - -```yaml -# GitHub Actions example -- name: Run Lua tests - run: | - docker build -t lua-tests -f openresty/Dockerfile.test openresty - docker run --rm lua-tests luajit tests/run.lua -``` +1. Create a file in `openresty/tests/unit/`. +2. Register it in `openresty/tests/run.lua`. +3. Run the suite. diff --git a/openresty/tests/run.lua b/openresty/tests/run.lua index be17449..9c30fbd 100644 --- a/openresty/tests/run.lua +++ b/openresty/tests/run.lua @@ -18,7 +18,8 @@ local specs = { "tests.unit.log_handler_extended_spec", "tests.unit.body_filter_handler_spec", "tests.unit.body_filter_handler_extended_spec", - "tests.unit.internal_access_spec", + "tests.unit.treasury_access_spec", + "tests.unit.admin_access_spec", "tests.unit.lab_manager_access_spec", "tests.unit.jwt_handler_spec" } diff --git a/openresty/tests/unit/admin_access_spec.lua b/openresty/tests/unit/admin_access_spec.lua new file mode 100644 index 0000000..e8b437f --- /dev/null +++ b/openresty/tests/unit/admin_access_spec.lua @@ -0,0 +1,161 @@ +local runner = require "tests.helpers.runner" +local ngx_factory = require "tests.helpers.ngx_stub" + +local function resolve_admin_access_path() + local source = debug.getinfo(1, "S").source + if source:sub(1, 1) == "@" then + source = source:sub(2) + end + source = source:gsub("\\", "/") + local dir = source:match("^(.*)/[^/]+$") or "." + + local candidates = { + dir .. "/../../lua/admin_access.lua", + dir .. "/../lua/admin_access.lua", + "openresty/lua/admin_access.lua", + "lua/admin_access.lua" + } + + for _, path in ipairs(candidates) do + local file = io.open(path, "r") + if file then + file:close() + return path + end + end + + error("Cannot locate admin_access.lua for tests") +end + +local function with_env(env, fn) + local original_getenv = os.getenv + env = env or {} + ---@diagnostic disable-next-line: duplicate-set-field + os.getenv = function(name) + local value = env[name] + if value ~= nil then + return value + end + return original_getenv(name) + end + + local ok, result = xpcall(fn, debug.traceback) + os.getenv = original_getenv + if not ok then + error(result, 0) + end + return result +end + +local function run_admin_access(opts) + local env = opts.env or {} + local headers = opts.headers or {} + local uri_args = opts.uri_args or {} + local ngx = ngx_factory.new({ + var = opts.var or {} + }) + + ngx.req.get_headers = function() + return headers + end + ngx.req.get_uri_args = function() + return uri_args + end + ngx.say = function(message) + ngx._body = message + end + ngx.exit = function(code) + ngx._exit = code + return code + end + + _G.ngx = ngx + with_env(env, function() + dofile(resolve_admin_access_path()) + end) + _G.ngx = nil + + return ngx +end + +runner.describe("Treasury admin token guard", function() + runner.it("rejects public IPs when TREASURY_TOKEN is not configured", function() + local ngx = run_admin_access({ + env = { TREASURY_TOKEN = "" }, + var = { remote_addr = "8.8.8.8" } + }) + + runner.assert.equals(ngx.HTTP_UNAUTHORIZED, ngx.status) + runner.assert.equals(ngx.HTTP_UNAUTHORIZED, ngx._exit) + end) + + runner.it("allows loopback when TREASURY_TOKEN is not configured", function() + local ngx = run_admin_access({ + env = { TREASURY_TOKEN = "" }, + var = { remote_addr = "127.0.0.1" } + }) + + runner.assert.equals(nil, ngx.status) + runner.assert.equals(nil, ngx._exit) + end) + + runner.it("rejects invalid TREASURY_TOKEN on public IP", function() + local ngx = run_admin_access({ + env = { TREASURY_TOKEN = "treasury-token" }, + headers = { ["X-Access-Token"] = "wrong-token" }, + var = { remote_addr = "8.8.8.8" } + }) + + runner.assert.equals(ngx.HTTP_UNAUTHORIZED, ngx.status) + runner.assert.equals(ngx.HTTP_UNAUTHORIZED, ngx._exit) + end) + + runner.it("requires token on private network when TREASURY_TOKEN is configured", function() + local ngx = run_admin_access({ + env = { TREASURY_TOKEN = "treasury-token" }, + var = { remote_addr = "172.17.0.2" } + }) + + runner.assert.equals(ngx.HTTP_UNAUTHORIZED, ngx.status) + runner.assert.equals(ngx.HTTP_UNAUTHORIZED, ngx._exit) + end) + + runner.it("allows valid TREASURY_TOKEN on public IP", function() + local ngx = run_admin_access({ + env = { TREASURY_TOKEN = "treasury-token" }, + headers = { ["X-Access-Token"] = "treasury-token" }, + var = { remote_addr = "8.8.8.8" } + }) + + runner.assert.equals(nil, ngx.status) + runner.assert.equals("treasury-token", ngx.req.headers["X-Access-Token"]) + end) + + runner.it("does not accept LAB_MANAGER_TOKEN header for treasury admin access", function() + local ngx = run_admin_access({ + env = { + TREASURY_TOKEN = "treasury-token", + LAB_MANAGER_TOKEN = "lab-token" + }, + headers = { ["X-Lab-Manager-Token"] = "lab-token" }, + var = { remote_addr = "8.8.8.8" } + }) + + runner.assert.equals(ngx.HTTP_UNAUTHORIZED, ngx.status) + runner.assert.equals(ngx.HTTP_UNAUTHORIZED, ngx._exit) + end) + + runner.it("accepts token from query parameter and sets access cookie", function() + local ngx = run_admin_access({ + env = { TREASURY_TOKEN = "treasury-token" }, + uri_args = { token = "treasury-token" }, + var = { remote_addr = "8.8.8.8" } + }) + + runner.assert.equals(nil, ngx.status) + runner.assert.equals("treasury-token", ngx.req.headers["X-Access-Token"]) + runner.assert.equals("access_token=treasury-token; Path=/; HttpOnly; Secure; SameSite=Lax", ngx.header["Set-Cookie"]) + end) +end) + +return runner diff --git a/openresty/tests/unit/internal_access_spec.lua b/openresty/tests/unit/treasury_access_spec.lua similarity index 77% rename from openresty/tests/unit/internal_access_spec.lua rename to openresty/tests/unit/treasury_access_spec.lua index 8a87e59..8ed7c07 100644 --- a/openresty/tests/unit/internal_access_spec.lua +++ b/openresty/tests/unit/treasury_access_spec.lua @@ -1,7 +1,7 @@ local runner = require "tests.helpers.runner" local ngx_factory = require "tests.helpers.ngx_stub" -local function resolve_internal_access_path() +local function resolve_treasury_access_path() local source = debug.getinfo(1, "S").source if source:sub(1, 1) == "@" then source = source:sub(2) @@ -10,10 +10,10 @@ local function resolve_internal_access_path() local dir = source:match("^(.*)/[^/]+$") or "." local candidates = { - dir .. "/../../lua/internal_access.lua", - dir .. "/../lua/internal_access.lua", - "openresty/lua/internal_access.lua", - "lua/internal_access.lua" + dir .. "/../../lua/treasury_access.lua", + dir .. "/../lua/treasury_access.lua", + "openresty/lua/treasury_access.lua", + "lua/treasury_access.lua" } for _, path in ipairs(candidates) do @@ -24,7 +24,7 @@ local function resolve_internal_access_path() end end - error("Cannot locate internal_access.lua for tests") + error("Cannot locate treasury_access.lua for tests") end local function with_env(env, fn) @@ -47,7 +47,7 @@ local function with_env(env, fn) return result end -local function run_internal_access(opts) +local function run_treasury_access(opts) local env = opts.env or {} local headers = opts.headers or {} local ngx = ngx_factory.new({ @@ -67,17 +67,17 @@ local function run_internal_access(opts) _G.ngx = ngx with_env(env, function() - dofile(resolve_internal_access_path()) + dofile(resolve_treasury_access_path()) end) _G.ngx = nil return ngx end -runner.describe("Access token guard", function() +runner.describe("Treasury token guard", function() runner.it("rejects public IPs when no token configured", function() - local ngx = run_internal_access({ - env = { SECURITY_ACCESS_TOKEN = "" }, + local ngx = run_treasury_access({ + env = { TREASURY_TOKEN = "" }, var = { remote_addr = "8.8.8.8" } }) @@ -87,8 +87,8 @@ runner.it("rejects public IPs when no token configured", function() end) runner.it("allows loopback when no token configured", function() - local ngx = run_internal_access({ - env = { SECURITY_ACCESS_TOKEN = "" }, + local ngx = run_treasury_access({ + env = { TREASURY_TOKEN = "" }, var = { remote_addr = "127.0.0.1" } }) @@ -98,8 +98,8 @@ runner.it("allows loopback when no token configured", function() end) runner.it("rejects when token is invalid", function() - local ngx = run_internal_access({ - env = { SECURITY_ACCESS_TOKEN = "secret-token" }, + local ngx = run_treasury_access({ + env = { TREASURY_TOKEN = "secret-token" }, headers = { ["X-Access-Token"] = "wrong-token" }, var = { remote_addr = "8.8.8.8" } }) @@ -110,8 +110,8 @@ end) end) runner.it("allows private network without provided token", function() - local ngx = run_internal_access({ - env = { SECURITY_ACCESS_TOKEN = "secret-token" }, + local ngx = run_treasury_access({ + env = { TREASURY_TOKEN = "secret-token" }, var = { remote_addr = "172.17.0.2" } }) @@ -120,8 +120,8 @@ end) end) runner.it("allows valid token on public IP", function() - local ngx = run_internal_access({ - env = { SECURITY_ACCESS_TOKEN = "secret-token" }, + local ngx = run_treasury_access({ + env = { TREASURY_TOKEN = "secret-token" }, headers = { ["X-Access-Token"] = "secret-token" }, var = { remote_addr = "8.8.8.8" } }) @@ -131,8 +131,8 @@ end) end) runner.it("accepts token from cookie", function() - local ngx = run_internal_access({ - env = { SECURITY_ACCESS_TOKEN = "secret-token" }, + local ngx = run_treasury_access({ + env = { TREASURY_TOKEN = "secret-token" }, var = { remote_addr = "8.8.8.8", cookie_access_token = "secret-token" diff --git a/ops-worker/README.md b/ops-worker/README.md index c3a5001..c56304e 100644 --- a/ops-worker/README.md +++ b/ops-worker/README.md @@ -1,35 +1,39 @@ # Ops Worker for Lab Station Integration -This lightweight worker centralizes the operational tasks the Lab Gateway needs to perform on Lab Station hosts: +This service handles remote lab host operations for the gateway: -- Send Wake-on-LAN and validate boot (ping with retries). -- Execute `LabStation.exe` commands via WinRM (`prepare-session`, `release-session --reboot`, `session guard`, `power shutdown|hibernate`, `status-json`, `recovery reboot-if-needed`). -- Poll `heartbeat.json` (and optionally `session-guard-events.jsonl`) and persist denormalized fields for the UI/alerts. +- Wake-on-LAN and reachability checks. +- Remote LabStation command execution over WinRM. +- Heartbeat polling and persistence in MySQL. +- Optional reservation automation (start/end orchestration). -## Components +## Main components -- `worker.py`: Flask API exposing `/api/wol`, `/api/winrm`, `/api/heartbeat/poll`. -- Scheduler (optional) runs inside the worker to poll heartbeats periodically when `OPS_POLL_ENABLED=true`. -- MySQL persistence for host catalog, heartbeat snapshots, and reservation operations (see `mysql/001-create-schema.sql` and `mysql/002-labstation-ops.sql`). -- `/ops/` is protected with `OPS_SECRET` (env). OpenResty sets cookie `ops_token` when serving `/lab-manager/` and validates cookie or header `X-Ops-Token` before proxying. +- `worker.py`: Flask API and scheduler. +- `hosts.json` (`OPS_CONFIG`): host inventory and credentials references. +- MySQL tables from `mysql/001-create-schema.sql` and `mysql/002-labstation-ops.sql`. ## Quick start (dev) ```bash cd ops-worker python -m venv .venv -. .venv/Scripts/activate # or source .venv/bin/activate +. .venv/Scripts/activate # or: source .venv/bin/activate pip install -r requirements.txt + export OPS_BIND=0.0.0.0 export OPS_PORT=8081 export OPS_CONFIG=hosts.json -export MYSQL_DSN="mysql+pymysql://user:pass@mysql:3306/lab_gateway" +export MYSQL_DSN="mysql+pymysql://user:pass@mysql:3306/guacamole_db" export OPS_POLL_ENABLED=true export OPS_POLL_INTERVAL=60 + python worker.py ``` -`hosts.json` (example, copy from `hosts.sample.json` and edit secrets): +## hosts.json example + +Copy `hosts.sample.json` and replace credentials. ```json { @@ -42,91 +46,55 @@ python worker.py "winrm_pass": "env:WINRM_PASS_LAB_WS_01", "winrm_transport": "ntlm", "heartbeat_path": "C:\\\\LabStation\\\\labstation\\\\data\\\\telemetry\\\\heartbeat.json", - "events_path": "C:\\\\LabStation\\\\labstation\\\\data\\\\telemetry\\\\session-guard-events.jsonl" + "events_path": "C:\\\\LabStation\\\\labstation\\\\data\\\\telemetry\\\\session-guard-events.jsonl", + "labs": ["1"] } ] } ``` -## Smoke tests (curl) - -```bash -# Health -curl -s http://localhost:8081/health - -# Wake-on-LAN + ping -curl -s -XPOST http://localhost:8081/api/wol -H "Content-Type: application/json" -d '{"host":"lab-ws-01"}' - -# Heartbeat poll (persists to MySQL when configured) -curl -s -XPOST http://localhost:8081/api/heartbeat/poll -H "Content-Type: application/json" -d '{"host":"lab-ws-01"}' - -# Run LabStation.exe prepare-session via WinRM -curl -s -XPOST http://localhost:8081/api/winrm -H "Content-Type: application/json" -d '{"host":"lab-ws-01","command":"prepare-session","args":["--guard-grace=90"]}' -``` - ## API (internal) -- `POST /api/wol` โ†’ `{ host, mac?, broadcast?, port?, ping_target?, ping_timeout?, attempts? }` -- `POST /api/winrm` โ†’ `{ host, command, args?, user?, password?, transport?, use_ssl?, port? }` - - Builds and runs `C:\LabStation\LabStation.exe ` via PowerShell/WinRM. -- `POST /api/heartbeat/poll` โ†’ `{ host }` - - Reads `heartbeat.json` via WinRM (`Get-Content -Raw`) and upserts into MySQL. -- `POST /api/reservations/start` โ†’ `{ reservationId, host, labId?, wake?, wakeOptions?, prepare?, prepareArgs?, guardGrace? }` - - Sends WoL + ping validation (unless `wake=false`), then runs `prepare-session` with configurable args. Each step logs into `reservation_operations`. -- `POST /api/reservations/end` โ†’ `{ reservationId, host, labId?, release?, releaseArgs?, powerAction? }` - - Runs `release-session` (defaults to `--reboot`) and optional `power shutdown|hibernate` with args, logging outcomes per reservation. - -Responses include `duration_ms`, stdout/stderr, parsed JSON (when applicable), and DB persistence status. +- `GET /health` +- `POST /api/wol` + - Body: `{ host, mac?, broadcast?, port?, ping_target?, ping_timeout?, attempts? }` +- `POST /api/winrm` + - Body: `{ host, command, args?, user?, password?, transport?, use_ssl?, port? }` + - Runs `C:\LabStation\LabStation.exe ` via WinRM. +- `POST /api/heartbeat/poll` + - Body: `{ host, include_events? }` +- `POST /api/reservations/start` + - Body: `{ reservationId, host, labId?, wake?, wakeOptions?, prepare?, prepareArgs?, guardGrace? }` +- `POST /api/reservations/end` + - Body: `{ reservationId, host, labId?, release?, releaseArgs?, powerAction? }` +- `GET /api/reservations/timeline?reservationId=...&limit=...&offset=...` +- `POST /api/hosts/reload` +- `POST /api/hosts/quarantine` + - Body: `{ host, quarantined }` ## Scheduler -Enable with `OPS_POLL_ENABLED=true` (env) and set `OPS_POLL_INTERVAL=60` (seconds). The worker will: - -- Iterate configured hosts in `hosts.json`. -- Fetch heartbeat and latest session-guard event line. -- Upsert host catalog + insert heartbeat snapshot. -- If `OPS_RESERVATION_AUTOMATION=true`, it looks for reservations in `lab_reservations`, awakes/prepares/closes and persists `reservation_operations`. - -## Reservation automation (optional) - -`OPS_RESERVATION_AUTOMATION=true` (default value) lets the worker wake/prepare/release Lab Stations around each booking automatically. Requirements: - -1. Every host entry in `hosts.json` must declare the blockchain lab IDs it can serve: - - ```json - { - "name": "lab-ws-01", - "address": "lab-ws-01", - "labs": ["1", "chemistry-lab"] - } - ``` - -2. `MYSQL_DSN` must point to the same MySQL instance used by `blockchain-services` (where Flyway creates `lab_reservations`) and by the ops-worker tables from `mysql/001-create-schema.sql` + `mysql/002-labstation-ops.sql`. - -When enabled, the worker: +Enable with: -- Looks ahead `OPS_RESERVATION_START_LEAD` seconds (default 120) and triggers `/api/reservations/start` once for CONFIRMED reservations; successful runs mark the DB row as `ACTIVE`. -- Waits `OPS_RESERVATION_END_DELAY` seconds (default 60) after the end time to invoke `/api/reservations/end`; successful runs mark the row as `COMPLETED`. -- Writes summary rows to `reservation_operations` using `action = "scheduler:start" | "scheduler:end"` so you can audit or retry. +- `OPS_POLL_ENABLED=true` +- `OPS_POLL_INTERVAL=60` -Tuning knobs: +Reservation automation knobs: -| Variable | Default | Purpose | -| --- | --- | --- | -| `OPS_RESERVATION_AUTOMATION` | `true` | Master toggle. | -| `OPS_RESERVATION_SCAN_INTERVAL` | `30` | How often to scan MySQL (seconds). | -| `OPS_RESERVATION_START_LEAD` | `120` | Seconds before `start_time` to prepare the host / lab station. | -| `OPS_RESERVATION_END_DELAY` | `60` | Seconds after `end_time` to release/power actions. | -| `OPS_RESERVATION_LOOKBACK` | `21600` | Maximum age (seconds) of reservations to consider when catching up. | -| `OPS_RESERVATION_RETRY_COOLDOWN` | `60` | Minimum seconds between scheduler attempts for the same reservation. | +- `OPS_RESERVATION_AUTOMATION` (compose default: `true`) +- `OPS_RESERVATION_SCAN_INTERVAL` (default `30`) +- `OPS_RESERVATION_START_LEAD` (default `120`) +- `OPS_RESERVATION_END_DELAY` (default `60`) +- `OPS_RESERVATION_LOOKBACK` (default `21600`) +- `OPS_RESERVATION_RETRY_COOLDOWN` (default `60`) ## Deployment notes -- OpenResty proxies `/ops/` to this service (see `openresty/lab_access.conf`). -- Use a dedicated network-only account for WinRM (`SeDenyInteractiveLogonRight` on the host / lab station). -- Keep secrets outside git; `hosts.json` is gitignored on purpose. -- `/ops/` is gated by `OPS_SECRET` (env) and expects header `X-Ops-Token` or cookie `ops_token`. -- WinRM allowlist: set `OPS_ALLOWED_COMMANDS` (comma-separated) to restrict `/api/winrm` (default: `prepare-session,release-session,power,session,energy,status-json,recovery,account,service,wol,status`). -- WinRM timeouts: `OPS_WINRM_READ_TIMEOUT` and `OPS_WINRM_OPERATION_TIMEOUT` (seconds) configure the session. -- Credentials: prefer `env:VAR` in `hosts.json` for `winrm_user`/`winrm_pass` and set those env vars in the container (avoid plaintext in JSON). -- In OpenResty, `/lab-manager/` allows private networks by default and requires `LAB_MANAGER_TOKEN` for non-private clients; `/ops/` remains limited to `127.0.0.1` and Docker private networks (`172.16.0.0/12`) and requires `OPS_SECRET`. +- OpenResty proxies `/ops/` to this service. +- `/ops/` requires `LAB_MANAGER_TOKEN` via `X-Lab-Manager-Token` header or `lab_manager_token` cookie. +- **Network restriction**: OpenResty allows `/ops/` only from `127.0.0.1` and `172.16.0.0/12` (enforced before token validation). + - Lab Manager UI (`/lab-manager`) works from any network with valid token. + - Lab Station operations (`/ops` API) require access from gateway server or private networks. + - When accessing Lab Manager remotely, ops features will show a network restriction warning. +- Prefer `env:VAR_NAME` in `hosts.json` for WinRM credentials. +- Keep `hosts.json` secrets out of git. diff --git a/setup.bat b/setup.bat index f204a0d..a454a7d 100644 --- a/setup.bat +++ b/setup.bat @@ -158,74 +158,31 @@ call :UpdateEnv "%ROOT_ENV_FILE%" "GUAC_ADMIN_USER" "!guac_admin_user!" call :UpdateEnv "%ROOT_ENV_FILE%" "GUAC_ADMIN_PASS" "!guac_admin_pass!" echo. -REM OPS Worker Secret -echo OPS Worker Secret -echo ================== -echo This secret authenticates the ops-worker for lab station operations. -set "ops_secret=" -set /p "ops_secret=OPS secret (leave empty for auto-generated): " -set "ops_secret=!ops_secret: =!" - -if "!ops_secret!"=="" ( - set "ops_secret=ops_%RANDOM%%RANDOM%%RANDOM%" - echo Generated OPS secret: !ops_secret! -) -if /i "!ops_secret!"=="supersecretvalue" ( - echo Refusing to use insecure OPS secret. Set a strong value. - exit /b 1 -) -if /i "!ops_secret!"=="changeme" ( - echo Refusing to use insecure OPS secret. Set a strong value. - exit /b 1 -) -if /i "!ops_secret!"=="change_me" ( - echo Refusing to use insecure OPS secret. Set a strong value. - exit /b 1 -) -if /i "!ops_secret!"=="password" ( - echo Refusing to use insecure OPS secret. Set a strong value. - exit /b 1 -) -if /i "!ops_secret!"=="test" ( - echo Refusing to use insecure OPS secret. Set a strong value. - exit /b 1 -) - -call :UpdateEnv "%ROOT_ENV_FILE%" "OPS_SECRET" "!ops_secret!" -echo. - -REM Blockchain Services Access Token -echo Blockchain Services Access Token -echo ================================= -echo This token protects /wallet, /treasury, and /wallet-dashboard behind OpenResty. +REM Wallet/Treasury Access Token +echo Wallet/Treasury Access Token +echo ============================ +echo This token protects /wallet, /treasury, /wallet-dashboard, and /treasury/admin/** behind OpenResty. set "access_token=" -set /p "access_token=Access token (leave empty for auto-generated): " +set /p "access_token=Wallet/Treasury token (leave empty for auto-generated): " set "access_token=!access_token: =!" if "!access_token!"=="" ( set "access_token=acc_%RANDOM%%RANDOM%%RANDOM%" - echo Generated access token: !access_token! + echo Generated Wallet/Treasury token: !access_token! ) -call :UpdateEnvBoth "SECURITY_ACCESS_TOKEN" "!access_token!" -call :UpdateEnvBoth "SECURITY_ACCESS_TOKEN_HEADER" "X-Access-Token" -call :UpdateEnvBoth "SECURITY_ACCESS_TOKEN_COOKIE" "access_token" -call :UpdateEnvBoth "SECURITY_ACCESS_TOKEN_REQUIRED" "true" +call :UpdateEnvBoth "TREASURY_TOKEN" "!access_token!" +call :UpdateEnvBoth "TREASURY_TOKEN_HEADER" "X-Access-Token" +call :UpdateEnvBoth "TREASURY_TOKEN_COOKIE" "access_token" +call :UpdateEnvBoth "TREASURY_TOKEN_REQUIRED" "true" call :UpdateEnvBoth "SECURITY_ALLOW_PRIVATE_NETWORKS" "true" call :UpdateEnvBoth "ADMIN_DASHBOARD_ALLOW_PRIVATE" "true" -if exist "%BLOCKCHAIN_ENV_FILE%" ( - call :UpdateEnv "%BLOCKCHAIN_ENV_FILE%" "BCHAIN_SECURITY_ACCESS_TOKEN" "!access_token!" - call :UpdateEnv "%BLOCKCHAIN_ENV_FILE%" "BCHAIN_SECURITY_ACCESS_TOKEN_HEADER" "X-Access-Token" - call :UpdateEnv "%BLOCKCHAIN_ENV_FILE%" "BCHAIN_SECURITY_ACCESS_TOKEN_COOKIE" "access_token" - call :UpdateEnv "%BLOCKCHAIN_ENV_FILE%" "BCHAIN_SECURITY_ACCESS_TOKEN_REQUIRED" "true" - call :UpdateEnv "%BLOCKCHAIN_ENV_FILE%" "BCHAIN_SECURITY_ALLOW_PRIVATE_NETWORKS" "true" -) echo. REM Lab Manager Access Token echo Lab Manager Access Token echo ======================== -echo This token protects /lab-manager when accessed outside private networks. +echo This token protects /lab-manager and /ops when accessed outside private networks. set "lab_manager_token=" set /p "lab_manager_token=Lab Manager token (leave empty for auto-generated): " set "lab_manager_token=!lab_manager_token: =!" @@ -531,7 +488,7 @@ if /i "!domain!"=="localhost" ( set "token_host=https://!domain!:!https_port!" ) ) -echo * Access token cookie: !token_host!/wallet-dashboard?token=!access_token! +echo * Wallet/Treasury token cookie: !token_host!/wallet-dashboard?token=!access_token! echo * Lab Manager token cookie: !token_host!/lab-manager?token=!lab_manager_token! echo * Guacamole: /guacamole/ echo * Blockchain Services API: /auth @@ -589,7 +546,7 @@ if /i "!domain!"=="localhost" ( set "token_host=https://!domain!:!https_port!" ) ) -echo * Access token cookie: !token_host!/wallet-dashboard?token=!access_token! +echo * Wallet/Treasury token cookie: !token_host!/wallet-dashboard?token=!access_token! echo * Lab Manager token cookie: !token_host!/lab-manager?token=!lab_manager_token! echo * Guacamole: /guacamole/ ^(!guac_admin_user! / !guac_admin_pass!^) echo * Blockchain Services API: /auth diff --git a/setup.sh b/setup.sh index f189ec3..a0f7979 100644 --- a/setup.sh +++ b/setup.sh @@ -162,59 +162,30 @@ update_env_var "$ROOT_ENV_FILE" "GUAC_ADMIN_USER" "$guac_admin_user" update_env_var "$ROOT_ENV_FILE" "GUAC_ADMIN_PASS" "$guac_admin_pass" echo -# OPS Worker Secret -echo "OPS Worker Secret" -echo "==================" -echo "This secret authenticates the ops-worker for lab station operations." -read -p "OPS secret (leave empty for auto-generated): " ops_secret -ops_secret=$(echo "$ops_secret" | tr -d ' ') - -if [ -z "$ops_secret" ]; then - ops_secret="ops_$(openssl rand -hex 16 2>/dev/null || echo ${RANDOM}${RANDOM}${RANDOM})" - echo "Generated OPS secret: $ops_secret" -fi - -case "$(printf '%s' "$ops_secret" | tr '[:upper:]' '[:lower:]')" in - supersecretvalue|changeme|change_me|password|test) - echo "Refusing to use insecure OPS secret. Set a strong value." >&2 - exit 1 - ;; -esac - -update_env_var "$ROOT_ENV_FILE" "OPS_SECRET" "$ops_secret" -echo - -# Access Token for blockchain-services -echo "Blockchain Services Access Token" -echo "=================================" -echo "This token protects /wallet, /treasury, and /wallet-dashboard behind OpenResty." -read -p "Access token (leave empty for auto-generated): " access_token +# Wallet/Treasury Access Token +echo "Wallet/Treasury Access Token" +echo "============================" +echo "This token protects /wallet, /treasury, /wallet-dashboard, and /treasury/admin/** behind OpenResty." +read -p "Wallet/Treasury token (leave empty for auto-generated): " access_token access_token=$(echo "$access_token" | tr -d ' ') if [ -z "$access_token" ]; then access_token="acc_$(openssl rand -hex 16 2>/dev/null || echo ${RANDOM}${RANDOM}${RANDOM})" - echo "Generated access token: $access_token" + echo "Generated Wallet/Treasury token: $access_token" fi -update_env_in_all "SECURITY_ACCESS_TOKEN" "$access_token" -update_env_in_all "SECURITY_ACCESS_TOKEN_HEADER" "X-Access-Token" -update_env_in_all "SECURITY_ACCESS_TOKEN_COOKIE" "access_token" -update_env_in_all "SECURITY_ACCESS_TOKEN_REQUIRED" "true" +update_env_in_all "TREASURY_TOKEN" "$access_token" +update_env_in_all "TREASURY_TOKEN_HEADER" "X-Access-Token" +update_env_in_all "TREASURY_TOKEN_COOKIE" "access_token" +update_env_in_all "TREASURY_TOKEN_REQUIRED" "true" update_env_in_all "SECURITY_ALLOW_PRIVATE_NETWORKS" "true" update_env_in_all "ADMIN_DASHBOARD_ALLOW_PRIVATE" "true" -if [ -f "$BLOCKCHAIN_ENV_FILE" ]; then - update_env_var "$BLOCKCHAIN_ENV_FILE" "BCHAIN_SECURITY_ACCESS_TOKEN" "$access_token" - update_env_var "$BLOCKCHAIN_ENV_FILE" "BCHAIN_SECURITY_ACCESS_TOKEN_HEADER" "X-Access-Token" - update_env_var "$BLOCKCHAIN_ENV_FILE" "BCHAIN_SECURITY_ACCESS_TOKEN_COOKIE" "access_token" - update_env_var "$BLOCKCHAIN_ENV_FILE" "BCHAIN_SECURITY_ACCESS_TOKEN_REQUIRED" "true" - update_env_var "$BLOCKCHAIN_ENV_FILE" "BCHAIN_SECURITY_ALLOW_PRIVATE_NETWORKS" "true" -fi echo # Lab Manager Access Token echo "Lab Manager Access Token" echo "========================" -echo "This token protects /lab-manager when accessed outside private networks." +echo "This token protects /lab-manager and /ops when accessed outside private networks." read -p "Lab Manager token (leave empty for auto-generated): " lab_manager_token lab_manager_token=$(echo "$lab_manager_token" | tr -d ' ') @@ -559,7 +530,7 @@ else token_host="${token_host}:${https_port}" fi fi -echo " * Access token cookie: ${token_host}/wallet-dashboard?token=${access_token}" +echo " * Wallet/Treasury token cookie: ${token_host}/wallet-dashboard?token=${access_token}" echo " * Lab Manager token cookie: ${token_host}/lab-manager?token=${lab_manager_token}" echo " * Guacamole: /guacamole/" echo " * Blockchain Services API: /auth" @@ -613,7 +584,7 @@ else token_host="${token_host}:${https_port}" fi fi -echo " * Access token cookie: ${token_host}/wallet-dashboard?token=${access_token}" +echo " * Wallet/Treasury token cookie: ${token_host}/wallet-dashboard?token=${access_token}" echo " * Lab Manager token cookie: ${token_host}/lab-manager?token=${lab_manager_token}" echo " * Guacamole: /guacamole/ ($guac_admin_user / $guac_admin_pass)" echo " * Blockchain Services API: /auth" diff --git a/tests/integration/README.md b/tests/integration/README.md index af57b08..42a53cb 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -1,126 +1,62 @@ # Integration Tests -This directory contains integration tests for the DecentraLabs Gateway. +This suite validates OpenResty routing and security behavior against mock services. -## Overview +## What is covered -The integration tests verify the correct behavior of the full gateway stack including: - -- **Rate Limiting**: Verifies that public auth endpoints are rate-limited -- **Health Endpoints**: Tests aggregated health checks across all services -- **Authentication Flow**: Tests JWKS, OpenID Configuration, and token validation -- **Ops Worker Protection**: Verifies token-based access control -- **Security Headers**: Ensures all required security headers are present -- **HTTPS Redirect**: Verifies HTTP to HTTPS redirection +- `/health` and `/gateway/health` +- OIDC endpoints (`/auth/jwks`, `/.well-known/openid-configuration`) +- Auth endpoint rate limiting behavior (mocked backend) +- CORS behavior on auth paths +- `/ops` token protection +- Static files and HTTP->HTTPS redirect +- Security headers ## Prerequisites -- Docker and Docker Compose -- Bash shell (Git Bash on Windows) -- curl +- Docker + Docker Compose plugin +- Bash (Git Bash on Windows) +- `curl` -## Running Tests +## Run ```bash -# From the tests/integration directory -./run-integration.sh - -# Or from the project root +# From repo root ./tests/integration/run-integration.sh + +# Or inside tests/integration +./run-integration.sh ``` -## Test Structure +## Files -``` +```text tests/integration/ -โ”œโ”€โ”€ run-integration.sh # Main test runner -โ”œโ”€โ”€ docker-compose.integration.yml # Test infrastructure -โ”œโ”€โ”€ README.md # This file -โ”œโ”€โ”€ certs/ # Self-signed certificates for testing -โ”‚ โ””โ”€โ”€ generate-certs.sh # Certificate generation script -โ””โ”€โ”€ mocks/ # Mock services - โ”œโ”€โ”€ blockchain-services/ # Mock auth service with rate limiting - โ”‚ โ”œโ”€โ”€ Dockerfile - โ”‚ โ””โ”€โ”€ server.py - โ”œโ”€โ”€ guacamole/ # Mock Guacamole API - โ”‚ โ”œโ”€โ”€ Dockerfile - โ”‚ โ””โ”€โ”€ server.py - โ””โ”€โ”€ ops-worker/ # Mock ops-worker service - โ”œโ”€โ”€ Dockerfile - โ””โ”€โ”€ server.py +|- run-integration.sh +|- docker-compose.integration.yml +|- certs/generate-certs.sh +`- mocks/ + |- blockchain-services/ + |- guacamole/ + `- ops-worker/ ``` -## Test Cases - -| # | Test | Description | -|---|------|-------------| -| 1 | Health endpoint | Verifies `/health` returns healthy status | -| 2 | Gateway aggregated health | Verifies `/gateway/health` aggregates all services | -| 3 | JWKS endpoint | Verifies `/auth/jwks` returns public keys | -| 4 | OpenID Configuration | Verifies `/.well-known/openid-configuration` | -| 5 | Rate limiting | Verifies rate limiting triggers on burst requests | -| 6 | CORS headers | Verifies CORS headers on auth endpoints | -| 7 | Guacamole access | Verifies Guacamole endpoint is accessible | -| 8 | Ops security (no token) | Verifies ops endpoint rejects unauthorized requests | -| 9 | Ops security (valid token) | Verifies ops endpoint accepts valid token | -| 10 | Static files | Verifies static files are served correctly | -| 11 | HTTP redirect | Verifies HTTP to HTTPS redirect | -| 12 | Security headers | Verifies HSTS, X-Frame-Options, etc. | - -## Mock Services - -### blockchain-services (Port 8080) -Simulates the authentication service with: -- Rate limiting (5 requests per 10 seconds per IP) -- JWT/JWKS generation -- OpenID Configuration -- Wallet authentication simulation - -### guacamole (Port 8080) -Simulates Apache Guacamole API with: -- Connection listing -- Token-based authentication -- Session management - -### ops-worker (Port 5001) -Simulates the lab station operations service with: -- Health endpoints -- Lab station management -- WoL/command simulation - -## Environment Variables - -The docker-compose.integration.yml uses these environment variables for OpenResty: - -| Variable | Value | Description | -|----------|-------|-------------| -| `SERVER_NAME` | localhost | Server hostname | -| `HTTPS_PORT` | 18443 | HTTPS port for tests | -| `HTTP_PORT` | 18080 | HTTP port for tests | -| `OPS_SECRET` | integration-test-secret | Token for ops-worker access | +## Important ports (test stack) + +- OpenResty HTTPS: `18443` +- OpenResty HTTP: `18080` +- Mock ops-worker: `5001` (internal) ## Cleanup -Tests automatically clean up containers after completion. To manually clean up: +The script cleans up automatically. Manual cleanup: ```bash -docker compose -f docker-compose.integration.yml down -v +docker compose -f tests/integration/docker-compose.integration.yml down -v ``` ## Troubleshooting -### Services fail to start -Check the logs: ```bash -docker compose -f docker-compose.integration.yml logs +docker compose -f tests/integration/docker-compose.integration.yml logs ``` - -### Certificate issues -Regenerate certificates: -```bash -rm -f certs/privkey.pem certs/fullchain.pem -./certs/generate-certs.sh -``` - -### Rate limiting not triggering -The mock blockchain-services uses a 5 req/10s rate limit. Ensure no other requests are being made during the test. diff --git a/tests/integration/docker-compose.integration.yml b/tests/integration/docker-compose.integration.yml index 21b6c88..5c5ef05 100644 --- a/tests/integration/docker-compose.integration.yml +++ b/tests/integration/docker-compose.integration.yml @@ -37,7 +37,7 @@ services: GUAC_API_URL: http://guacamole:8080/guacamole/api AUTH_SERVICE_URL: http://blockchain-services:8080 OPS_API_URL: http://ops-worker:5001 - OPS_SECRET: integration-test-secret + LAB_MANAGER_TOKEN: integration-test-secret CORS_ALLOWED_ORIGINS: http://localhost:3000 volumes: - ./certs:/etc/ssl/private diff --git a/tests/integration/run-integration.sh b/tests/integration/run-integration.sh index e14dda3..eac8451 100644 --- a/tests/integration/run-integration.sh +++ b/tests/integration/run-integration.sh @@ -183,7 +183,7 @@ fi # Test 9: Ops endpoint accepts valid token # ================================================================= echo "Test 9: Ops endpoint with valid token" -OPS_WITH_TOKEN=$(curl -sk -H "X-Ops-Token: integration-test-secret" "${BASE_URL}/ops/health" || echo "error") +OPS_WITH_TOKEN=$(curl -sk -H "X-Lab-Manager-Token: integration-test-secret" "${BASE_URL}/ops/health" || echo "error") if echo "$OPS_WITH_TOKEN" | grep -q "ok\|status"; then log_pass "Ops endpoint accepts valid token" else diff --git a/tests/smoke/docker-compose.smoke.yml b/tests/smoke/docker-compose.smoke.yml index dbaefe5..4b2e806 100644 --- a/tests/smoke/docker-compose.smoke.yml +++ b/tests/smoke/docker-compose.smoke.yml @@ -18,8 +18,8 @@ services: SERVER_NAME: lab.test HTTPS_PORT: "18443" GUAC_API_URL: http://guacamole:8080/guacamole/api - OPS_SECRET: test-ops-secret-123 - SECURITY_ACCESS_TOKEN: smoke-access-token + LAB_MANAGER_TOKEN: test-ops-secret-123 + TREASURY_TOKEN: smoke-access-token SECURITY_ALLOW_PRIVATE_NETWORKS: "true" volumes: - ./certs:/etc/ssl/private diff --git a/tests/smoke/run-smoke.sh b/tests/smoke/run-smoke.sh index 28c89e8..64d5b81 100644 --- a/tests/smoke/run-smoke.sh +++ b/tests/smoke/run-smoke.sh @@ -109,7 +109,7 @@ fi # Test 5: Ops endpoint accepts valid token # ================================================================= echo "Test 5: Ops endpoint accepts valid token" -OPS_OK=$(curl -sk --resolve lab.test:${PORT}:127.0.0.1 -H "X-Ops-Token: test-ops-secret-123" -b "ops_token=test-ops-secret-123" https://lab.test:${PORT}/ops/health || true) +OPS_OK=$(curl -sk --resolve lab.test:${PORT}:127.0.0.1 -H "X-Lab-Manager-Token: test-ops-secret-123" -b "lab_manager_token=test-ops-secret-123" https://lab.test:${PORT}/ops/health || true) if echo "$OPS_OK" | grep -Eq '"status"[[:space:]]*:[[:space:]]*"ok"' || [ "$OPS_OK" = "ops-worker-ok" ]; then log_pass "Ops endpoint accepts valid token" else diff --git a/web/assets/js/auth-token-fetch.js b/web/assets/js/auth-token-fetch.js index 85bcde1..56f7adf 100644 --- a/web/assets/js/auth-token-fetch.js +++ b/web/assets/js/auth-token-fetch.js @@ -12,12 +12,16 @@ header: 'X-Lab-Manager-Token' }, '/wallet-dashboard': { - key: 'dlabs_security_token', + key: 'dlabs_treasury_token', header: 'X-Access-Token' }, '/institution-config': { - key: 'dlabs_security_token', + key: 'dlabs_treasury_token', header: 'X-Access-Token' + }, + '/ops': { + key: 'dlabs_lab_manager_token', + header: 'X-Lab-Manager-Token' } }; @@ -32,6 +36,20 @@ return null; } + function getRequestPath(url) { + try { + if (typeof url === 'string') { + return new URL(url, window.location.origin).pathname; + } + if (url && typeof url.url === 'string') { + return new URL(url.url, window.location.origin).pathname; + } + } catch (_) { + return window.location.pathname; + } + return window.location.pathname; + } + // Extract token from URL SYNCHRONOUSLY on page load try { const urlParams = new URLSearchParams(window.location.search); @@ -62,7 +80,8 @@ // Wrap fetch to add token header const originalFetch = window.fetch; window.fetch = function(url, options = {}) { - const config = getTokenConfigForPath(window.location.pathname); + const requestPath = getRequestPath(url); + const config = getTokenConfigForPath(requestPath) || getTokenConfigForPath(window.location.pathname); if (config) { const storedToken = localStorage.getItem(config.key); if (storedToken) { diff --git a/web/assets/js/auth-token-handler.js b/web/assets/js/auth-token-handler.js index 8da35ad..8d21094 100644 --- a/web/assets/js/auth-token-handler.js +++ b/web/assets/js/auth-token-handler.js @@ -9,8 +9,7 @@ // Token storage const TOKEN_STORAGE = { LAB_MANAGER: 'dlabs_lab_manager_token', - SECURITY: 'dlabs_security_token', - OPS: 'dlabs_ops_token' + TREASURY: 'dlabs_treasury_token' }; // Token configuration based on path @@ -19,29 +18,29 @@ key: TOKEN_STORAGE.LAB_MANAGER, header: 'X-Lab-Manager-Token', cookie: 'lab_manager_token', - title: 'Lab Manager Access', + title: 'Lab Manager Access Token', description: 'This area requires a Lab Manager access token.' }, '/wallet-dashboard': { - key: TOKEN_STORAGE.SECURITY, + key: TOKEN_STORAGE.TREASURY, header: 'X-Access-Token', cookie: 'access_token', - title: 'Security Access', - description: 'This area requires a security access token.' + title: 'Wallet/Treasury Access Token', + description: 'This area requires a Wallet/Treasury access token.' }, '/institution-config': { - key: TOKEN_STORAGE.SECURITY, + key: TOKEN_STORAGE.TREASURY, header: 'X-Access-Token', cookie: 'access_token', - title: 'Security Access', - description: 'This area requires a security access token.' + title: 'Wallet/Treasury Access Token', + description: 'This area requires a Wallet/Treasury access token.' }, - '/ops-api': { - key: TOKEN_STORAGE.OPS, - header: 'X-Ops-Token', - cookie: 'ops_token', - title: 'Ops API Access', - description: 'This area requires an Ops API access token.' + '/ops': { + key: TOKEN_STORAGE.LAB_MANAGER, + header: 'X-Lab-Manager-Token', + cookie: 'lab_manager_token', + title: 'Lab Manager Access Token', + description: 'This area requires a Lab Manager access token.' } }; @@ -210,6 +209,29 @@ return null; } + function getRequestPath(url) { + try { + if (typeof url === 'string') { + return new URL(url, window.location.origin).pathname; + } + if (url && typeof url.url === 'string') { + return new URL(url.url, window.location.origin).pathname; + } + } catch (_) { + return window.location.pathname; + } + return window.location.pathname; + } + + function setRequestHeader(options, name, value) { + options.headers = options.headers || {}; + if (options.headers instanceof Headers) { + options.headers.set(name, value); + return; + } + options.headers[name] = value; + } + // Enhanced fetch wrapper function createAuthenticatedFetch() { const originalFetch = window.fetch; @@ -217,16 +239,14 @@ window.fetch = function(...args) { let [url, options = {}] = args; - // Get current path - const currentPath = window.location.pathname; - const config = getTokenConfigForPath(currentPath); + const requestPath = getRequestPath(url); + const config = getTokenConfigForPath(requestPath) || getTokenConfigForPath(window.location.pathname); // Add stored token if available if (config) { const storedToken = localStorage.getItem(config.key); if (storedToken) { - options.headers = options.headers || {}; - options.headers[config.header] = storedToken; + setRequestHeader(options, config.header, storedToken); } } @@ -240,8 +260,7 @@ showTokenModal(config, (token) => { // Retry request with token const retryOptions = { ...options }; - retryOptions.headers = retryOptions.headers || {}; - retryOptions.headers[config.header] = token; + setRequestHeader(retryOptions, config.header, token); originalFetch(url, retryOptions) .then(retryResponse => { diff --git a/web/assets/js/lab-manager.js b/web/assets/js/lab-manager.js index 54ca6a3..b34eb88 100644 --- a/web/assets/js/lab-manager.js +++ b/web/assets/js/lab-manager.js @@ -64,6 +64,7 @@ document.addEventListener('DOMContentLoaded', () => { loadConfig(); loadAuthHealth(); + checkOpsAvailability(); // Lab Station ops state const hostInput = $('#hostInput'); @@ -418,6 +419,14 @@ document.addEventListener('DOMContentLoaded', () => { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ host }) }); + if (res.status === 403) { + showToast('Access denied: /ops restricted to private networks', 'error'); + return; + } + if (res.status === 401) { + showToast('Unauthorized: check LAB_MANAGER_TOKEN', 'error'); + return; + } if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); hostState[host] = data; @@ -425,7 +434,7 @@ document.addEventListener('DOMContentLoaded', () => { showToast(`Heartbeat ${host} ok`, 'success'); } catch (err) { console.error(err); - showToast(`Heartbeat failed for ${host}`, 'error'); + showToast(`Heartbeat failed for ${host}: ${err.message}`, 'error'); } } @@ -436,12 +445,20 @@ document.addEventListener('DOMContentLoaded', () => { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ host }) }); + if (res.status === 403) { + showToast('Access denied: /ops restricted to private networks', 'error'); + return; + } + if (res.status === 401) { + showToast('Unauthorized: check LAB_MANAGER_TOKEN', 'error'); + return; + } if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); showToast(`WoL ${host}: ${data.success ? 'sent' : 'failed'}`, data.success ? 'success' : 'error'); } catch (err) { console.error(err); - showToast(`WoL failed for ${host}`, 'error'); + showToast(`WoL failed for ${host}: ${err.message}`, 'error'); } } @@ -452,13 +469,21 @@ document.addEventListener('DOMContentLoaded', () => { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ host, command, args }) }); + if (res.status === 403) { + showToast('Access denied: /ops restricted to private networks', 'error'); + return; + } + if (res.status === 401) { + showToast('Unauthorized: check LAB_MANAGER_TOKEN', 'error'); + return; + } if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); const ok = data.exit_code === 0; showToast(`${command} on ${host}: ${ok ? 'ok' : 'err'}`, ok ? 'success' : 'error'); } catch (err) { console.error(err); - showToast(`${command} failed on ${host}`, 'error'); + showToast(`${command} failed on ${host}: ${err.message}`, 'error'); } } @@ -497,6 +522,18 @@ document.addEventListener('DOMContentLoaded', () => { offset: String(offset) }); const res = await fetch(`/ops/api/reservations/timeline?${params.toString()}`); + if (res.status === 403) { + const msg = 'Access denied: /ops restricted to private networks'; + if (!append) setTimelineMessage(msg); + showToast(msg, 'error'); + return; + } + if (res.status === 401) { + const msg = 'Unauthorized: check LAB_MANAGER_TOKEN'; + if (!append) setTimelineMessage(msg); + showToast(msg, 'error'); + return; + } const body = await res.json(); if (!res.ok) { const msg = body?.error || `Unable to load timeline (HTTP ${res.status}).`; @@ -902,4 +939,34 @@ document.addEventListener('DOMContentLoaded', () => { if (value === false) return 'missing'; return 'unknown'; } -}); + async function checkOpsAvailability() { + try { + const res = await fetch('/ops/health', { method: 'HEAD' }); + if (res.status === 403) { + showOpsWarning(); + return false; + } + return res.ok || res.status === 401; // 401 = token issue, not network + } catch { + return false; + } + } + + function showOpsWarning() { + const opsHint = $('#opsHint'); + if (opsHint) { + opsHint.innerHTML = ` + + Network restriction: Lab Station operations require access from the gateway server or private networks (127.0.0.1, 172.16.0.0/12). + Access /lab-manager from the institution network to enable these features. + `; + opsHint.style.backgroundColor = '#fff3cd'; + opsHint.style.color = '#856404'; + opsHint.style.padding = '12px'; + opsHint.style.borderRadius = '4px'; + opsHint.style.border = '1px solid #ffc107'; + } + if (refreshHostsBtn) refreshHostsBtn.disabled = true; + if (addHostBtn) addHostBtn.disabled = true; + if (timelineBtn) timelineBtn.disabled = true; + }}); diff --git a/web/lab-manager/index.html b/web/lab-manager/index.html index ee6a840..903d0b3 100644 --- a/web/lab-manager/index.html +++ b/web/lab-manager/index.html @@ -8,6 +8,7 @@ + @@ -70,7 +71,7 @@

Lab Station Ops

-
Hosts are stored locally in your browser. Ensure ops-worker/hosts.json has matching entries and WinRM creds.
+
Hosts are stored locally in your browser. Ensure ops-worker/hosts.json has matching entries and WinRM credentials.
Add a host to start polling heartbeat.
@@ -116,13 +117,13 @@

Notifications Setup

Timezone -
@@ -220,9 +221,9 @@

Delivery Settings

SMTP

- STARTTLS -
@@ -282,7 +283,7 @@

Microsoft Graph

- + From d72d6ee481bcdc481e7a835f98e97514bc9c87a6 Mon Sep 17 00:00:00 2001 From: Ravenink Date: Sat, 14 Feb 2026 15:37:40 +0100 Subject: [PATCH 10/16] Fix setup scripts when mysql volume already exists --- mysql/ensure-user-entrypoint.sh | 13 +++-- setup.bat | 72 ++++++++++++++++++++++++++- setup.sh | 88 +++++++++++++++++++++++++++++++-- 3 files changed, 161 insertions(+), 12 deletions(-) diff --git a/mysql/ensure-user-entrypoint.sh b/mysql/ensure-user-entrypoint.sh index bb647f7..942f93d 100644 --- a/mysql/ensure-user-entrypoint.sh +++ b/mysql/ensure-user-entrypoint.sh @@ -26,9 +26,11 @@ trap 'forward_signal INT' INT # Wait for MySQL to be ready before running the ensure-user script if [[ -f "${ENSURE_SCRIPT}" ]]; then echo "Waiting for MySQL to be ready before ensuring user permissions..." - - # Wait up to 60 seconds for MySQL to be ready - for i in {1..60}; do + + max_wait="${MYSQL_ENSURE_USER_WAIT_SECONDS:-180}" + + # Wait up to max_wait seconds for MySQL to be ready + for (( i=1; i<=max_wait; i++ )); do if mysql -u root -p"${MYSQL_ROOT_PASSWORD}" -e "SELECT 1" >/dev/null 2>&1; then echo "MySQL is ready. Waiting for Guacamole schema..." waited=0 @@ -65,8 +67,9 @@ if [[ -f "${ENSURE_SCRIPT}" ]]; then break fi - if [[ $i -eq 60 ]]; then - echo "Warning: MySQL did not become ready in time. Ensure-user script not executed." >&2 + if [[ $i -eq $max_wait ]]; then + echo "Warning: MySQL did not become ready in ${max_wait}s. Ensure-user script not executed." >&2 + echo "Hint: if mysql_data already exists, verify .env MYSQL_ROOT_PASSWORD matches the stored root password." >&2 fi sleep 1 diff --git a/setup.bat b/setup.bat index a454a7d..0814029 100644 --- a/setup.bat +++ b/setup.bat @@ -12,6 +12,11 @@ set "compose_files=" set "compose_full=" set "cf_enabled=0" set "certbot_enabled=0" +set "existing_mysql_root_password=" +set "existing_mysql_password=" +set "db_credentials_changed=0" +set "reset_mysql_volume=0" +set "mysql_volume_name=" echo DecentraLabs Gateway - Full Version Setup echo ========================================== @@ -55,6 +60,9 @@ if errorlevel 1 ( echo blockchain-services submodule ready. echo. +call :ReadEnvValue "%ROOT_ENV_FILE%" "MYSQL_ROOT_PASSWORD" existing_mysql_root_password +call :ReadEnvValue "%ROOT_ENV_FILE%" "MYSQL_PASSWORD" existing_mysql_password + REM Check if .env already exists if exist "%ROOT_ENV_FILE%" ( echo .env file already exists! @@ -95,12 +103,30 @@ set "mysql_password=" set /p "mysql_root_password=MySQL root password: " set /p "mysql_password=Guacamole database password: " +if "!mysql_root_password!"=="" ( + if not "!existing_mysql_root_password!"=="" ( + call :IsPlaceholderSecret "!existing_mysql_root_password!" + if errorlevel 1 ( + set "mysql_root_password=!existing_mysql_root_password!" + echo Reusing existing MySQL root password from .env + ) + ) +) if "!mysql_root_password!"=="" ( set mysql_root_password=R00t_P@ss_%RANDOM%_%TIME:~9% set mysql_root_password=!mysql_root_password: =! echo Generated root password: !mysql_root_password! ) +if "!mysql_password!"=="" ( + if not "!existing_mysql_password!"=="" ( + call :IsPlaceholderSecret "!existing_mysql_password!" + if errorlevel 1 ( + set "mysql_password=!existing_mysql_password!" + echo Reusing existing Guacamole DB password from .env + ) + ) +) if "!mysql_password!"=="" ( set mysql_password=Gu@c_%RANDOM%_%TIME:~9% set mysql_password=!mysql_password: =! @@ -110,6 +136,29 @@ if "!mysql_password!"=="" ( call :UpdateEnvBoth "MYSQL_ROOT_PASSWORD" "!mysql_root_password!" call :UpdateEnvBoth "MYSQL_PASSWORD" "!mysql_password!" +set "db_credentials_changed=0" +if not "!mysql_root_password!"=="!existing_mysql_root_password!" set "db_credentials_changed=1" +if not "!mysql_password!"=="!existing_mysql_password!" set "db_credentials_changed=1" + +for /f %%V in ('powershell -NoLogo -NoProfile -Command "$p=$env:COMPOSE_PROJECT_NAME; if (-not $p) { $p=[IO.Path]::GetFileName((Get-Location).Path).ToLowerInvariant() -replace '[^a-z0-9]','' }; $vol=docker volume ls -q --filter \"label=com.docker.compose.project=$p\" --filter \"label=com.docker.compose.volume=mysql_data\" | Select-Object -First 1; if (-not $vol) { $fallback=($p + '_mysql_data'); docker volume inspect $fallback *> $null; if ($LASTEXITCODE -eq 0) { $vol=$fallback } }; if ($vol) { $vol }"') do set "mysql_volume_name=%%V" + +if "!db_credentials_changed!"=="1" if defined mysql_volume_name ( + echo. + echo Detected existing MySQL volume: !mysql_volume_name! + echo Database credentials changed in .env, so startup can fail with Access denied ^(1045^). + set /p "reset_mysql_input=Reset MySQL volume now to apply new credentials? This removes MySQL data. (y/N): " + set "reset_mysql_input=!reset_mysql_input: =!" + if /i "!reset_mysql_input!"=="y" ( + set "reset_mysql_volume=1" + echo MySQL volume will be reset before startup. + ) else if /i "!reset_mysql_input!"=="yes" ( + set "reset_mysql_volume=1" + echo MySQL volume will be reset before startup. + ) else ( + echo Keeping existing MySQL volume. If startup fails, run: docker compose down -v + ) +) + echo. echo IMPORTANT: Save these passwords securely! echo Root password: !mysql_root_password! @@ -502,7 +551,11 @@ echo. echo Building and starting services... echo This may take several minutes on first run... -call !compose_full! down --remove-orphans +if "!reset_mysql_volume!"=="1" ( + call !compose_full! down --remove-orphans -v +) else ( + call !compose_full! down --remove-orphans +) if errorlevel 1 goto compose_fail call !compose_full! build --no-cache if errorlevel 1 ( @@ -522,6 +575,9 @@ goto compose_success :compose_fail echo Failed to start services. Check the error messages above. +if "!db_credentials_changed!"=="1" if defined mysql_volume_name if "!reset_mysql_volume!"=="0" ( + echo Hint: Existing MySQL volume likely has old credentials. Run: docker compose down -v +) goto docker_start_done :compose_success @@ -560,7 +616,7 @@ echo. echo Configuration: echo Environment: %ROOT_ENV_FILE% echo Blockchain Services Config: %BLOCKCHAIN_ENV_FILE% -echo Certificates & Keys: certs\ +echo Certificates ^& Keys: certs\ echo Wallet data directory: blockchain-data\ echo. echo Full version deployment complete! @@ -613,3 +669,15 @@ if exist "%read_file%" ( :read_done if "%~3" NEQ "" set "%~3=%read_result%" exit /b + +:IsPlaceholderSecret +set "secret_value=%~1" +if /i "%secret_value%"=="" exit /b 0 +if /i "%secret_value%"=="CHANGE_ME" exit /b 0 +if /i "%secret_value%"=="CHANGEME" exit /b 0 +if /i "%secret_value%"=="SECURE_PASSWORD" exit /b 0 +if /i "%secret_value%"=="DB_PASSWORD" exit /b 0 +if /i "%secret_value%"=="YOUR_PASSWORD" exit /b 0 +if /i "%secret_value%"=="PASSWORD" exit /b 0 +if /i "%secret_value%"=="TEST" exit /b 0 +exit /b 1 diff --git a/setup.sh b/setup.sh index a0f7979..aeacf01 100644 --- a/setup.sh +++ b/setup.sh @@ -14,6 +14,11 @@ compose_files="" compose_profiles="" cf_enabled=false certbot_enabled=false +existing_mysql_root_password="" +existing_mysql_password="" +db_credentials_changed=false +reset_mysql_volume=false +mysql_volume_name="" echo "DecentraLabs Gateway - Full Version Setup" echo "==========================================" @@ -37,10 +42,44 @@ get_env_default() { local value="" if [ -f "$file" ]; then value=$(grep -E "^${key}=" "$file" | head -n 1 | cut -d'=' -f2-) + value="${value%$'\r'}" fi echo "$value" } +is_placeholder_secret() { + local raw="$1" + local lower + lower="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | tr -d ' \r\n\t')" + case "$lower" in + ""|changeme|change_me|secure_password|db_password|your_password|password|test) + return 0 + ;; + *) + return 1 + ;; + esac +} + +detect_mysql_volume() { + local project_name="${COMPOSE_PROJECT_NAME:-}" + local volume_name="" + + if [ -z "$project_name" ]; then + project_name="$(basename "$PWD" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9')" + fi + + volume_name="$(docker volume ls -q \ + --filter "label=com.docker.compose.project=${project_name}" \ + --filter "label=com.docker.compose.volume=mysql_data" | head -n 1)" + + if [ -z "$volume_name" ] && docker volume inspect "${project_name}_mysql_data" >/dev/null 2>&1; then + volume_name="${project_name}_mysql_data" + fi + + echo "$volume_name" +} + update_env_in_all() { local key="$1" local value="$2" @@ -77,6 +116,9 @@ git submodule update --init --recursive blockchain-services echo "blockchain-services submodule ready." echo +existing_mysql_root_password="$(get_env_default "MYSQL_ROOT_PASSWORD" "$ROOT_ENV_FILE")" +existing_mysql_password="$(get_env_default "MYSQL_PASSWORD" "$ROOT_ENV_FILE")" + # Check if .env already exists if [ -f "$ROOT_ENV_FILE" ]; then echo ".env file already exists!" @@ -112,19 +154,48 @@ read -p "MySQL root password: " mysql_root_password read -p "Guacamole database password: " mysql_password if [ -z "$mysql_root_password" ]; then - mysql_root_password="R00t_P@ss_${RANDOM}_$(date +%s)" - echo "Generated root password: $mysql_root_password" + if [ -n "$existing_mysql_root_password" ] && ! is_placeholder_secret "$existing_mysql_root_password"; then + mysql_root_password="$existing_mysql_root_password" + echo "Reusing existing MySQL root password from .env" + else + mysql_root_password="R00t_P@ss_${RANDOM}_$(date +%s)" + echo "Generated root password: $mysql_root_password" + fi fi if [ -z "$mysql_password" ]; then - mysql_password="Gu@c_${RANDOM}_$(date +%s)" - echo "Generated database password: $mysql_password" + if [ -n "$existing_mysql_password" ] && ! is_placeholder_secret "$existing_mysql_password"; then + mysql_password="$existing_mysql_password" + echo "Reusing existing Guacamole DB password from .env" + else + mysql_password="Gu@c_${RANDOM}_$(date +%s)" + echo "Generated database password: $mysql_password" + fi fi # Update passwords in env files update_env_in_all "MYSQL_ROOT_PASSWORD" "$mysql_root_password" update_env_in_all "MYSQL_PASSWORD" "$mysql_password" +if [ "$mysql_root_password" != "$existing_mysql_root_password" ] || [ "$mysql_password" != "$existing_mysql_password" ]; then + db_credentials_changed=true +fi + +mysql_volume_name="$(detect_mysql_volume)" +if [ -n "$mysql_volume_name" ] && [ "$db_credentials_changed" = true ]; then + echo + echo "Detected existing MySQL volume: ${mysql_volume_name}" + echo "Database credentials changed in .env, so startup can fail with Access denied (1045)." + read -p "Reset MySQL volume now to apply new credentials? This removes MySQL data. (y/N): " reset_mysql_input + reset_mysql_input="$(echo "$reset_mysql_input" | tr -d ' ' | tr '[:upper:]' '[:lower:]')" + if [[ "$reset_mysql_input" =~ ^(y|yes)$ ]]; then + reset_mysql_volume=true + echo "MySQL volume will be reset before startup." + else + echo "Keeping existing MySQL volume. If startup fails, run: docker compose down -v" + fi +fi + echo echo "IMPORTANT: Save these passwords securely!" echo " Root password: $mysql_root_password" @@ -559,7 +630,11 @@ echo "Building and starting services..." echo "This may take several minutes on first run..." set +e -$compose_full down --remove-orphans +if [ "$reset_mysql_volume" = true ]; then + $compose_full down --remove-orphans -v +else + $compose_full down --remove-orphans +fi $compose_full build --no-cache $compose_full up -d compose_result=$? @@ -604,6 +679,9 @@ echo " * Blockchain Services API: /auth" echo "Your blockchain-based authentication system is now running." else echo "Failed to start services. Check the error messages above." + if [ -n "$mysql_volume_name" ] && [ "$db_credentials_changed" = true ] && [ "$reset_mysql_volume" != true ]; then + echo "Hint: Existing MySQL volume uses old credentials. Run: $compose_full down -v" + fi fi echo From 351989875e0c94fa7e1a361b20ae772d54a2da45 Mon Sep 17 00:00:00 2001 From: Ravenink Date: Sat, 14 Feb 2026 16:24:16 +0100 Subject: [PATCH 11/16] Cleaned env variables and fixed setup scripts --- .env.example | 33 ++++++------------------------- README.md | 17 ++++------------ blockchain-services | 2 +- setup.bat | 37 +++++++++++++++++------------------ setup.sh | 47 +++++++++++++++++++++++++-------------------- 5 files changed, 55 insertions(+), 81 deletions(-) diff --git a/.env.example b/.env.example index 87b4780..d6d641a 100644 --- a/.env.example +++ b/.env.example @@ -15,8 +15,6 @@ OPENRESTY_BIND_ADDRESS=127.0.0.1 # OpenResty bind ports (local ports on the host) OPENRESTY_BIND_HTTPS_PORT=443 OPENRESTY_BIND_HTTP_PORT=80 -# Deployment mode (informational) -DEPLOY_MODE=direct # Host UID/GID for bind mounts (Linux/macOS) # setup.sh will auto-fill these values. @@ -31,6 +29,8 @@ MYSQL_ROOT_PASSWORD=CHANGE_ME MYSQL_DATABASE=guacamole_db MYSQL_USER=guacamole_user MYSQL_PASSWORD=CHANGE_ME +# Optional override for blockchain-services schema +BLOCKCHAIN_MYSQL_DATABASE=blockchain_services # ============================================================================= # GUACAMOLE CONFIGURATION @@ -55,9 +55,8 @@ CERTBOT_EMAIL= # ============================================================================= # This configuration is optional - to expose the gateway without public IP -# Set ENABLE_CLOUDFLARE=true to start the cloudflared sidecar (docker compose --profile cloudflare up -d) +# Start the sidecar with compose profile: --profile cloudflare or --profile cloudflare-token # If you have a token from a Cloudflare tunnel, paste it below. If empty, a Quick Tunnel will be used. -ENABLE_CLOUDFLARE=false CLOUDFLARE_TUNNEL_TOKEN= # ============================================================================= @@ -70,13 +69,6 @@ LAB_MANAGER_TOKEN=CHANGE_ME LAB_MANAGER_TOKEN_HEADER=X-Lab-Manager-Token LAB_MANAGER_TOKEN_COOKIE=lab_manager_token -# ============================================================================= -# BLOCKCHAIN RPC CONFIGURATION -# ============================================================================= - -# Sepolia RPC endpoints (comma-separated). Keep these updated or use a paid provider. -ETHEREUM_SEPOLIA_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com,https://0xrpc.io/sep,https://ethereum-sepolia-public.nodies.app - # ============================================================================= # BLOCKCHAIN-SERVICES REMOTE ACCESS # ============================================================================= @@ -90,19 +82,6 @@ TREASURY_TOKEN_REQUIRED=true SECURITY_ALLOW_PRIVATE_NETWORKS=true ADMIN_DASHBOARD_ALLOW_PRIVATE=true -# ============================================================================= -# BLOCKCHAIN-SERVICES FEATURES -# ============================================================================= - -# Enable provider registration endpoints. -FEATURES_PROVIDERS_REGISTRATION_ENABLED=true - -# Treasury admin EIP-712 signature domain (optional overrides) -TREASURY_ADMIN_DOMAIN_NAME=DecentraLabsTreasuryAdmin -TREASURY_ADMIN_DOMAIN_VERSION=1 -TREASURY_ADMIN_DOMAIN_CHAIN_ID=11155111 -TREASURY_ADMIN_DOMAIN_VERIFYING_CONTRACT= - # ============================================================================= # CORS CONFIGURATION (OpenResty) # ============================================================================= @@ -112,6 +91,6 @@ TREASURY_ADMIN_DOMAIN_VERIFYING_CONTRACT= # Leave empty to use MARKETPLACE_URL as the only allowed origin (recommended). CORS_ALLOWED_ORIGINS= -# Blockchain-services wallet/treasury CORS allowlist. -# Must include the gateway origin to allow /wallet and /treasury from the browser. -WALLET_ALLOWED_ORIGINS= +# Blockchain-specific settings (CONTRACT_ADDRESS, ETHEREUM_*_RPC_URL, +# WALLET_ALLOWED_ORIGINS, ALLOWED_ORIGINS, MARKETPLACE_PUBLIC_KEY_URL, etc.) +# belong in blockchain-services/.env. diff --git a/README.md b/README.md index 5bd7234..c0e8c08 100644 --- a/README.md +++ b/README.md @@ -236,8 +236,6 @@ OPENRESTY_BIND_ADDRESS=0.0.0.0 # OpenResty bind ports (local ports on the host) OPENRESTY_BIND_HTTPS_PORT=443 OPENRESTY_BIND_HTTP_PORT=80 -# Deployment mode (informational) -DEPLOY_MODE=direct # Host UID/GID for bind mounts (Linux/macOS) HOST_UID=1000 @@ -248,6 +246,7 @@ MYSQL_ROOT_PASSWORD=secure_password MYSQL_DATABASE=guacamole_db MYSQL_USER=guacamole_user MYSQL_PASSWORD=db_password +BLOCKCHAIN_MYSQL_DATABASE=blockchain_services # Guacamole GUAC_ADMIN_USER=guacadmin @@ -257,9 +256,6 @@ AUTO_LOGOUT_ON_DISCONNECT=true # OpenResty CORS allowlist (comma-separated, optional) CORS_ALLOWED_ORIGINS=https://your-frontend.com,https://marketplace.com -# Wallet/Treasury CORS allowlist (blockchain-services) -WALLET_ALLOWED_ORIGINS=https://your-domain - # Lab Manager + Ops Worker LAB_MANAGER_TOKEN=your_lab_manager_token LAB_MANAGER_TOKEN_HEADER=X-Lab-Manager-Token @@ -273,12 +269,6 @@ TREASURY_TOKEN_REQUIRED=true SECURITY_ALLOW_PRIVATE_NETWORKS=true ADMIN_DASHBOARD_ALLOW_PRIVATE=true -# Treasury admin EIP-712 signature domain (optional overrides) -TREASURY_ADMIN_DOMAIN_NAME=DecentraLabsTreasuryAdmin -TREASURY_ADMIN_DOMAIN_VERSION=1 -TREASURY_ADMIN_DOMAIN_CHAIN_ID=11155111 -TREASURY_ADMIN_DOMAIN_VERIFYING_CONTRACT= - # Certbot / ACME (optional - for Let's Encrypt automation) CERTBOT_DOMAINS=yourdomain.com,www.yourdomain.com CERTBOT_EMAIL=you@example.com @@ -313,15 +303,16 @@ environment. All authentication endpoints live under the fixed `/auth` base path Optional Cloudflare Tunnel settings (filled automatically if you opt in during setup): ```env -ENABLE_CLOUDFLARE=true CLOUDFLARE_TUNNEL_TOKEN=your_cloudflare_tunnel_token_or_empty_for_quick_tunnel ``` +Runtime activation requires Compose profiles (`--profile cloudflare` or `--profile cloudflare-token`). #### Blockchain Service Configuration (`blockchain-services/.env`) ```env # Smart Contract CONTRACT_ADDRESS=0xYourSmartContractAddress +TREASURY_ADMIN_DOMAIN_VERIFYING_CONTRACT=0xYourSmartContractAddress # Network RPC URLs (with failover support) RPC_URL=https://1rpc.io/sepolia @@ -332,7 +323,7 @@ INSTITUTIONAL_WALLET_ADDRESS=0xYourWalletAddress INSTITUTIONAL_WALLET_PASSWORD=YourSecurePassword # Security -WALLET_ENCRYPTION_SALT=RandomString32CharsOrMore +WALLET_ALLOWED_ORIGINS=https://gateway.example.com ALLOWED_ORIGINS=https://your-frontend.com,https://marketplace.com MARKETPLACE_PUBLIC_KEY_URL=https://marketplace.com/.well-known/public-key.pem ``` diff --git a/blockchain-services b/blockchain-services index 9039142..28f5253 160000 --- a/blockchain-services +++ b/blockchain-services @@ -1 +1 @@ -Subproject commit 90391427525568911d261ab87ddce1400798b91a +Subproject commit 28f525362eba2d3dced00bb48491ba48d11a89ed diff --git a/setup.bat b/setup.bat index 0814029..d18ae19 100644 --- a/setup.bat +++ b/setup.bat @@ -133,8 +133,8 @@ if "!mysql_password!"=="" ( echo Generated database password: !mysql_password! ) -call :UpdateEnvBoth "MYSQL_ROOT_PASSWORD" "!mysql_root_password!" -call :UpdateEnvBoth "MYSQL_PASSWORD" "!mysql_password!" +call :UpdateEnv "%ROOT_ENV_FILE%" "MYSQL_ROOT_PASSWORD" "!mysql_root_password!" +call :UpdateEnv "%ROOT_ENV_FILE%" "MYSQL_PASSWORD" "!mysql_password!" set "db_credentials_changed=0" if not "!mysql_root_password!"=="!existing_mysql_root_password!" set "db_credentials_changed=1" @@ -214,6 +214,8 @@ echo This token protects /wallet, /treasury, /wallet-dashboard, and /treasury/ad set "access_token=" set /p "access_token=Wallet/Treasury token (leave empty for auto-generated): " set "access_token=!access_token: =!" +if "!access_token!"=="=" set "access_token=" +if /i "!access_token!"=="CHANGE_ME" set "access_token=" if "!access_token!"=="" ( set "access_token=acc_%RANDOM%%RANDOM%%RANDOM%" @@ -235,6 +237,8 @@ echo This token protects /lab-manager and /ops when accessed outside private net set "lab_manager_token=" set /p "lab_manager_token=Lab Manager token (leave empty for auto-generated): " set "lab_manager_token=!lab_manager_token: =!" +if "!lab_manager_token!"=="=" set "lab_manager_token=" +if /i "!lab_manager_token!"=="CHANGE_ME" set "lab_manager_token=" if "!lab_manager_token!"=="" ( set "lab_manager_token=lab_%RANDOM%%RANDOM%%RANDOM%" @@ -265,7 +269,6 @@ if /i "!domain!"=="localhost" ( call :UpdateEnv "%ROOT_ENV_FILE%" "OPENRESTY_BIND_ADDRESS" "127.0.0.1" call :UpdateEnv "%ROOT_ENV_FILE%" "OPENRESTY_BIND_HTTPS_PORT" "8443" call :UpdateEnv "%ROOT_ENV_FILE%" "OPENRESTY_BIND_HTTP_PORT" "8081" - call :UpdateEnv "%ROOT_ENV_FILE%" "DEPLOY_MODE" "local" set "https_port=8443" set "http_port=8081" echo * Server: https://localhost:8443 @@ -285,7 +288,6 @@ if /i "!domain!"=="localhost" ( if "!deploy_mode!"=="2" ( echo Router mode selected. - call :UpdateEnv "%ROOT_ENV_FILE%" "DEPLOY_MODE" "router" call :UpdateEnv "%ROOT_ENV_FILE%" "OPENRESTY_BIND_ADDRESS" "0.0.0.0" set /p "public_https=Public HTTPS port (the port clients use, e.g., 8043): " set "public_https=!public_https: =!" @@ -309,7 +311,6 @@ if /i "!domain!"=="localhost" ( echo * OpenResty will bind to 0.0.0.0:!local_https! and 0.0.0.0:!local_http! ) else ( echo Direct mode selected. - call :UpdateEnv "%ROOT_ENV_FILE%" "DEPLOY_MODE" "direct" call :UpdateEnv "%ROOT_ENV_FILE%" "OPENRESTY_BIND_ADDRESS" "0.0.0.0" set /p "direct_https=HTTPS port (default: 443): " set "direct_https=!direct_https: =!" @@ -338,7 +339,6 @@ if /i "!enable_cf!"=="y" set "cf_enabled=1" if /i "!enable_cf!"=="yes" set "cf_enabled=1" if "!cf_enabled!"=="1" ( - call :UpdateEnv "%ROOT_ENV_FILE%" "ENABLE_CLOUDFLARE" "true" set "cf_token=" set /p "cf_token=Cloudflare Tunnel token (leave empty to use a Quick Tunnel): " set "cf_token=!cf_token: =!" @@ -357,7 +357,6 @@ if "!cf_enabled!"=="1" ( set "http_port=80" ) ) else ( - call :UpdateEnv "%ROOT_ENV_FILE%" "ENABLE_CLOUDFLARE" "false" ) if "!cf_enabled!"=="1" ( if not "!cf_token!"=="" ( @@ -393,7 +392,7 @@ if /i "!domain!"=="localhost" ( set "wallet_origin=https://!domain!:!https_port_value!" ) ) -call :UpdateEnvBoth "WALLET_ALLOWED_ORIGINS" "!wallet_origin!" +call :UpdateEnv "%BLOCKCHAIN_ENV_FILE%" "WALLET_ALLOWED_ORIGINS" "!wallet_origin!" echo Configured WALLET_ALLOWED_ORIGINS to !wallet_origin! REM Build complete compose command: base + files + profile @@ -467,43 +466,43 @@ echo Blockchain Services Configuration echo ================================== echo. rem Provider registration enabled by default (non-interactive). -call :UpdateEnvBoth "FEATURES_PROVIDERS_REGISTRATION_ENABLED" "true" +call :UpdateEnv "%BLOCKCHAIN_ENV_FILE%" "FEATURES_PROVIDERS_REGISTRATION_ENABLED" "true" call :ReadEnvValue "%BLOCKCHAIN_ENV_FILE%" "CONTRACT_ADDRESS" contract_default if defined contract_default ( - call :UpdateEnvBoth "CONTRACT_ADDRESS" "!contract_default!" - call :UpdateEnvBoth "TREASURY_ADMIN_DOMAIN_VERIFYING_CONTRACT" "!contract_default!" + call :UpdateEnv "%BLOCKCHAIN_ENV_FILE%" "CONTRACT_ADDRESS" "!contract_default!" + call :UpdateEnv "%BLOCKCHAIN_ENV_FILE%" "TREASURY_ADMIN_DOMAIN_VERIFYING_CONTRACT" "!contract_default!" ) -call :ReadEnvValue "%ROOT_ENV_FILE%" "ETHEREUM_SEPOLIA_RPC_URL" sepolia_default +call :ReadEnvValue "%BLOCKCHAIN_ENV_FILE%" "ETHEREUM_SEPOLIA_RPC_URL" sepolia_default if not defined sepolia_default set "sepolia_default=https://ethereum-sepolia-rpc.publicnode.com,https://0xrpc.io/sep,https://ethereum-sepolia-public.nodies.app" set /p "sepolia_rpc=Sepolia RPC URLs (comma separated) [!sepolia_default!]: " if "!sepolia_rpc!"=="" set "sepolia_rpc=!sepolia_default!" if not "!sepolia_rpc!"=="" ( - call :UpdateEnvBoth "ETHEREUM_SEPOLIA_RPC_URL" "!sepolia_rpc!" + call :UpdateEnv "%BLOCKCHAIN_ENV_FILE%" "ETHEREUM_SEPOLIA_RPC_URL" "!sepolia_rpc!" ) -call :ReadEnvValue "%ROOT_ENV_FILE%" "ALLOWED_ORIGINS" origins_default +call :ReadEnvValue "%BLOCKCHAIN_ENV_FILE%" "ALLOWED_ORIGINS" origins_default if not defined origins_default set "origins_default=https://marketplace-decentralabs.vercel.app" set /p "allowed_origins=Allowed origins for CORS [!origins_default!]: " if "!allowed_origins!"=="" set "allowed_origins=!origins_default!" if not "!allowed_origins!"=="" ( - call :UpdateEnvBoth "ALLOWED_ORIGINS" "!allowed_origins!" + call :UpdateEnv "%BLOCKCHAIN_ENV_FILE%" "ALLOWED_ORIGINS" "!allowed_origins!" call :UpdateEnv "%ROOT_ENV_FILE%" "CORS_ALLOWED_ORIGINS" "!allowed_origins!" ) -call :ReadEnvValue "%ROOT_ENV_FILE%" "MARKETPLACE_PUBLIC_KEY_URL" mpk_default +call :ReadEnvValue "%BLOCKCHAIN_ENV_FILE%" "MARKETPLACE_PUBLIC_KEY_URL" mpk_default if not defined mpk_default set "mpk_default=https://marketplace-decentralabs.vercel.app/.well-known/public-key.pem" set /p "marketplace_pk=Marketplace public key URL [!mpk_default!]: " if "!marketplace_pk!"=="" set "marketplace_pk=!mpk_default!" if not "!marketplace_pk!"=="" ( - call :UpdateEnvBoth "MARKETPLACE_PUBLIC_KEY_URL" "!marketplace_pk!" + call :UpdateEnv "%BLOCKCHAIN_ENV_FILE%" "MARKETPLACE_PUBLIC_KEY_URL" "!marketplace_pk!" ) echo. echo Institutional Wallet Reminder echo ----------------------------- echo Wallet creation/import is handled inside the blockchain-services web console. -echo After creating the wallet, update these variables in both %ROOT_ENV_FILE% and %BLOCKCHAIN_ENV_FILE%: +echo After creating the wallet, update these variables in %BLOCKCHAIN_ENV_FILE%: echo * INSTITUTIONAL_WALLET_ADDRESS echo * INSTITUTIONAL_WALLET_PASSWORD echo Wallet data will be persisted in the blockchain-data\ directory. @@ -513,7 +512,7 @@ echo Next Steps echo ========== echo 1. Review and customize %ROOT_ENV_FILE% if needed echo 2. Ensure SSL certificates and RSA keys are present in certs\ -echo 3. Review blockchain settings in %ROOT_ENV_FILE% and %BLOCKCHAIN_ENV_FILE% +echo 3. Review blockchain contract/RPC/wallet settings in %BLOCKCHAIN_ENV_FILE% echo 4. Run: !compose_full! up -d if "!cf_enabled!"=="1" ( echo 5. Cloudflare tunnel: check '!compose_full! logs !cf_service!' for the public hostname ^(or your configured tunnel token domain^). diff --git a/setup.sh b/setup.sh index aeacf01..1169bd2 100644 --- a/setup.sh +++ b/setup.sh @@ -173,9 +173,10 @@ if [ -z "$mysql_password" ]; then fi fi -# Update passwords in env files -update_env_in_all "MYSQL_ROOT_PASSWORD" "$mysql_root_password" -update_env_in_all "MYSQL_PASSWORD" "$mysql_password" +# Update passwords only in gateway env (.env). Standalone blockchain-services +# uses BCHAIN_MYSQL_* keys in its own .env. +update_env_var "$ROOT_ENV_FILE" "MYSQL_ROOT_PASSWORD" "$mysql_root_password" +update_env_var "$ROOT_ENV_FILE" "MYSQL_PASSWORD" "$mysql_password" if [ "$mysql_root_password" != "$existing_mysql_root_password" ] || [ "$mysql_password" != "$existing_mysql_password" ]; then db_credentials_changed=true @@ -239,6 +240,11 @@ echo "============================" echo "This token protects /wallet, /treasury, /wallet-dashboard, and /treasury/admin/** behind OpenResty." read -p "Wallet/Treasury token (leave empty for auto-generated): " access_token access_token=$(echo "$access_token" | tr -d ' ') +case "$(printf '%s' "$access_token" | tr '[:upper:]' '[:lower:]')" in + ""|"="|changeme|change_me) + access_token="" + ;; +esac if [ -z "$access_token" ]; then access_token="acc_$(openssl rand -hex 16 2>/dev/null || echo ${RANDOM}${RANDOM}${RANDOM})" @@ -259,6 +265,11 @@ echo "========================" echo "This token protects /lab-manager and /ops when accessed outside private networks." read -p "Lab Manager token (leave empty for auto-generated): " lab_manager_token lab_manager_token=$(echo "$lab_manager_token" | tr -d ' ') +case "$(printf '%s' "$lab_manager_token" | tr '[:upper:]' '[:lower:]')" in + ""|"="|changeme|change_me) + lab_manager_token="" + ;; +esac if [ -z "$lab_manager_token" ]; then lab_manager_token="lab_$(openssl rand -hex 16 2>/dev/null || echo ${RANDOM}${RANDOM}${RANDOM})" @@ -290,7 +301,6 @@ if [ "$domain" == "localhost" ]; then update_env_var "$ROOT_ENV_FILE" "OPENRESTY_BIND_ADDRESS" "127.0.0.1" update_env_var "$ROOT_ENV_FILE" "OPENRESTY_BIND_HTTPS_PORT" "8443" update_env_var "$ROOT_ENV_FILE" "OPENRESTY_BIND_HTTP_PORT" "8081" - update_env_var "$ROOT_ENV_FILE" "DEPLOY_MODE" "local" echo " * Server: https://localhost:8443" echo " * Using development ports (8443/8081)" else @@ -309,7 +319,6 @@ else if [ "$deploy_mode" == "2" ]; then echo "Router mode selected." - update_env_var "$ROOT_ENV_FILE" "DEPLOY_MODE" "router" update_env_var "$ROOT_ENV_FILE" "OPENRESTY_BIND_ADDRESS" "0.0.0.0" read -p "Public HTTPS port (the port clients use; default: 443): " public_https public_https=$(echo "$public_https" | tr -d ' ') @@ -339,7 +348,6 @@ else echo " * OpenResty will bind to 0.0.0.0 ($local_https/$local_http)" else echo "Direct mode selected." - update_env_var "$ROOT_ENV_FILE" "DEPLOY_MODE" "direct" update_env_var "$ROOT_ENV_FILE" "OPENRESTY_BIND_ADDRESS" "0.0.0.0" read -p "HTTPS port (default: 443): " direct_https direct_https=$(echo "$direct_https" | tr -d ' ') @@ -367,7 +375,6 @@ read -p "Enable Cloudflare Tunnel to expose the gateway without opening inbound enable_cf=$(echo "$enable_cf" | tr -d ' ' | tr '[:upper:]' '[:lower:]') if [[ "$enable_cf" =~ ^(y|yes)$ ]]; then cf_enabled=true - update_env_var "$ROOT_ENV_FILE" "ENABLE_CLOUDFLARE" "true" read -p "Cloudflare Tunnel token (leave empty to use a Quick Tunnel): " cf_token cf_token=$(echo "$cf_token" | tr -d ' ') if [ -n "$cf_token" ]; then @@ -383,7 +390,6 @@ if [[ "$enable_cf" =~ ^(y|yes)$ ]]; then update_env_var "$ROOT_ENV_FILE" "OPENRESTY_BIND_HTTP_PORT" "80" fi else - update_env_var "$ROOT_ENV_FILE" "ENABLE_CLOUDFLARE" "false" fi echo @@ -409,7 +415,7 @@ else wallet_origin="https://${domain}:${https_port_value}" fi fi -update_env_in_all "WALLET_ALLOWED_ORIGINS" "$wallet_origin" +update_env_var "$BLOCKCHAIN_ENV_FILE" "WALLET_ALLOWED_ORIGINS" "$wallet_origin" echo "Configured WALLET_ALLOWED_ORIGINS to ${wallet_origin}" echo @@ -504,35 +510,35 @@ echo "=================================" echo # Provider registration enabled by default (non-interactive). -update_env_in_all "FEATURES_PROVIDERS_REGISTRATION_ENABLED" "true" +update_env_var "$BLOCKCHAIN_ENV_FILE" "FEATURES_PROVIDERS_REGISTRATION_ENABLED" "true" # Use CONTRACT_ADDRESS from blockchain-services/.env (no prompt) contract_default=$(get_env_default "CONTRACT_ADDRESS" "$BLOCKCHAIN_ENV_FILE") if [ -n "$contract_default" ]; then - update_env_in_all "CONTRACT_ADDRESS" "$contract_default" - update_env_in_all "TREASURY_ADMIN_DOMAIN_VERIFYING_CONTRACT" "$contract_default" + update_env_var "$BLOCKCHAIN_ENV_FILE" "CONTRACT_ADDRESS" "$contract_default" + update_env_var "$BLOCKCHAIN_ENV_FILE" "TREASURY_ADMIN_DOMAIN_VERIFYING_CONTRACT" "$contract_default" fi -sepolia_default=$(get_env_default "ETHEREUM_SEPOLIA_RPC_URL" "$ROOT_ENV_FILE") +sepolia_default=$(get_env_default "ETHEREUM_SEPOLIA_RPC_URL" "$BLOCKCHAIN_ENV_FILE") read -p "Comma-separated Sepolia RPC URLs [${sepolia_default:-https://ethereum-sepolia-rpc.publicnode.com,https://0xrpc.io/sep,https://ethereum-sepolia-public.nodies.app}]: " sepolia_rpc sepolia_rpc=${sepolia_rpc:-$sepolia_default} if [ -n "$sepolia_rpc" ]; then - update_env_in_all "ETHEREUM_SEPOLIA_RPC_URL" "$sepolia_rpc" + update_env_var "$BLOCKCHAIN_ENV_FILE" "ETHEREUM_SEPOLIA_RPC_URL" "$sepolia_rpc" fi -allowed_origins_default=$(get_env_default "ALLOWED_ORIGINS" "$ROOT_ENV_FILE") +allowed_origins_default=$(get_env_default "ALLOWED_ORIGINS" "$BLOCKCHAIN_ENV_FILE") read -p "Allowed origins for CORS [${allowed_origins_default:-https://marketplace-decentralabs.vercel.app}]: " allowed_origins allowed_origins=${allowed_origins:-${allowed_origins_default:-https://marketplace-decentralabs.vercel.app}} if [ -n "$allowed_origins" ]; then - update_env_in_all "ALLOWED_ORIGINS" "$allowed_origins" + update_env_var "$BLOCKCHAIN_ENV_FILE" "ALLOWED_ORIGINS" "$allowed_origins" update_env_var "$ROOT_ENV_FILE" "CORS_ALLOWED_ORIGINS" "$allowed_origins" fi -public_key_url_default=$(get_env_default "MARKETPLACE_PUBLIC_KEY_URL" "$ROOT_ENV_FILE") +public_key_url_default=$(get_env_default "MARKETPLACE_PUBLIC_KEY_URL" "$BLOCKCHAIN_ENV_FILE") read -p "Marketplace public key URL [${public_key_url_default:-https://marketplace-decentralabs.vercel.app/.well-known/public-key.pem}]: " marketplace_pk marketplace_pk=${marketplace_pk:-$public_key_url_default} if [ -n "$marketplace_pk" ]; then - update_env_in_all "MARKETPLACE_PUBLIC_KEY_URL" "$marketplace_pk" + update_env_var "$BLOCKCHAIN_ENV_FILE" "MARKETPLACE_PUBLIC_KEY_URL" "$marketplace_pk" fi if [ "$cf_enabled" = true ]; then @@ -569,7 +575,6 @@ echo "This script does not create wallets automatically." echo "After the stack is running, create or import the institutional wallet" echo "using the blockchain-services web console (or the /wallet API) and then" echo "update INSTITUTIONAL_WALLET_ADDRESS / PASSWORD in:" -echo " - .env" echo " - blockchain-services/.env" echo "Wallet data is stored in ./blockchain-data (already created)." @@ -578,7 +583,7 @@ echo "Next Steps" echo "==========" echo "1. Review and customize .env file if needed" echo "2. Ensure SSL certificates are in place" -echo "3. Configure blockchain settings in .env (CONTRACT_ADDRESS, ETHEREUM_*_RPC_URL, INSTITUTIONAL_WALLET_*)" +echo "3. Configure blockchain settings in blockchain-services/.env (CONTRACT_ADDRESS, ETHEREUM_*_RPC_URL, INSTITUTIONAL_WALLET_*)" echo "4. Run: $compose_full up -d" if [ "$cf_enabled" = true ]; then echo "5. Cloudflare tunnel: check '$compose_full logs ${cf_service:-cloudflared}' for the public hostname (or your configured tunnel token domain)." @@ -613,7 +618,7 @@ if [[ "$start_services" =~ ^[Nn]$ ]] || [[ "$start_services" =~ ^[Nn][Oo]$ ]]; t echo "Configuration complete!" echo echo "Next steps:" -echo "1. Configure blockchain settings in .env (CONTRACT_ADDRESS, WALLET_ADDRESS, INSTITUTIONAL_WALLET_*)" +echo "1. Configure blockchain settings in blockchain-services/.env (CONTRACT_ADDRESS, WALLET_ADDRESS, INSTITUTIONAL_WALLET_*)" echo "2. Run: $compose_full up -d" echo "3. Access your services" if [ "$cf_enabled" = true ]; then From bf729047ca704f003e29bcbdca2ad7d94bc2c38e Mon Sep 17 00:00:00 2001 From: Ravenink Date: Sat, 14 Feb 2026 16:27:14 +0100 Subject: [PATCH 12/16] Fixed setup --- setup.bat | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.bat b/setup.bat index d18ae19..d91564f 100644 --- a/setup.bat +++ b/setup.bat @@ -356,7 +356,6 @@ if "!cf_enabled!"=="1" ( set "https_port=443" set "http_port=80" ) -) else ( ) if "!cf_enabled!"=="1" ( if not "!cf_token!"=="" ( From 1a0300b1615f268694d891b526f1f7d5c579d317 Mon Sep 17 00:00:00 2001 From: Ravenink Date: Sat, 14 Feb 2026 17:45:05 +0100 Subject: [PATCH 13/16] Refactor token handling and configuration for wallet and treasury endpoints --- .env.example | 4 -- setup.bat | 36 +++++----- web/assets/css/lab-manager.css | 29 ++++++++ web/assets/js/auth-token-fetch.js | 41 ++++++++--- web/assets/js/auth-token-handler.js | 105 +++++++++++++++++++++++---- web/assets/js/lab-manager.js | 108 +++++++++++++++++++++++++++- 6 files changed, 278 insertions(+), 45 deletions(-) diff --git a/.env.example b/.env.example index d6d641a..050fc4d 100644 --- a/.env.example +++ b/.env.example @@ -69,10 +69,6 @@ LAB_MANAGER_TOKEN=CHANGE_ME LAB_MANAGER_TOKEN_HEADER=X-Lab-Manager-Token LAB_MANAGER_TOKEN_COOKIE=lab_manager_token -# ============================================================================= -# BLOCKCHAIN-SERVICES REMOTE ACCESS -# ============================================================================= - # Wallet/Treasury access token # Used by /wallet, /treasury, /wallet-dashboard and /treasury/admin/** (sent by OpenResty). TREASURY_TOKEN=CHANGE_ME diff --git a/setup.bat b/setup.bat index d91564f..3d8163b 100644 --- a/setup.bat +++ b/setup.bat @@ -67,7 +67,7 @@ REM Check if .env already exists if exist "%ROOT_ENV_FILE%" ( echo .env file already exists! set /p "overwrite=Do you want to overwrite it? (y/N): " - set "overwrite=!overwrite: =!" + if defined overwrite set "overwrite=!overwrite: =!" if /i "!overwrite!"=="y" ( copy ".env.example" "%ROOT_ENV_FILE%" >nul echo Overwritten .env file from template @@ -114,7 +114,7 @@ if "!mysql_root_password!"=="" ( ) if "!mysql_root_password!"=="" ( set mysql_root_password=R00t_P@ss_%RANDOM%_%TIME:~9% - set mysql_root_password=!mysql_root_password: =! + if defined mysql_root_password set "mysql_root_password=!mysql_root_password: =!" echo Generated root password: !mysql_root_password! ) @@ -129,7 +129,7 @@ if "!mysql_password!"=="" ( ) if "!mysql_password!"=="" ( set mysql_password=Gu@c_%RANDOM%_%TIME:~9% - set mysql_password=!mysql_password: =! + if defined mysql_password set "mysql_password=!mysql_password: =!" echo Generated database password: !mysql_password! ) @@ -147,7 +147,7 @@ if "!db_credentials_changed!"=="1" if defined mysql_volume_name ( echo Detected existing MySQL volume: !mysql_volume_name! echo Database credentials changed in .env, so startup can fail with Access denied ^(1045^). set /p "reset_mysql_input=Reset MySQL volume now to apply new credentials? This removes MySQL data. (y/N): " - set "reset_mysql_input=!reset_mysql_input: =!" + if defined reset_mysql_input set "reset_mysql_input=!reset_mysql_input: =!" if /i "!reset_mysql_input!"=="y" ( set "reset_mysql_volume=1" echo MySQL volume will be reset before startup. @@ -179,7 +179,7 @@ set /p "guac_admin_pass=Guacamole admin password (leave empty for auto-generated if "!guac_admin_user!"=="" set "guac_admin_user=guacadmin" if "!guac_admin_pass!"=="" ( set "guac_admin_pass=Guac_%RANDOM%_%TIME:~9%" - set "guac_admin_pass=!guac_admin_pass: =!" + if defined guac_admin_pass set "guac_admin_pass=!guac_admin_pass: =!" echo Generated Guacamole admin password: !guac_admin_pass! ) if /i "!guac_admin_pass!"=="guacadmin" ( @@ -213,7 +213,7 @@ echo ============================ echo This token protects /wallet, /treasury, /wallet-dashboard, and /treasury/admin/** behind OpenResty. set "access_token=" set /p "access_token=Wallet/Treasury token (leave empty for auto-generated): " -set "access_token=!access_token: =!" +if defined access_token set "access_token=!access_token: =!" if "!access_token!"=="=" set "access_token=" if /i "!access_token!"=="CHANGE_ME" set "access_token=" @@ -236,7 +236,7 @@ echo ======================== echo This token protects /lab-manager and /ops when accessed outside private networks. set "lab_manager_token=" set /p "lab_manager_token=Lab Manager token (leave empty for auto-generated): " -set "lab_manager_token=!lab_manager_token: =!" +if defined lab_manager_token set "lab_manager_token=!lab_manager_token: =!" if "!lab_manager_token!"=="=" set "lab_manager_token=" if /i "!lab_manager_token!"=="CHANGE_ME" set "lab_manager_token=" @@ -284,22 +284,22 @@ if /i "!domain!"=="localhost" ( echo 1^) Direct - Gateway has a public IP ^(ports bound directly^) echo 2^) Router - Behind NAT/router with port forwarding ^(e.g., router:8043 -^> host:443^) set /p "deploy_mode=Choose [1/2] (default: 1): " - set "deploy_mode=!deploy_mode: =!" + if defined deploy_mode set "deploy_mode=!deploy_mode: =!" if "!deploy_mode!"=="2" ( echo Router mode selected. call :UpdateEnv "%ROOT_ENV_FILE%" "OPENRESTY_BIND_ADDRESS" "0.0.0.0" set /p "public_https=Public HTTPS port (the port clients use, e.g., 8043): " - set "public_https=!public_https: =!" + if defined public_https set "public_https=!public_https: =!" if "!public_https!"=="" set "public_https=443" set /p "local_https=Local HTTPS port to bind on this host (default: 443): " - set "local_https=!local_https: =!" + if defined local_https set "local_https=!local_https: =!" if "!local_https!"=="" set "local_https=443" set /p "public_http=Public HTTP port (default: 80): " - set "public_http=!public_http: =!" + if defined public_http set "public_http=!public_http: =!" if "!public_http!"=="" set "public_http=80" set /p "local_http=Local HTTP port to bind on this host (default: 80): " - set "local_http=!local_http: =!" + if defined local_http set "local_http=!local_http: =!" if "!local_http!"=="" set "local_http=80" call :UpdateEnv "%ROOT_ENV_FILE%" "HTTPS_PORT" "!public_https!" call :UpdateEnv "%ROOT_ENV_FILE%" "HTTP_PORT" "!public_http!" @@ -313,10 +313,10 @@ if /i "!domain!"=="localhost" ( echo Direct mode selected. call :UpdateEnv "%ROOT_ENV_FILE%" "OPENRESTY_BIND_ADDRESS" "0.0.0.0" set /p "direct_https=HTTPS port (default: 443): " - set "direct_https=!direct_https: =!" + if defined direct_https set "direct_https=!direct_https: =!" if "!direct_https!"=="" set "direct_https=443" set /p "direct_http=HTTP port (default: 80): " - set "direct_http=!direct_http: =!" + if defined direct_http set "direct_http=!direct_http: =!" if "!direct_http!"=="" set "direct_http=80" call :UpdateEnv "%ROOT_ENV_FILE%" "HTTPS_PORT" "!direct_https!" call :UpdateEnv "%ROOT_ENV_FILE%" "HTTP_PORT" "!direct_http!" @@ -334,14 +334,14 @@ echo Remote Access (Cloudflare Tunnel) echo ================================= set "enable_cf=" set /p "enable_cf=Enable Cloudflare Tunnel to expose the gateway without opening inbound ports? (y/N): " -set "enable_cf=!enable_cf: =!" +if defined enable_cf set "enable_cf=!enable_cf: =!" if /i "!enable_cf!"=="y" set "cf_enabled=1" if /i "!enable_cf!"=="yes" set "cf_enabled=1" if "!cf_enabled!"=="1" ( set "cf_token=" set /p "cf_token=Cloudflare Tunnel token (leave empty to use a Quick Tunnel): " - set "cf_token=!cf_token: =!" + if defined cf_token set "cf_token=!cf_token: =!" if not "!cf_token!"=="" ( call :UpdateEnv "%ROOT_ENV_FILE%" "CLOUDFLARE_TUNNEL_TOKEN" "!cf_token!" ) else ( @@ -442,10 +442,10 @@ echo Certbot (Let's Encrypt) - optional automation echo ============================================ set "cb_domains=" set /p "cb_domains=Domains for TLS (comma-separated, leave empty to skip ACME): " -set "cb_domains=%cb_domains: =%" +if defined cb_domains set "cb_domains=!cb_domains: =!" set "cb_email=" set /p "cb_email=Email for ACME (leave empty to skip ACME): " -set "cb_email=%cb_email: =%" +if defined cb_email set "cb_email=!cb_email: =!" if not "%cb_domains%"=="" if not "%cb_email%"=="" ( call :UpdateEnv "%ROOT_ENV_FILE%" "CERTBOT_DOMAINS" "%cb_domains%" call :UpdateEnv "%ROOT_ENV_FILE%" "CERTBOT_EMAIL" "%cb_email%" diff --git a/web/assets/css/lab-manager.css b/web/assets/css/lab-manager.css index c2fe3e3..fab2983 100644 --- a/web/assets/css/lab-manager.css +++ b/web/assets/css/lab-manager.css @@ -106,6 +106,35 @@ body { } .pill.soft { background: var(--pill-soft); } +.pill.token-required-action { + cursor: pointer; + border-color: var(--warning); + color: var(--warning); + box-shadow: 0 0 0 0 rgba(246, 195, 68, 0.35); + animation: token-pill-pulse 2.2s ease-in-out infinite; + transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; +} + +.pill.token-required-action:hover, +.pill.token-required-action:focus-visible { + transform: translateY(-1px); + border-color: #ffd26c; + box-shadow: 0 0 0 4px rgba(246, 195, 68, 0.18); + outline: none; +} + +@keyframes token-pill-pulse { + 0% { + box-shadow: 0 0 0 0 rgba(246, 195, 68, 0.34); + } + 70% { + box-shadow: 0 0 0 8px rgba(246, 195, 68, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(246, 195, 68, 0); + } +} + .form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); diff --git a/web/assets/js/auth-token-fetch.js b/web/assets/js/auth-token-fetch.js index 56f7adf..dfa9f36 100644 --- a/web/assets/js/auth-token-fetch.js +++ b/web/assets/js/auth-token-fetch.js @@ -11,6 +11,18 @@ key: 'dlabs_lab_manager_token', header: 'X-Lab-Manager-Token' }, + '/ops': { + key: 'dlabs_lab_manager_token', + header: 'X-Lab-Manager-Token' + }, + '/wallet': { + key: 'dlabs_treasury_token', + header: 'X-Access-Token' + }, + '/treasury': { + key: 'dlabs_treasury_token', + header: 'X-Access-Token' + }, '/wallet-dashboard': { key: 'dlabs_treasury_token', header: 'X-Access-Token' @@ -18,22 +30,35 @@ '/institution-config': { key: 'dlabs_treasury_token', header: 'X-Access-Token' - }, - '/ops': { - key: 'dlabs_lab_manager_token', - header: 'X-Lab-Manager-Token' } }; + function isUsableToken(value) { + if (typeof value !== 'string') { + return false; + } + const token = value.trim(); + if (!token || token === '=') { + return false; + } + const lower = token.toLowerCase(); + return lower !== 'change_me' && lower !== 'changeme'; + } + function getTokenConfigForPath(path) { const normalizedPath = path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path; + let bestMatch = null; + let bestMatchLength = -1; for (const [prefix, config] of Object.entries(TOKEN_CONFIG)) { const normalizedPrefix = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix; if (normalizedPath.startsWith(normalizedPrefix)) { - return config; + if (normalizedPrefix.length > bestMatchLength) { + bestMatch = config; + bestMatchLength = normalizedPrefix.length; + } } } - return null; + return bestMatch; } function getRequestPath(url) { @@ -70,7 +95,7 @@ const config = getTokenConfigForPath(window.location.pathname); if (config) { const stored = localStorage.getItem(config.key); - console.log('[AuthToken] Token in localStorage:', stored ? 'YES' : 'NO'); + console.log('[AuthToken] Token in localStorage:', isUsableToken(stored) ? 'YES' : 'NO'); } } } catch (e) { @@ -84,7 +109,7 @@ const config = getTokenConfigForPath(requestPath) || getTokenConfigForPath(window.location.pathname); if (config) { const storedToken = localStorage.getItem(config.key); - if (storedToken) { + if (isUsableToken(storedToken)) { // Firefox compatibility: Handle both Headers instances and plain objects if (!options.headers) { options.headers = {}; diff --git a/web/assets/js/auth-token-handler.js b/web/assets/js/auth-token-handler.js index 8d21094..7cc846d 100644 --- a/web/assets/js/auth-token-handler.js +++ b/web/assets/js/auth-token-handler.js @@ -21,6 +21,27 @@ title: 'Lab Manager Access Token', description: 'This area requires a Lab Manager access token.' }, + '/ops': { + key: TOKEN_STORAGE.LAB_MANAGER, + header: 'X-Lab-Manager-Token', + cookie: 'lab_manager_token', + title: 'Lab Manager Access Token', + description: 'This area requires a Lab Manager access token.' + }, + '/wallet': { + key: TOKEN_STORAGE.TREASURY, + header: 'X-Access-Token', + cookie: 'access_token', + title: 'Wallet/Treasury Access Token', + description: 'This area requires a Wallet/Treasury access token.' + }, + '/treasury': { + key: TOKEN_STORAGE.TREASURY, + header: 'X-Access-Token', + cookie: 'access_token', + title: 'Wallet/Treasury Access Token', + description: 'This area requires a Wallet/Treasury access token.' + }, '/wallet-dashboard': { key: TOKEN_STORAGE.TREASURY, header: 'X-Access-Token', @@ -34,16 +55,48 @@ cookie: 'access_token', title: 'Wallet/Treasury Access Token', description: 'This area requires a Wallet/Treasury access token.' - }, - '/ops': { - key: TOKEN_STORAGE.LAB_MANAGER, - header: 'X-Lab-Manager-Token', - cookie: 'lab_manager_token', - title: 'Lab Manager Access Token', - description: 'This area requires a Lab Manager access token.' } }; + function isUsableToken(value) { + if (typeof value !== 'string') { + return false; + } + const token = value.trim(); + if (!token || token === '=') { + return false; + } + const lower = token.toLowerCase(); + return lower !== 'change_me' && lower !== 'changeme'; + } + + function isPrivateOrLoopbackHost(hostname) { + if (!hostname) { + return false; + } + const host = hostname.toLowerCase(); + if (host === 'localhost' || host === '::1' || host === '[::1]') { + return true; + } + if (/^127\./.test(host)) { + return true; + } + if (/^10\./.test(host)) { + return true; + } + if (/^192\.168\./.test(host)) { + return true; + } + const match172 = host.match(/^172\.(\d{1,3})\./); + if (match172) { + const octet = Number(match172[1]); + if (octet >= 16 && octet <= 31) { + return true; + } + } + return false; + } + // Create token modal HTML function createTokenModal() { if (document.getElementById('authTokenModal')) { @@ -146,7 +199,7 @@ // Check for stored token const storedToken = localStorage.getItem(config.key); - if (storedToken) { + if (isUsableToken(storedToken)) { input.value = storedToken; } @@ -199,14 +252,19 @@ function getTokenConfigForPath(path) { // Normalize path by removing trailing slash for comparison const normalizedPath = path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path; - + + let bestMatch = null; + let bestMatchLength = -1; for (const [prefix, config] of Object.entries(TOKEN_CONFIG)) { const normalizedPrefix = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix; if (normalizedPath.startsWith(normalizedPrefix)) { - return config; + if (normalizedPrefix.length > bestMatchLength) { + bestMatch = config; + bestMatchLength = normalizedPrefix.length; + } } } - return null; + return bestMatch; } function getRequestPath(url) { @@ -232,6 +290,19 @@ options.headers[name] = value; } + function shouldBypassTokenPrompt(config) { + if (!config) { + return false; + } + // If this client is already in a private/loopback context and no token + // is stored, let the request fail/succeed naturally without forcing modal. + if (!isPrivateOrLoopbackHost(window.location.hostname)) { + return false; + } + const storedToken = localStorage.getItem(config.key); + return !isUsableToken(storedToken); + } + // Enhanced fetch wrapper function createAuthenticatedFetch() { const originalFetch = window.fetch; @@ -245,7 +316,7 @@ // Add stored token if available if (config) { const storedToken = localStorage.getItem(config.key); - if (storedToken) { + if (isUsableToken(storedToken)) { setRequestHeader(options, config.header, storedToken); } } @@ -255,6 +326,9 @@ .then(response => { // If 401 and we have config for this path if (response.status === 401 && config) { + if (shouldBypassTokenPrompt(config)) { + return response; + } return new Promise((resolve, reject) => { // Show modal and retry with token showTokenModal(config, (token) => { @@ -296,7 +370,12 @@ // Check if we have a stored token const storedToken = localStorage.getItem(config.key); - if (!storedToken) { + if (!isUsableToken(storedToken)) { + // If access rules already allow this client (localhost/private host), + // don't force token entry from the UI; let server-side ACL decide. + if (isPrivateOrLoopbackHost(window.location.hostname)) { + return; + } // Prevent navigation and show token modal e.preventDefault(); showTokenModal(config, (token) => { diff --git a/web/assets/js/lab-manager.js b/web/assets/js/lab-manager.js index b34eb88..110eab9 100644 --- a/web/assets/js/lab-manager.js +++ b/web/assets/js/lab-manager.js @@ -7,6 +7,24 @@ function escapeHtml(str) { } document.addEventListener('DOMContentLoaded', () => { + const TREASURY_TOKEN_STORAGE_KEY = 'dlabs_treasury_token'; + + function isUsableToken(value) { + if (typeof value !== 'string') return false; + const token = value.trim(); + if (!token || token === '=') return false; + const lower = token.toLowerCase(); + return lower !== 'change_me' && lower !== 'changeme'; + } + + function hasTreasuryToken() { + try { + return isUsableToken(localStorage.getItem(TREASURY_TOKEN_STORAGE_KEY)); + } catch (_) { + return false; + } + } + const driverEl = $('#driver'); const enabledEl = $('#enabled'); const fromEl = $('#from'); @@ -35,6 +53,7 @@ document.addEventListener('DOMContentLoaded', () => { const graphClientSecretEl = $('#graphClientSecret'); const graphFromEl = $('#graphFrom'); const driverSummary = $('#driverSummary'); + const configStatusEl = $('#configStatus'); // Auth/health elements const authStatusPill = $('#authStatusPill'); @@ -55,7 +74,16 @@ document.addEventListener('DOMContentLoaded', () => { $('#saveConfigBtn').addEventListener('click', saveConfig); $('#btnTestEmail').addEventListener('click', sendTestEmail); driverEl.addEventListener('change', toggleSections); - configureBtn.addEventListener('click', openModal); + configureBtn.addEventListener('click', () => { + if (!hasTreasuryToken()) { + promptTreasuryToken(() => { + loadConfig(); + openModal(); + }); + return; + } + openModal(); + }); closeModalBtn.addEventListener('click', closeModal); cancelModalBtn.addEventListener('click', closeModal); if (authRefreshBtn) { @@ -65,6 +93,7 @@ document.addEventListener('DOMContentLoaded', () => { loadConfig(); loadAuthHealth(); checkOpsAvailability(); + updateTreasuryStatusAction(); // Lab Station ops state const hostInput = $('#hostInput'); @@ -111,7 +140,14 @@ document.addEventListener('DOMContentLoaded', () => { } function loadConfig() { + if (!hasTreasuryToken()) { + setStatus('Treasury token not configured'); + updateTreasuryStatusAction(); + return; + } + setStatus('Loading...'); + updateTreasuryStatusAction(); fetch('/treasury/admin/notifications', { credentials: 'include' }) .then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}`); @@ -141,16 +177,23 @@ document.addEventListener('DOMContentLoaded', () => { toggleSections(); updateDriverSummary(); setStatus('Loaded'); + updateTreasuryStatusAction(); showToast('Configuration loaded', 'success'); }) .catch(err => { console.error(err); setStatus('Error'); + updateTreasuryStatusAction(); showToast('Cannot load config (check admin access)', 'error'); }); } function saveConfig() { + if (!hasTreasuryToken()) { + showToast('Wallet/Treasury token required for notifications', 'error'); + return; + } + const payload = { enabled: enabledEl.checked, driver: driverEl.value, @@ -250,10 +293,71 @@ document.addEventListener('DOMContentLoaded', () => { } function setStatus(text) { - $('#configStatus').textContent = text; + if (configStatusEl) { + configStatusEl.textContent = text; + } + } + + function promptTreasuryToken(onSuccess) { + const handler = window.AuthTokenHandler; + if (!handler || typeof handler.showTokenModal !== 'function') { + showToast('Token modal unavailable on this page', 'error'); + return; + } + + let config = null; + if (typeof handler.getTokenConfigForPath === 'function') { + config = handler.getTokenConfigForPath('/treasury/admin/notifications'); + } + if (!config) { + config = { + key: TREASURY_TOKEN_STORAGE_KEY, + header: 'X-Access-Token', + cookie: 'access_token', + title: 'Wallet/Treasury Access Token', + description: 'This area requires a Wallet/Treasury access token.' + }; + } + + handler.showTokenModal(config, () => { + if (typeof onSuccess === 'function') { + onSuccess(); + } + }); + } + + function updateTreasuryStatusAction() { + if (!configStatusEl) { + return; + } + const needsToken = !hasTreasuryToken(); + configStatusEl.classList.toggle('token-required-action', needsToken); + configStatusEl.title = needsToken ? 'Click to enter Wallet/Treasury token' : ''; + configStatusEl.setAttribute('aria-disabled', needsToken ? 'false' : 'true'); + } + + if (configStatusEl) { + configStatusEl.setAttribute('role', 'button'); + configStatusEl.tabIndex = 0; + configStatusEl.addEventListener('click', () => { + if (!hasTreasuryToken()) { + promptTreasuryToken(() => loadConfig()); + } + }); + configStatusEl.addEventListener('keydown', (e) => { + if ((e.key === 'Enter' || e.key === ' ') && !hasTreasuryToken()) { + e.preventDefault(); + promptTreasuryToken(() => loadConfig()); + } + }); } function sendTestEmail() { + if (!hasTreasuryToken()) { + showToast('Wallet/Treasury token required for notifications', 'error'); + return; + } + fetch('/treasury/admin/notifications/test', { method: 'POST', credentials: 'include' From 1566e952ef17fd587ab50acab2a65de8976fe214 Mon Sep 17 00:00:00 2001 From: Ravenink Date: Sat, 14 Feb 2026 17:48:29 +0100 Subject: [PATCH 14/16] Updates ref --- blockchain-services | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blockchain-services b/blockchain-services index 28f5253..30932a3 160000 --- a/blockchain-services +++ b/blockchain-services @@ -1 +1 @@ -Subproject commit 28f525362eba2d3dced00bb48491ba48d11a89ed +Subproject commit 30932a3543fcea30a8d5ea4108b2dab65ffca631 From 36cc5d791580aae9d2e51077a6baea30a44af242 Mon Sep 17 00:00:00 2001 From: Ravenink Date: Mon, 16 Feb 2026 13:37:45 +0100 Subject: [PATCH 15/16] Updates reference --- blockchain-services | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blockchain-services b/blockchain-services index 30932a3..76b113a 160000 --- a/blockchain-services +++ b/blockchain-services @@ -1 +1 @@ -Subproject commit 30932a3543fcea30a8d5ea4108b2dab65ffca631 +Subproject commit 76b113ae0bc094a31a06cfef5e064d95ee462d4b From ec3d48aaca2e7892d6362a4d132fea70f4733306 Mon Sep 17 00:00:00 2001 From: Ravenink Date: Mon, 16 Feb 2026 21:23:48 +0100 Subject: [PATCH 16/16] Refactored to support lite and full version installations --- .env.example | 6 + docker-compose.yml | 1 + openresty/init-ssl.sh | 169 +++++++++++++++--- openresty/lab_access.conf | 93 ++++++++++ openresty/lua/admin_access.lua | 13 ++ openresty/lua/init.lua | 31 ++++ openresty/lua/treasury_access.lua | 13 ++ openresty/nginx.conf | 1 + openresty/tests/README.md | 3 + .../tests/run-jwt-key-sync-integration.ps1 | 105 +++++++++++ .../tests/run-jwt-key-sync-integration.sh | 95 ++++++++++ openresty/tests/run-lua-tests.ps1 | 31 ++-- openresty/tests/run-lua-tests.sh | 8 +- openresty/tests/unit/admin_access_spec.lua | 14 +- openresty/tests/unit/treasury_access_spec.lua | 15 +- setup.bat | 24 +++ setup.sh | 24 +++ web/assets/js/app.js | 28 +++ web/index.html | 2 +- 19 files changed, 637 insertions(+), 39 deletions(-) create mode 100644 openresty/tests/run-jwt-key-sync-integration.ps1 create mode 100644 openresty/tests/run-jwt-key-sync-integration.sh diff --git a/.env.example b/.env.example index 050fc4d..44ede7d 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,12 @@ SERVER_NAME=localhost HTTPS_PORT=443 HTTP_PORT=80 +# Optional JWT issuer override expected by OpenResty (must match token iss exactly). +# Leave empty to auto-build as https://[:]/auth. +# If set to an external Full issuer (Lite mode), the gateway auto-syncs the JWT public key +# from https:///.well-known/public-key.pem. +# Lite mode also disables local auth/treasury/intents endpoints in this gateway instance. +ISSUER= # OpenResty bind address (127.0.0.1 for local-only, 0.0.0.0 for public) OPENRESTY_BIND_ADDRESS=127.0.0.1 diff --git a/docker-compose.yml b/docker-compose.yml index 2b6c863..b058179 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -82,6 +82,7 @@ services: - SERVER_NAME=${SERVER_NAME} - HTTPS_PORT=${HTTPS_PORT} - HTTP_PORT=${HTTP_PORT} + - ISSUER=${ISSUER} - MYSQL_DATABASE=${MYSQL_DATABASE} - MYSQL_USER=${MYSQL_USER} - MYSQL_PASSWORD=${MYSQL_PASSWORD} diff --git a/openresty/init-ssl.sh b/openresty/init-ssl.sh index 01dfac9..a739a1e 100644 --- a/openresty/init-ssl.sh +++ b/openresty/init-ssl.sh @@ -22,6 +22,73 @@ echo "Private Key: $KEY_FILE" # Create SSL directory if it doesn't exist mkdir -p "$SSL_DIR" +trim() { + echo "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' +} + +build_local_issuer() { + local_name="$(trim "${SERVER_NAME:-localhost}")" + local_port="$(trim "${HTTPS_PORT:-443}")" + if [ -z "$local_name" ]; then + local_name="localhost" + fi + if [ -n "$local_port" ] && [ "$local_port" != "443" ]; then + echo "https://${local_name}:${local_port}/auth" + else + echo "https://${local_name}/auth" + fi +} + +build_key_url_from_issuer() { + issuer_raw="$(trim "$1")" + issuer_no_slash="$(echo "$issuer_raw" | sed 's:/*$::')" + origin="$(echo "$issuer_no_slash" | sed -n 's#^\(https\?://[^/]*\).*$#\1#p')" + if [ -z "$origin" ]; then + return 1 + fi + echo "${origin}/.well-known/public-key.pem" +} + +sync_jwt_public_key_from_issuer() { + target_issuer="$1" + key_url="$(build_key_url_from_issuer "$target_issuer" 2>/dev/null || true)" + if [ -z "$key_url" ]; then + echo "Invalid issuer URL for key sync: '$target_issuer'" + return 1 + fi + + tmp_key="${JWT_PUBLIC_KEY}.download" + echo "Syncing JWT public key from: $key_url" + if ! curl -fsSL --connect-timeout 10 --max-time 20 "$key_url" -o "$tmp_key"; then + echo "Failed to download JWT public key from $key_url" + rm -f "$tmp_key" + return 1 + fi + + if ! grep -q "BEGIN PUBLIC KEY" "$tmp_key"; then + echo "Downloaded file is not a PEM public key" + rm -f "$tmp_key" + return 1 + fi + + if ! openssl pkey -pubin -in "$tmp_key" -noout >/dev/null 2>&1; then + echo "Downloaded PEM public key is invalid" + rm -f "$tmp_key" + return 1 + fi + + if [ -f "$JWT_PUBLIC_KEY" ] && cmp -s "$tmp_key" "$JWT_PUBLIC_KEY"; then + rm -f "$tmp_key" + echo "JWT public key already up-to-date" + return 0 + fi + + mv "$tmp_key" "$JWT_PUBLIC_KEY" + chmod 644 "$JWT_PUBLIC_KEY" + echo "JWT public key updated from issuer" + return 10 +} + generate_self_signed() { echo "SSL certificates missing or expiring - generating self-signed certificates for development" mkdir -p "$TEMP_SSL_DIR" @@ -66,11 +133,11 @@ EOF chmod 644 "$CERT_FILE" chmod 600 "$KEY_FILE" date +%s > "$SELF_SIGNED_MARKER" 2>/dev/null || true - echo "โœ” Self-signed SSL certificates generated successfully" + echo "Self-signed SSL certificates generated successfully" echo " Valid for: localhost, *.localhost, 127.0.0.1" echo " WARNING: These are self-signed certificates for development only!" else - echo "โœ– Failed to generate certificates - this will cause nginx startup failure" + echo "Failed to generate certificates - this will cause nginx startup failure" exit 1 fi } @@ -110,7 +177,7 @@ renew_acme_cert() { # Try renewal first (faster if cert exists) if certbot renew --webroot -w "$CERTBOT_WEBROOT" --quiet --deploy-hook "echo 'Certificate renewed successfully'" 2>/dev/null; then - echo "โœ” ACME certificate renewed via certbot renew" + echo "ACME certificate renewed via certbot renew" return 0 fi @@ -131,11 +198,11 @@ renew_acme_cert() { cp "/etc/letsencrypt/live/$primary_domain/privkey.pem" "$KEY_FILE" chmod 644 "$CERT_FILE" chmod 600 "$KEY_FILE" - echo "โœ” ACME certificate obtained and installed" + echo "ACME certificate obtained and installed" return 0 fi - echo "โœ– ACME certificate renewal failed" + echo "ACME certificate renewal failed" return 1 } @@ -151,7 +218,7 @@ if [ ! -f "$CERT_FILE" ] || [ ! -f "$KEY_FILE" ]; then generate_self_signed fi else - echo "โœ” SSL certificates found" + echo "SSL certificates found" days_left=$(get_cert_days_until_expiry) echo " Days until expiry: $days_left" @@ -178,7 +245,7 @@ else else # User-provided cert: warn but don't replace if [ "$days_left" -lt 30 ]; then - echo " โš  Warning: User-provided certificate expires in $days_left days!" + echo " Warning: User-provided certificate expires in $days_left days!" echo " Please renew manually or configure CERTBOT_DOMAINS and CERTBOT_EMAIL" else echo " Status: Valid user-provided certificate" @@ -186,23 +253,61 @@ else fi fi -# Wait for JWT public key (generated by blockchain-services) +# Bootstrap JWT public key (local generation in Full mode or remote sync in Lite mode) JWT_PUBLIC_KEY="$SSL_DIR/public_key.pem" -echo "=== Waiting for JWT public key ===" -echo "Expected: $JWT_PUBLIC_KEY" -wait_count=0 -max_wait=60 # 60 seconds max -while [ ! -f "$JWT_PUBLIC_KEY" ] && [ $wait_count -lt $max_wait ]; do - echo "Waiting for blockchain-services to generate JWT keys... (${wait_count}s)" - sleep 2 - wait_count=$((wait_count + 2)) -done - -if [ -f "$JWT_PUBLIC_KEY" ]; then - echo "โœ” JWT public key found" +LOCAL_ISSUER="$(build_local_issuer)" +ISSUER_OVERRIDE="$(trim "${ISSUER:-}" | sed 's:/*$::')" +EFFECTIVE_ISSUER="$LOCAL_ISSUER" +jwt_key_sync_mode="local" +if [ -n "$ISSUER_OVERRIDE" ]; then + EFFECTIVE_ISSUER="$ISSUER_OVERRIDE" +fi + +echo "=== JWT Public Key Bootstrap ===" +echo "Expected key file: $JWT_PUBLIC_KEY" +echo "Local issuer: $LOCAL_ISSUER" +echo "Effective issuer: $EFFECTIVE_ISSUER" + +if [ -n "$ISSUER_OVERRIDE" ] && [ "$EFFECTIVE_ISSUER" != "$LOCAL_ISSUER" ]; then + jwt_key_sync_mode="remote" + echo "Detected external issuer mode (Lite): syncing JWT public key from issuer origin" + + wait_count=0 + max_wait=60 + synced=false + while [ "$synced" != "true" ] && [ $wait_count -lt $max_wait ]; do + sync_jwt_public_key_from_issuer "$EFFECTIVE_ISSUER" + sync_result=$? + if [ $sync_result -eq 0 ] || [ $sync_result -eq 10 ]; then + synced=true + break + fi + echo "Retrying JWT key sync in 2s... (${wait_count}s)" + sleep 2 + wait_count=$((wait_count + 2)) + done + + if [ -f "$JWT_PUBLIC_KEY" ]; then + echo "JWT public key ready for external issuer mode" + else + echo "WARNING: JWT public key sync failed after ${max_wait}s - JWT validation will fail until next refresh" + fi else - echo "โš  JWT public key not found after ${max_wait}s - JWT validation will fail until key is available" - echo " blockchain-services should generate it on startup" + echo "Detected local issuer mode (Full): waiting for blockchain-services to generate JWT keys" + wait_count=0 + max_wait=60 + while [ ! -f "$JWT_PUBLIC_KEY" ] && [ $wait_count -lt $max_wait ]; do + echo "Waiting for blockchain-services to generate JWT keys... (${wait_count}s)" + sleep 2 + wait_count=$((wait_count + 2)) + done + + if [ -f "$JWT_PUBLIC_KEY" ]; then + echo "JWT public key found" + else + echo "WARNING: JWT public key not found after ${max_wait}s - JWT validation will fail until key is available" + echo "blockchain-services should generate it on startup" + fi fi echo "=== Starting OpenResty ===" @@ -275,4 +380,24 @@ auto_renew_acme() { auto_renew_acme & +auto_refresh_jwt_public_key() { + if [ "$jwt_key_sync_mode" != "remote" ]; then + return + fi + while true; do + sleep 86400 # 24h + sync_jwt_public_key_from_issuer "$EFFECTIVE_ISSUER" + sync_result=$? + if [ $sync_result -eq 10 ]; then + echo "JWT public key changed - reloading OpenResty" + /usr/local/openresty/bin/openresty -s reload || true + elif [ $sync_result -ne 0 ]; then + echo "WARNING: JWT public key refresh failed; keeping current cached key" + fi + done +} + +auto_refresh_jwt_public_key & + exec /usr/local/openresty/bin/openresty -g "daemon off;" + diff --git a/openresty/lab_access.conf b/openresty/lab_access.conf index 57feac1..b186414 100644 --- a/openresty/lab_access.conf +++ b/openresty/lab_access.conf @@ -29,6 +29,12 @@ server { #autoindex on; } + location = /.well-known/public-key.pem { + alias /etc/ssl/private/public_key.pem; + default_type text/plain; + add_header Cache-Control "public, max-age=300" always; + } + # ACME HTTP-01 challenge (certbot webroot) location ^~ /.well-known/acme-challenge/ { alias /var/www/certbot/.well-known/acme-challenge/; @@ -139,6 +145,12 @@ server { try_files $uri $uri/ =404; } + location = /.well-known/public-key.pem { + alias /etc/ssl/private/public_key.pem; + default_type text/plain; + add_header Cache-Control "public, max-age=300" always; + } + # Ensure /lab-manager works even without trailing slash location = /lab-manager { content_by_lua_block { @@ -175,6 +187,16 @@ server { # Blockchain Services - Authentication microservice location /auth { + access_by_lua_block { + local lite_mode = ngx.shared.config:get("lite_mode") + if lite_mode == 1 or lite_mode == true or lite_mode == "1" then + ngx.status = 403 + ngx.header["Content-Type"] = "text/plain" + ngx.say("Forbidden: auth endpoints are disabled in Lite mode.") + return ngx.exit(403) + end + } + set $cors_preflight_origin $cors_allow_origin; if ($cors_preflight_origin = "DENY") { set $cors_preflight_origin ""; @@ -237,6 +259,16 @@ server { } location = /.well-known/openid-configuration { + access_by_lua_block { + local lite_mode = ngx.shared.config:get("lite_mode") + if lite_mode == 1 or lite_mode == true or lite_mode == "1" then + ngx.status = 403 + ngx.header["Content-Type"] = "text/plain" + ngx.say("Forbidden: OpenID discovery is disabled in Lite mode.") + return ngx.exit(403) + end + } + if ($cors_allow_origin = "DENY") { return 403; } @@ -268,6 +300,14 @@ server { # Ensure both /wallet-dashboard and /wallet-dashboard/ work location = /wallet-dashboard { content_by_lua_block { + local lite_mode = ngx.shared.config:get("lite_mode") + if lite_mode == 1 or lite_mode == true or lite_mode == "1" then + ngx.status = 403 + ngx.header["Content-Type"] = "text/plain" + ngx.say("Forbidden: wallet dashboard is disabled in Lite mode.") + return ngx.exit(403) + end + local token = ngx.var.arg_token if token and token ~= "" then local expected = os.getenv("TREASURY_TOKEN") or "" @@ -308,6 +348,15 @@ server { # Institution configuration UI + provisioning token endpoints location = /institution-config { + access_by_lua_block { + local lite_mode = ngx.shared.config:get("lite_mode") + if lite_mode == 1 or lite_mode == true or lite_mode == "1" then + ngx.status = 403 + ngx.header["Content-Type"] = "text/plain" + ngx.say("Forbidden: institution configuration is disabled in Lite mode.") + return ngx.exit(403) + end + } return 307 /institution-config/; } @@ -434,6 +483,16 @@ server { # These endpoints implement the dedicated onboarding endpoint from the Federated SSO spec # where the browser talks directly to the WIB for WebAuthn credential registration. location /onboarding/webauthn/ { + access_by_lua_block { + local lite_mode = ngx.shared.config:get("lite_mode") + if lite_mode == 1 or lite_mode == true or lite_mode == "1" then + ngx.status = 403 + ngx.header["Content-Type"] = "text/plain" + ngx.say("Forbidden: onboarding endpoints are disabled in Lite mode.") + return ngx.exit(403) + end + } + # Enable CORS for SP/marketplace origins if ($cors_allow_origin = "DENY") { return 403; @@ -481,10 +540,29 @@ server { # Intent endpoints (institutional intents ingress) location = /intents { + access_by_lua_block { + local lite_mode = ngx.shared.config:get("lite_mode") + if lite_mode == 1 or lite_mode == true or lite_mode == "1" then + ngx.status = 403 + ngx.header["Content-Type"] = "text/plain" + ngx.say("Forbidden: intents endpoint is disabled in Lite mode.") + return ngx.exit(403) + end + } return 307 /intents/; } location /intents/ { + access_by_lua_block { + local lite_mode = ngx.shared.config:get("lite_mode") + if lite_mode == 1 or lite_mode == true or lite_mode == "1" then + ngx.status = 403 + ngx.header["Content-Type"] = "text/plain" + ngx.say("Forbidden: intents endpoint is disabled in Lite mode.") + return ngx.exit(403) + end + } + if ($cors_allow_origin = "DENY") { return 403; } @@ -607,6 +685,21 @@ server { content_by_lua_file /etc/openresty/lua/gateway_health.lua; } + location /gateway/mode { + if ($request_method = 'OPTIONS') { + return 204; + } + content_by_lua_block { + local lite_mode = ngx.shared.config:get("lite_mode") + local mode = "full" + if lite_mode == 1 or lite_mode == true or lite_mode == "1" then + mode = "lite" + end + ngx.header["Content-Type"] = "application/json" + ngx.say(string.format('{"mode":"%s","lite":%s}', mode, mode == "lite" and "true" or "false")) + } + } + # Internal helpers for aggregated health location = /__health_blockchain { internal; diff --git a/openresty/lua/admin_access.lua b/openresty/lua/admin_access.lua index 01cac8b..1973006 100644 --- a/openresty/lua/admin_access.lua +++ b/openresty/lua/admin_access.lua @@ -2,6 +2,8 @@ -- Uses TREASURY_TOKEN only. If unset, allows loopback/Docker ranges only. local token = os.getenv("TREASURY_TOKEN") or "" +local config = ngx.shared and ngx.shared.config +local lite_mode = config and config:get("lite_mode") local header_name = os.getenv("TREASURY_TOKEN_HEADER") or "X-Access-Token" local cookie_name = os.getenv("TREASURY_TOKEN_COOKIE") or "access_token" @@ -13,6 +15,17 @@ local function deny(message) return ngx.exit(ngx.HTTP_UNAUTHORIZED) end +local function deny_forbidden(message) + ngx.status = 403 + ngx.header["Content-Type"] = "text/plain" + ngx.say(message or "Forbidden") + return ngx.exit(403) +end + +if lite_mode == 1 or lite_mode == true or lite_mode == "1" then + return deny_forbidden("Forbidden: treasury admin endpoints are disabled in Lite mode.") +end + local function is_loopback_or_docker(ip) if not ip or ip == "" then return false diff --git a/openresty/lua/init.lua b/openresty/lua/init.lua index 01dc520..b336548 100644 --- a/openresty/lua/init.lua +++ b/openresty/lua/init.lua @@ -67,11 +67,36 @@ local function build_default_issuer() return string.format("https://%s%s%s", name, port_segment, "/auth") end +local function build_local_issuer() + local name = trim(server_name) or "localhost" + local port_segment = "" + if https_port and https_port ~= "" and https_port ~= "443" then + port_segment = ":" .. https_port + end + return string.format("https://%s%s%s", name, port_segment, "/auth") +end + +local function normalize_issuer(value) + local normalized = trim(value) + if not normalized or normalized == "" then + return "" + end + normalized = normalized:gsub("/+$", "") + return normalized +end + +local configured_issuer = trim(os.getenv("ISSUER")) +local local_issuer = build_local_issuer() local issuer = build_default_issuer() +local lite_mode = false +if configured_issuer and configured_issuer ~= "" then + lite_mode = normalize_issuer(configured_issuer) ~= normalize_issuer(local_issuer) +end config:set("server_name", server_name) config:set("guac_uri", "/guacamole") config:set("issuer", issuer) +config:set("lite_mode", lite_mode and 1 or 0) config:set("admin_user", admin_user) config:set("admin_pass", admin_pass) config:set("https_port", https_port) @@ -82,6 +107,12 @@ else config:set("guac_api_url", "http://127.0.0.1:8080/guacamole/api") end +if lite_mode then + ngx.log(ngx.INFO, "Lite mode enabled: treasury/auth/intents endpoints are restricted on this gateway") +else + ngx.log(ngx.INFO, "Full mode enabled") +end + -- Read the public key from a file local file = io.open("/etc/ssl/private/public_key.pem", "r") if file then diff --git a/openresty/lua/treasury_access.lua b/openresty/lua/treasury_access.lua index 9850ba0..f3c40ef 100644 --- a/openresty/lua/treasury_access.lua +++ b/openresty/lua/treasury_access.lua @@ -2,6 +2,8 @@ -- Enforces the treasury token for non-local clients when configured. local token = os.getenv("TREASURY_TOKEN") or "" +local config = ngx.shared and ngx.shared.config +local lite_mode = config and config:get("lite_mode") local function deny(message) ngx.status = ngx.HTTP_UNAUTHORIZED @@ -10,6 +12,17 @@ local function deny(message) return ngx.exit(ngx.HTTP_UNAUTHORIZED) end +local function deny_forbidden(message) + ngx.status = 403 + ngx.header["Content-Type"] = "text/plain" + ngx.say(message or "Forbidden") + return ngx.exit(403) +end + +if lite_mode == 1 or lite_mode == true or lite_mode == "1" then + return deny_forbidden("Forbidden: wallet/treasury endpoints are disabled in Lite mode.") +end + local function is_loopback_or_docker(ip) if not ip or ip == "" then return false diff --git a/openresty/nginx.conf b/openresty/nginx.conf index 7db7805..7e54735 100644 --- a/openresty/nginx.conf +++ b/openresty/nginx.conf @@ -3,6 +3,7 @@ env HTTPS_PORT; env HTTP_PORT; env SERVER_NAME; +env ISSUER; env GUAC_ADMIN_USER; env GUAC_ADMIN_PASS; env AUTO_LOGOUT_ON_DISCONNECT; diff --git a/openresty/tests/README.md b/openresty/tests/README.md index f2ebc7f..b3767b1 100644 --- a/openresty/tests/README.md +++ b/openresty/tests/README.md @@ -36,12 +36,14 @@ Linux/macOS: ```bash ./openresty/tests/run-lua-tests.sh +./openresty/tests/run-jwt-key-sync-integration.sh ``` Windows (PowerShell): ```powershell .\openresty\tests\run-lua-tests.ps1 +.\openresty\tests\run-jwt-key-sync-integration.ps1 ``` Direct run (if LuaJIT + lua-cjson are installed): @@ -56,6 +58,7 @@ luajit openresty/tests/run.lua - JWT validation and JWKS fetch (`jwt_handler`) - Header/body filters for Guacamole auth flow - Session cleanup and revocation (`log_handler`, `session_guard`) +- Lite-mode JWT key synchronization from `ISSUER` origin (`run-jwt-key-sync-integration.*`) ## Add a new spec diff --git a/openresty/tests/run-jwt-key-sync-integration.ps1 b/openresty/tests/run-jwt-key-sync-integration.ps1 new file mode 100644 index 0000000..c786b99 --- /dev/null +++ b/openresty/tests/run-jwt-key-sync-integration.ps1 @@ -0,0 +1,105 @@ +#!/usr/bin/env pwsh +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" +$PSNativeCommandUseErrorActionPreference = $false +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$OpenrestyDir = Split-Path -Parent $ScriptDir +$ProjectRoot = Split-Path -Parent $OpenrestyDir + +$TempRoot = Join-Path $ProjectRoot ".tmp-jwt-key-sync-test" +$NetworkName = "lgw-key-sync-test-net" +$KeyServerContainer = "lgw-key-sync-keysrv" +$LiteContainer = "lgw-key-sync-lite" + +function Cleanup { + docker rm -f $LiteContainer 2>$null | Out-Null + docker rm -f $KeyServerContainer 2>$null | Out-Null + docker network rm $NetworkName 2>$null | Out-Null + if (Test-Path $TempRoot) { + Remove-Item -Recurse -Force $TempRoot + } +} + +try { + docker version | Out-Null + + Write-Host "Building OpenResty image..." -ForegroundColor Yellow + docker compose -f (Join-Path $ProjectRoot "docker-compose.yml") build openresty | Out-Null + + New-Item -ItemType Directory -Force -Path (Join-Path $TempRoot "keysrv/.well-known") | Out-Null + New-Item -ItemType Directory -Force -Path (Join-Path $TempRoot "lite-certs") | Out-Null + + Copy-Item -Force (Join-Path $ProjectRoot "certs/fullchain.pem") (Join-Path $TempRoot "lite-certs/fullchain.pem") + Copy-Item -Force (Join-Path $ProjectRoot "certs/privkey.pem") (Join-Path $TempRoot "lite-certs/privkey.pem") + Copy-Item -Force (Join-Path $ProjectRoot "certs/public_key.pem") (Join-Path $TempRoot "keysrv/.well-known/public-key.pem") + + $TempRootDocker = $TempRoot -replace "\\", "/" + $ProjectRootDocker = $ProjectRoot -replace "\\", "/" + + # Prepare a deliberately wrong key in lite-certs to verify replacement. + docker run --rm ` + -v "${TempRootDocker}/lite-certs:/w" ` + labgateway-openresty:latest ` + sh -c "openssl genrsa -out /w/alt_private.pem 2048 >/dev/null 2>&1 && openssl rsa -in /w/alt_private.pem -pubout -out /w/public_key.pem >/dev/null 2>&1 && rm -f /w/alt_private.pem" | Out-Null + + $beforeHash = (Get-FileHash (Join-Path $TempRoot "lite-certs/public_key.pem") -Algorithm SHA256).Hash + + docker network create $NetworkName | Out-Null + docker run -d --name $KeyServerContainer --network $NetworkName ` + -v "${TempRootDocker}/keysrv:/srv:ro" ` + python:3.12-alpine ` + sh -c "cd /srv && python -m http.server 8000" | Out-Null + + docker run -d --name $LiteContainer --network $NetworkName ` + --add-host blockchain-services:127.0.0.1 ` + --add-host guacamole:127.0.0.1 ` + --add-host guacd:127.0.0.1 ` + --add-host mysql:127.0.0.1 ` + --add-host ops-worker:127.0.0.1 ` + -e GUAC_ADMIN_USER=admin ` + -e GUAC_ADMIN_PASS=TestPass_12345 ` + -e SERVER_NAME=lite.local ` + -e HTTPS_PORT=443 ` + -e HTTP_PORT=80 ` + -e ISSUER=http://lgw-key-sync-keysrv:8000/auth ` + -v "${TempRootDocker}/lite-certs:/etc/ssl/private" ` + -v "${ProjectRootDocker}/openresty/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro" ` + -v "${ProjectRootDocker}/openresty/lab_access.conf:/etc/openresty/lab_access.conf:ro" ` + -v "${ProjectRootDocker}/openresty/lua:/etc/openresty/lua:ro" ` + -v "${ProjectRootDocker}/web:/var/www/html:ro" ` + labgateway-openresty:latest | Out-Null + + $ready = $false + for ($i = 0; $i -lt 30; $i++) { + $logs = cmd /c "docker logs $LiteContainer 2>&1" + if ($logs -match "JWT public key ready for external issuer mode") { + $ready = $true + break + } + Start-Sleep -Seconds 1 + } + + if (-not $ready) { + throw "Lite container did not report JWT key sync readiness." + } + + $expectedHash = (Get-FileHash (Join-Path $TempRoot "keysrv/.well-known/public-key.pem") -Algorithm SHA256).Hash + $afterHash = (Get-FileHash (Join-Path $TempRoot "lite-certs/public_key.pem") -Algorithm SHA256).Hash + + if ($beforeHash -eq $expectedHash) { + throw "Precondition failed: lite key must start different from issuer key." + } + if ($afterHash -ne $expectedHash) { + throw "Lite key was not synchronized from issuer key." + } + + Write-Host "JWT key sync integration test passed." -ForegroundColor Green + Write-Host "before=$beforeHash" + Write-Host "after=$afterHash" + Write-Host "expected=$expectedHash" +} +finally { + Cleanup +} diff --git a/openresty/tests/run-jwt-key-sync-integration.sh b/openresty/tests/run-jwt-key-sync-integration.sh new file mode 100644 index 0000000..fd37c37 --- /dev/null +++ b/openresty/tests/run-jwt-key-sync-integration.sh @@ -0,0 +1,95 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OPENRESTY_DIR="$(dirname "$SCRIPT_DIR")" +PROJECT_ROOT="$(dirname "$OPENRESTY_DIR")" + +TEMP_ROOT="${PROJECT_ROOT}/.tmp-jwt-key-sync-test" +NETWORK_NAME="lgw-key-sync-test-net" +KEYSERVER_CONTAINER="lgw-key-sync-keysrv" +LITE_CONTAINER="lgw-key-sync-lite" + +cleanup() { + docker rm -f "$LITE_CONTAINER" >/dev/null 2>&1 || true + docker rm -f "$KEYSERVER_CONTAINER" >/dev/null 2>&1 || true + docker network rm "$NETWORK_NAME" >/dev/null 2>&1 || true + rm -rf "$TEMP_ROOT" +} +trap cleanup EXIT + +docker version >/dev/null + +echo "Building OpenResty image..." +docker compose -f "${PROJECT_ROOT}/docker-compose.yml" build openresty >/dev/null + +mkdir -p "${TEMP_ROOT}/keysrv/.well-known" "${TEMP_ROOT}/lite-certs" +cp "${PROJECT_ROOT}/certs/fullchain.pem" "${TEMP_ROOT}/lite-certs/fullchain.pem" +cp "${PROJECT_ROOT}/certs/privkey.pem" "${TEMP_ROOT}/lite-certs/privkey.pem" +cp "${PROJECT_ROOT}/certs/public_key.pem" "${TEMP_ROOT}/keysrv/.well-known/public-key.pem" + +# Prepare a deliberately wrong key in lite-certs to verify replacement. +docker run --rm \ + -v "${TEMP_ROOT}/lite-certs:/w" \ + labgateway-openresty:latest \ + sh -c "openssl genrsa -out /w/alt_private.pem 2048 >/dev/null 2>&1 && openssl rsa -in /w/alt_private.pem -pubout -out /w/public_key.pem >/dev/null 2>&1 && rm -f /w/alt_private.pem" >/dev/null + +before_hash="$(sha256sum "${TEMP_ROOT}/lite-certs/public_key.pem" | awk '{print $1}')" + +docker network create "$NETWORK_NAME" >/dev/null +docker run -d --name "$KEYSERVER_CONTAINER" --network "$NETWORK_NAME" \ + -v "${TEMP_ROOT}/keysrv:/srv:ro" \ + python:3.12-alpine \ + sh -c "cd /srv && python -m http.server 8000" >/dev/null + +docker run -d --name "$LITE_CONTAINER" --network "$NETWORK_NAME" \ + --add-host blockchain-services:127.0.0.1 \ + --add-host guacamole:127.0.0.1 \ + --add-host guacd:127.0.0.1 \ + --add-host mysql:127.0.0.1 \ + --add-host ops-worker:127.0.0.1 \ + -e GUAC_ADMIN_USER=admin \ + -e GUAC_ADMIN_PASS=TestPass_12345 \ + -e SERVER_NAME=lite.local \ + -e HTTPS_PORT=443 \ + -e HTTP_PORT=80 \ + -e ISSUER="http://${KEYSERVER_CONTAINER}:8000/auth" \ + -v "${TEMP_ROOT}/lite-certs:/etc/ssl/private" \ + -v "${PROJECT_ROOT}/openresty/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro" \ + -v "${PROJECT_ROOT}/openresty/lab_access.conf:/etc/openresty/lab_access.conf:ro" \ + -v "${PROJECT_ROOT}/openresty/lua:/etc/openresty/lua:ro" \ + -v "${PROJECT_ROOT}/web:/var/www/html:ro" \ + labgateway-openresty:latest >/dev/null + +ready=false +for _ in $(seq 1 30); do + logs="$(docker logs "$LITE_CONTAINER" 2>&1 || true)" + if echo "$logs" | grep -q "JWT public key ready for external issuer mode"; then + ready=true + break + fi + sleep 1 +done + +if [ "$ready" != "true" ]; then + echo "Lite container did not report JWT key sync readiness." + exit 1 +fi + +expected_hash="$(sha256sum "${TEMP_ROOT}/keysrv/.well-known/public-key.pem" | awk '{print $1}')" +after_hash="$(sha256sum "${TEMP_ROOT}/lite-certs/public_key.pem" | awk '{print $1}')" + +if [ "$before_hash" = "$expected_hash" ]; then + echo "Precondition failed: lite key must start different from issuer key." + exit 1 +fi + +if [ "$after_hash" != "$expected_hash" ]; then + echo "Lite key was not synchronized from issuer key." + exit 1 +fi + +echo "JWT key sync integration test passed." +echo "before=${before_hash}" +echo "after=${after_hash}" +echo "expected=${expected_hash}" diff --git a/openresty/tests/run-lua-tests.ps1 b/openresty/tests/run-lua-tests.ps1 index 817d756..d4c6b4a 100644 --- a/openresty/tests/run-lua-tests.ps1 +++ b/openresty/tests/run-lua-tests.ps1 @@ -18,6 +18,7 @@ param() $ErrorActionPreference = "Stop" +$PSNativeCommandUseErrorActionPreference = $false $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $ProjectRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir) @@ -39,37 +40,47 @@ $TestDockerfile = @" FROM openresty/openresty:alpine # Install luarocks and cjson -RUN apk add --no-cache luarocks5.1 && \ +RUN apk add --no-cache luarocks5.1 lua5.1-dev gcc musl-dev && \ luarocks-5.1 install lua-cjson WORKDIR /app "@ -$DockerfilePath = Join-Path $ProjectRoot "openresty" "Dockerfile.test" +$DockerfilePath = Join-Path (Join-Path $ProjectRoot "openresty") "Dockerfile.test" $TestDockerfile | Out-File -FilePath $DockerfilePath -Encoding utf8 -NoNewline try { Write-Host "Building test container..." -ForegroundColor Yellow - docker build -t openresty-lua-tests -f $DockerfilePath (Join-Path $ProjectRoot "openresty") 2>&1 | ForEach-Object { + $openrestyRoot = Join-Path $ProjectRoot "openresty" + $buildCmd = "docker build -t openresty-lua-tests -f `"$DockerfilePath`" `"$openrestyRoot`"" + $previousErrorAction = $ErrorActionPreference + $ErrorActionPreference = "Continue" + $buildOutput = cmd /c "$buildCmd 2>&1" + $ErrorActionPreference = $previousErrorAction + $buildOutput | ForEach-Object { if ($_ -match "error" -or $_ -match "Error") { Write-Host $_ -ForegroundColor Red } elseif ($VerbosePreference -eq "Continue") { Write-Host $_ } } + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } Write-Host "Running tests..." -ForegroundColor Yellow Write-Host "" - $openrestyPath = Join-Path $ProjectRoot "openresty" - + $repoPath = $ProjectRoot + $repoPathDocker = $repoPath -replace "\\", "/" + # Run tests in container - $result = docker run --rm ` - -v "${openrestyPath}:/app:ro" ` - -w /app ` - openresty-lua-tests ` - luajit tests/run.lua 2>&1 + $runCmd = "docker run --rm -v `"${repoPathDocker}:/workspace:ro`" -w /workspace openresty-lua-tests luajit openresty/tests/run.lua" + $previousErrorAction = $ErrorActionPreference + $ErrorActionPreference = "Continue" + $result = cmd /c "$runCmd 2>&1" + $ErrorActionPreference = $previousErrorAction $exitCode = $LASTEXITCODE diff --git a/openresty/tests/run-lua-tests.sh b/openresty/tests/run-lua-tests.sh index a2a359c..8211b30 100644 --- a/openresty/tests/run-lua-tests.sh +++ b/openresty/tests/run-lua-tests.sh @@ -23,7 +23,7 @@ cat > "${OPENRESTY_DIR}/Dockerfile.test" << 'EOF' FROM openresty/openresty:alpine # Install luarocks and cjson -RUN apk add --no-cache luarocks5.1 && \ +RUN apk add --no-cache luarocks5.1 lua5.1-dev gcc musl-dev && \ luarocks-5.1 install lua-cjson WORKDIR /app @@ -42,10 +42,10 @@ echo "" # Run tests in container docker run --rm \ - -v "${OPENRESTY_DIR}:/app:ro" \ - -w /app \ + -v "$(dirname "$OPENRESTY_DIR"):/workspace:ro" \ + -w /workspace \ openresty-lua-tests \ - luajit tests/run.lua + luajit openresty/tests/run.lua EXIT_CODE=$? diff --git a/openresty/tests/unit/admin_access_spec.lua b/openresty/tests/unit/admin_access_spec.lua index e8b437f..dea1e5a 100644 --- a/openresty/tests/unit/admin_access_spec.lua +++ b/openresty/tests/unit/admin_access_spec.lua @@ -52,7 +52,8 @@ local function run_admin_access(opts) local headers = opts.headers or {} local uri_args = opts.uri_args or {} local ngx = ngx_factory.new({ - var = opts.var or {} + var = opts.var or {}, + config = opts.config or {} }) ngx.req.get_headers = function() @@ -79,6 +80,17 @@ local function run_admin_access(opts) end runner.describe("Treasury admin token guard", function() + runner.it("blocks treasury admin access in lite mode", function() + local ngx = run_admin_access({ + env = { TREASURY_TOKEN = "treasury-token" }, + config = { lite_mode = 1 }, + var = { remote_addr = "127.0.0.1" } + }) + + runner.assert.equals(403, ngx.status) + runner.assert.equals(403, ngx._exit) + end) + runner.it("rejects public IPs when TREASURY_TOKEN is not configured", function() local ngx = run_admin_access({ env = { TREASURY_TOKEN = "" }, diff --git a/openresty/tests/unit/treasury_access_spec.lua b/openresty/tests/unit/treasury_access_spec.lua index 8ed7c07..33467c6 100644 --- a/openresty/tests/unit/treasury_access_spec.lua +++ b/openresty/tests/unit/treasury_access_spec.lua @@ -51,7 +51,8 @@ local function run_treasury_access(opts) local env = opts.env or {} local headers = opts.headers or {} local ngx = ngx_factory.new({ - var = opts.var or {} + var = opts.var or {}, + config = opts.config or {} }) ngx.req.get_headers = function() @@ -75,6 +76,18 @@ local function run_treasury_access(opts) end runner.describe("Treasury token guard", function() +runner.it("blocks all treasury access in lite mode", function() + local ngx = run_treasury_access({ + env = { TREASURY_TOKEN = "secret-token" }, + config = { lite_mode = 1 }, + var = { remote_addr = "127.0.0.1" } + }) + + runner.assert.equals(403, ngx.status) + runner.assert.equals(403, ngx._exit) + runner.assert.equals("text/plain", ngx.header["Content-Type"]) +end) + runner.it("rejects public IPs when no token configured", function() local ngx = run_treasury_access({ env = { TREASURY_TOKEN = "" }, diff --git a/setup.bat b/setup.bat index 3d8163b..5340e48 100644 --- a/setup.bat +++ b/setup.bat @@ -330,6 +330,30 @@ if /i "!domain!"=="localhost" ( ) echo. +echo JWT Issuer ^(Full/Lite^) +echo ====================== +echo ISSUER controls which JWT issuer OpenResty accepts: +echo - Leave empty -^> Full mode ^(this gateway handles auth + access^). +echo - Set https://^/auth -^> Lite mode ^(trust Full-issued JWTs^). +echo - In Lite mode, public key sync is automatic from https://^/.well-known/public-key.pem. +echo - Lite mode disables local auth/treasury/intents endpoints. +call :ReadEnvValue "%ROOT_ENV_FILE%" "ISSUER" current_issuer +if defined current_issuer ( + echo Current ISSUER in .env: !current_issuer! +) else ( + echo Current ISSUER in .env: ^(empty^) +) +set "issuer_value=" +set /p "issuer_value=ISSUER [empty->Full, https://full/auth->Lite]: " +if defined issuer_value set "issuer_value=!issuer_value: =!" +call :UpdateEnv "%ROOT_ENV_FILE%" "ISSUER" "!issuer_value!" +if "!issuer_value!"=="" ( + echo * ISSUER left empty ^(Full mode^). +) else ( + echo * ISSUER set to: !issuer_value! ^(Lite mode^). +) +echo. + echo Remote Access (Cloudflare Tunnel) echo ================================= set "enable_cf=" diff --git a/setup.sh b/setup.sh index 1169bd2..2463fb4 100644 --- a/setup.sh +++ b/setup.sh @@ -368,6 +368,30 @@ else fi fi +echo +echo "JWT Issuer (Full/Lite)" +echo "======================" +echo "ISSUER controls which JWT issuer OpenResty accepts:" +echo " - Leave empty -> Full mode (this gateway handles auth + access)." +echo " - Set https:///auth -> Lite mode (trust Full-issued JWTs)." +echo " - In Lite mode, public key sync is automatic from https:///.well-known/public-key.pem." +echo " - Lite mode disables local auth/treasury/intents endpoints." +current_issuer="$(get_env_default "ISSUER" "$ROOT_ENV_FILE")" +if [ -n "$current_issuer" ]; then + echo "Current ISSUER in .env: $current_issuer" +else + echo "Current ISSUER in .env: (empty)" +fi +read -p "ISSUER [empty->Full, https://full/auth->Lite]: " issuer_value +issuer_value=$(echo "$issuer_value" | tr -d ' ') +update_env_var "$ROOT_ENV_FILE" "ISSUER" "$issuer_value" +if [ -z "$issuer_value" ]; then + echo " * ISSUER left empty (Full mode)." +else + echo " * ISSUER set to: $issuer_value (Lite mode)." +fi +echo + echo echo "Remote Access (Cloudflare Tunnel)" echo "=================================" diff --git a/web/assets/js/app.js b/web/assets/js/app.js index 056af72..4c84c5f 100644 --- a/web/assets/js/app.js +++ b/web/assets/js/app.js @@ -1,5 +1,33 @@ // Effects and animations for the main page document.addEventListener('DOMContentLoaded', function() { + function applyGatewayMode() { + const walletButton = document.getElementById('wallet-treasury-btn'); + if (!walletButton) { + return; + } + + fetch('/gateway/mode', { cache: 'no-store' }) + .then(response => { + if (!response.ok) { + return null; + } + return response.json(); + }) + .then(modeInfo => { + if (!modeInfo) { + return; + } + if (modeInfo.lite === true || modeInfo.mode === 'lite') { + walletButton.remove(); + document.body.classList.add('gateway-lite-mode'); + } + }) + .catch(() => { + // Keep default UI if mode endpoint is unavailable. + }); + } + + applyGatewayMode(); // Entry animation for elements const observerOptions = { diff --git a/web/index.html b/web/index.html index 7bf97f6..376a87a 100644 --- a/web/index.html +++ b/web/index.html @@ -64,7 +64,7 @@

System Access

โ†’ - + Wallet & Treasury โ†’