diff --git a/config/README.md b/config/README.md index c785293..a5b8ebc 100644 --- a/config/README.md +++ b/config/README.md @@ -41,6 +41,8 @@ Additional service directories may be added when those services are implemented. `config/sql-server/` contains the version-controlled Docker Compose definition and safe example environment file for the SQL Server container. The live environment file is stored outside the repository at `/etc/atlas/sql-server.env`. +`config/nginx/` contains the temporary Atlas default HTTP site configuration. Live nginx configuration is deployed under `/etc/nginx/`. + ## Security Considerations Configuration templates must not contain live secrets. Secret-bearing live files should be stored outside the repository and referenced by documented paths or environment variable names. diff --git a/config/nginx/README.md b/config/nginx/README.md new file mode 100644 index 0000000..fa5a89c --- /dev/null +++ b/config/nginx/README.md @@ -0,0 +1,15 @@ +# nginx Configuration + +This directory contains version-controlled nginx configuration for Atlas. + +`atlas-default.conf` is the temporary default HTTP site. It serves a small static status page from `/var/www/atlas` and exposes `/health` for local validation. + +Deployment locations: + +- Source configuration: `config/nginx/atlas-default.conf` +- Live configuration: `/etc/nginx/sites-available/atlas-default.conf` +- Enabled site: `/etc/nginx/sites-enabled/atlas-default.conf` +- Static content: `/var/www/atlas` +- Health check: `/health` + +Do not store TLS private keys, live certificates, secrets, or PlotDirector-specific proxy credentials here. diff --git a/config/nginx/atlas-default.conf b/config/nginx/atlas-default.conf new file mode 100644 index 0000000..9b4e155 --- /dev/null +++ b/config/nginx/atlas-default.conf @@ -0,0 +1,20 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + + server_name _; + root /var/www/atlas; + index index.html; + + access_log /var/log/nginx/atlas-access.log; + error_log /var/log/nginx/atlas-error.log; + + location = /health { + add_header Content-Type text/plain; + return 200 "ok\n"; + } + + location / { + try_files $uri $uri/ =404; + } +} diff --git a/config/sql-server/compose.yml b/config/sql-server/compose.yml index 83c7e28..4031bf9 100644 --- a/config/sql-server/compose.yml +++ b/config/sql-server/compose.yml @@ -8,7 +8,7 @@ services: env_file: - /etc/atlas/sql-server.env ports: - - "1433:1433" + - "192.168.10.10:1433:1433" volumes: - /sql/mssql:/var/opt/mssql healthcheck: @@ -26,4 +26,3 @@ services: options: max-size: "10m" max-file: "5" - diff --git a/config/sql-server/sql-server.env.example b/config/sql-server/sql-server.env.example index 21f54ea..77c32aa 100644 --- a/config/sql-server/sql-server.env.example +++ b/config/sql-server/sql-server.env.example @@ -8,4 +8,3 @@ ACCEPT_EULA=Y MSSQL_PID=Developer MSSQL_TCP_PORT=1433 MSSQL_SA_PASSWORD=CHANGE_ME_DO_NOT_COMMIT - diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 32ab332..c406932 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -34,7 +34,9 @@ The intended model separates operating system state, SQL data, and development d - Primary network interface: `enp3s0` - Gitea SSH remote for this repository: `git@git.catherinelynwood.com:atlas-bot/AI-Development-Server.git` -DNS, firewall, public exposure, and TLS policy are `TODO: Confirm during the relevant implementation phase.` +UFW is active with default deny incoming and default allow outgoing. Inbound SSH `22`, HTTP `80`, and HTTPS `443` are allowed. SQL Server `1433` is allowed only from `192.168.10.0/24` and is bound to `192.168.10.10`. + +Public DNS, TLS, and router exposure policy are `TODO: Confirm during the relevant implementation phase.` ### Repository and Source-Control Flow @@ -95,14 +97,16 @@ Specific PlotDirector service names, ports, deployment paths, database names, an ### Role of nginx -nginx is planned as the likely local reverse proxy for HTTP and HTTPS applications. Its future responsibilities may include: +nginx is installed from the standard Ubuntu 26.04 repositories and currently serves a temporary Atlas HTTP status site. -- Routing requests to application services. -- TLS termination. -- Static file serving where appropriate. -- Request size, timeout, and header policy. +- Package version: nginx `1.28.3` +- Service: `nginx` +- Site configuration: `/etc/nginx/sites-available/atlas-default.conf` +- Version-controlled source: `config/nginx/atlas-default.conf` +- Static site root: `/var/www/atlas` +- Health endpoint: `http://192.168.10.10/health` -nginx installation, site layout, TLS certificate handling, and exposure rules are `TODO: Confirm during the relevant implementation phase.` +HTTPS, TLS certificates, and PlotDirector reverse-proxy configuration are not configured yet. ### Role of SQL Server @@ -185,8 +189,8 @@ flowchart TB Tests[Future smoke tests
tests/] SQL[(SQL data volume
/sql)] Deploy[Future deployed applications] - Nginx[Future nginx reverse proxy] - Docker[Future Docker runtime] + Nginx[nginx reverse proxy] + Docker[Docker runtime] end Repo --> Install diff --git a/docs/BUILD-LOG.md b/docs/BUILD-LOG.md index bd276fb..e758add 100644 --- a/docs/BUILD-LOG.md +++ b/docs/BUILD-LOG.md @@ -10,6 +10,59 @@ This file records chronological Atlas build and infrastructure milestones. Do no ### Phase +Phase 2 nginx and firewall baseline. + +### Objective + +Install nginx, deploy a minimal Atlas status site, restrict SQL Server binding, and enable a safe UFW baseline. + +### Changes + +- Added `install/phases/40-nginx.sh`. +- Installed nginx `1.28.3` from Ubuntu `resolute-updates`. +- Added Atlas default nginx site under `config/nginx/`. +- Deployed static site content to `/var/www/atlas`. +- Enabled `/health` endpoint. +- Enabled UFW with default deny incoming and default allow outgoing. +- Allowed inbound TCP `22`, `80`, and `443`. +- Allowed inbound TCP `1433` only from `192.168.10.0/24`. +- Changed SQL Server container binding to `192.168.10.10:1433:1433`. + +### Validation + +- `nginx -t` passed. +- HTTP checks passed for `127.0.0.1` and `192.168.10.10`. +- `/health` returned HTTP 200. +- nginx restart test passed. +- SQL Server remained healthy after container recreation. +- SQL data persisted after container recreation. +- SQL LAN connection test passed. +- Atlas discovery and validation passed. + +### Problems + +- Initial nginx deployment encountered a duplicate default-server config while the Ubuntu default site was still enabled. The installer now validates before and after disabling the default site. + +### Decisions + +- Keep nginx HTTP-only until a later TLS phase. +- Keep the temporary Atlas status site separate from future PlotDirector proxy configuration. +- Do not rely on UFW alone for Docker-published SQL Server; bind SQL Server to the Atlas LAN address. + +### Git Commit + +Not yet committed. + +### Operator + +Codex. + +### Date + +2026-07-10 + +### Phase + Phase 2 Docker and SQL Server container platform. ### Objective diff --git a/docs/REBUILD-GUIDE.md b/docs/REBUILD-GUIDE.md index d36264d..f0ac66a 100644 --- a/docs/REBUILD-GUIDE.md +++ b/docs/REBUILD-GUIDE.md @@ -11,6 +11,7 @@ This guide describes the planned rebuild path for Atlas. At Phase 1, the reposit - .NET installation script: present. - Docker installation script: present. - SQL Server container installation script: present. +- nginx installation script: present. - Other component installation scripts: not yet created. - Configuration templates: not yet created. - Operational scripts: not yet created. @@ -118,8 +119,10 @@ Planned bootstrap flow: 7. Run `install/phases/10-dotnet.sh` when .NET SDKs are required. 8. Run `install/phases/30-docker.sh`. 9. Run `install/phases/20-sql-server.sh` after Docker is available. -10. Run additional ordered installation scripts from `install/phases/` when they exist. -11. Run smoke tests from `tests/` when they exist. +10. Run `install/phases/40-nginx.sh`. +11. Apply the UFW baseline rules. +12. Run additional ordered installation scripts from `install/phases/` when they exist. +13. Run smoke tests from `tests/` when they exist. Bootstrap command sequence is `TODO: Confirm during the relevant implementation phase.` @@ -154,7 +157,22 @@ sudo docker inspect -f '{{.State.Health.Status}}' atlas-sql-server SQL Server live secrets are stored at `/etc/atlas/sql-server.env` and must not be committed. -These scripts do not exist yet. When created, each script must: +Current nginx validation commands: + +```bash +sudo nginx -t +systemctl is-active nginx +curl -fsS http://127.0.0.1/health +curl -fsS http://192.168.10.10/health +``` + +Current firewall inspection command: + +```bash +sudo ufw status numbered +``` + +Installation scripts must: - Be idempotent. - Validate prerequisites. diff --git a/install/README.md b/install/README.md index 9bbbf76..21419e8 100644 --- a/install/README.md +++ b/install/README.md @@ -54,6 +54,7 @@ Current Phase 2 installer: install/phases/10-dotnet.sh install/phases/20-sql-server.sh install/phases/30-docker.sh +install/phases/40-nginx.sh ``` The .NET installer installs only the required .NET SDK packages and verifies them with `dotnet --list-sdks`. @@ -67,6 +68,8 @@ Current installed SDKs: Docker is installed from Docker's official signed Ubuntu APT repository. SQL Server uses Microsoft's official SQL Server 2025 container image because native SQL Server packages are not supported on Ubuntu 26.04. +nginx is installed from Ubuntu repositories and serves the temporary Atlas status site until PlotDirector routing is configured. + Runtime output belongs outside the repository: - `/var/log/atlas/install/` diff --git a/install/phases/40-nginx.sh b/install/phases/40-nginx.sh new file mode 100755 index 0000000..920a9d6 --- /dev/null +++ b/install/phases/40-nginx.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +INSTALL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +REPO_ROOT="$(cd "${INSTALL_DIR}/.." && pwd)" + +# shellcheck source=install/lib/common.sh +source "${INSTALL_DIR}/lib/common.sh" +# shellcheck source=install/lib/logging.sh +source "${INSTALL_DIR}/lib/logging.sh" +# shellcheck source=install/lib/packages.sh +source "${INSTALL_DIR}/lib/packages.sh" + +atlas_set_script_name "40-nginx.sh" +atlas_install_error_trap + +ATLAS_SITE_ROOT="/var/www/atlas" +ATLAS_SITE_CONF_SOURCE="${REPO_ROOT}/config/nginx/atlas-default.conf" +ATLAS_SITE_CONF_DEST="/etc/nginx/sites-available/atlas-default.conf" +ATLAS_SITE_LINK="/etc/nginx/sites-enabled/atlas-default.conf" +NGINX_DEFAULT_LINK="/etc/nginx/sites-enabled/default" +NGINX_BACKUP_DIR="/etc/nginx/atlas-backups" + +usage() { + cat <<'USAGE' +Usage: install/phases/40-nginx.sh [options] + +Install nginx from Ubuntu repositories and deploy the temporary Atlas default +HTTP status site. This does not configure HTTPS or PlotDirector proxying. + +Options: + --help Show this help text. + --version Show script/framework version. + --dry-run Show intended actions without making changes. + --verbose Print additional diagnostic output. + +USAGE +} + +while [[ "$#" -gt 0 ]]; do + case "$1" in + --help) + usage + exit "${ATLAS_EXIT_OK}" + ;; + --version) + atlas_print_version + exit "${ATLAS_EXIT_OK}" + ;; + --dry-run) + atlas_enable_dry_run + ;; + --verbose) + atlas_enable_verbose + ;; + *) + atlas_usage_error "Unknown option: $1" + exit "${ATLAS_EXIT_USAGE}" + ;; + esac + shift +done + +install_nginx() { + if atlas_package_installed nginx; then + atlas_log_ok "nginx is already installed" + else + atlas_install_packages nginx + fi +} + +write_site_content() { + local tmp_file + tmp_file="$(mktemp)" + cat >"$tmp_file" <<'HTML' + + + + + + Atlas + + + +
+

Atlas is operational

+

This is the temporary Atlas status site. Application routing has not been configured yet.

+
+ + +HTML + + if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then + atlas_log_info "Dry-run mode: would deploy static site content to ${ATLAS_SITE_ROOT}" + rm -f "$tmp_file" + return 0 + fi + + sudo -n install -d -m 0755 -o www-data -g www-data "$ATLAS_SITE_ROOT" + sudo -n install -m 0644 -o www-data -g www-data "$tmp_file" "${ATLAS_SITE_ROOT}/index.html" + rm -f "$tmp_file" +} + +deploy_nginx_config() { + if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then + atlas_log_info "Dry-run mode: would deploy ${ATLAS_SITE_CONF_DEST}" + atlas_log_info "Dry-run mode: would enable Atlas nginx site and disable Ubuntu default site" + return 0 + fi + + sudo -n install -d -m 0755 /etc/nginx/sites-available /etc/nginx/sites-enabled + sudo -n install -d -m 0750 "$NGINX_BACKUP_DIR" + + sudo -n nginx -t + + if [[ -e "$ATLAS_SITE_CONF_DEST" ]] && ! cmp -s "$ATLAS_SITE_CONF_SOURCE" "$ATLAS_SITE_CONF_DEST"; then + sudo -n cp -a "$ATLAS_SITE_CONF_DEST" "${NGINX_BACKUP_DIR}/atlas-default.conf.$(date -u '+%Y%m%dT%H%M%SZ').bak" + fi + sudo -n install -m 0644 "$ATLAS_SITE_CONF_SOURCE" "$ATLAS_SITE_CONF_DEST" + + if [[ -e "$NGINX_DEFAULT_LINK" ]]; then + sudo -n cp -a "$NGINX_DEFAULT_LINK" "${NGINX_BACKUP_DIR}/default-link.$(date -u '+%Y%m%dT%H%M%SZ').bak" + sudo -n rm -f "$NGINX_DEFAULT_LINK" + fi + + sudo -n ln -sfn "$ATLAS_SITE_CONF_DEST" "$ATLAS_SITE_LINK" + + sudo -n nginx -t +} + +enable_nginx() { + atlas_sudo_run systemctl enable nginx + atlas_sudo_run systemctl restart nginx +} + +verify_nginx() { + if [[ "${ATLAS_DRY_RUN}" == "1" ]]; then + atlas_log_info "Dry-run mode: would verify nginx service and health endpoint" + return 0 + fi + sudo -n nginx -t + systemctl is-enabled nginx >/dev/null + systemctl is-active nginx >/dev/null + curl -fsS http://127.0.0.1/health >/dev/null + curl -fsS http://192.168.10.10/health >/dev/null + atlas_log_ok "nginx verification passed" +} + +main() { + atlas_require_sudo_noninteractive || { + atlas_log_error "Passwordless non-interactive sudo is required" + exit "${ATLAS_EXIT_PREREQ}" + } + install_nginx + write_site_content + deploy_nginx_config + enable_nginx + verify_nginx +} + +main diff --git a/install/phases/README.md b/install/phases/README.md index acea6b6..b2dc3b7 100644 --- a/install/phases/README.md +++ b/install/phases/README.md @@ -23,6 +23,7 @@ Each phase must source the shared framework under `install/lib/`, support dry-ru - `10-dotnet.sh`: installs the required .NET 8 and .NET 10 SDKs from official Ubuntu package sources. - `20-sql-server.sh`: deploys SQL Server 2025 Developer using Microsoft's official container image. - `30-docker.sh`: installs Docker Engine from Docker's official Ubuntu APT repository. +- `40-nginx.sh`: installs nginx and deploys the temporary Atlas status site. Installed SDKs after the current Phase 2 run: diff --git a/scripts/README.md b/scripts/README.md index 99bebcc..4acbfb4 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -53,3 +53,4 @@ Additional areas may be added when a repeated operational need exists. - Include validation and rollback guidance for service-affecting scripts. - Discovery and validation scripts must not dump environment variables, private keys, tokens, or complete sensitive configuration files. - SQL helper scripts must not embed or print SQL administrator passwords. +- Firewall inspection should use `sudo ufw status numbered`. diff --git a/scripts/sql/sqlcmd.sh b/scripts/sql/sqlcmd.sh index 6da46c8..f1c1aa4 100755 --- a/scripts/sql/sqlcmd.sh +++ b/scripts/sql/sqlcmd.sh @@ -31,4 +31,3 @@ fi exec sudo -n docker exec -i "$CONTAINER_NAME" /bin/bash -lc \ 'SQLCMDPASSWORD="$MSSQL_SA_PASSWORD" exec /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -C "$@"' \ sqlcmd "$@" - diff --git a/scripts/system/discover-system.sh b/scripts/system/discover-system.sh index 4b98d2a..bd549ae 100755 --- a/scripts/system/discover-system.sh +++ b/scripts/system/discover-system.sh @@ -131,6 +131,16 @@ sql_container_health() { fi } +nginx_status() { + if ! have nginx; then + printf 'not installed\n' + elif systemctl is-active --quiet nginx 2>/dev/null && sudo -n nginx -t >/dev/null 2>&1; then + printf 'active-config-valid\n' + else + printf 'installed-needs-attention\n' + fi +} + mount_status() { local path="$1" if have findmnt && findmnt -rn --target "$path" >/dev/null 2>&1; then @@ -243,6 +253,7 @@ human_report() { printf ' Docker Compose: %s\n\n' "$(if have docker; then docker compose version 2>/dev/null | sed -n '1p'; else printf 'not installed'; fi)" printf ' Docker status: %s\n' "$(docker_status)" printf ' SQL Server container health: %s\n\n' "$(sql_container_health)" + printf ' nginx status: %s\n\n' "$(nginx_status)" printf 'Services\n' printf ' Failed systemd services:\n' @@ -305,6 +316,7 @@ json_report() { --arg docker_version "$(component_version docker --version)" \ --arg docker_status "$(docker_status)" \ --arg sql_container_health "$(sql_container_health)" \ + --arg nginx_status "$(nginx_status)" \ --arg hostname "$(hostname)" \ --arg host_overrides "$(local_host_overrides_present)" \ --arg firewall "$(firewall_summary)" \ @@ -333,7 +345,8 @@ json_report() { nginx: $nginx_version, docker: $docker_version, docker_status: $docker_status, - sql_server_container_health: $sql_container_health + sql_server_container_health: $sql_container_health, + nginx_status: $nginx_status }, services: {failed_systemd_units: ($failed_services | split("\n") | map(select(length > 0)))}, network: { diff --git a/scripts/system/validate-host.sh b/scripts/system/validate-host.sh index 0c14656..267561c 100755 --- a/scripts/system/validate-host.sh +++ b/scripts/system/validate-host.sh @@ -246,6 +246,37 @@ check_docker_sql_state() { fi } +check_nginx_ufw_state() { + if command -v nginx >/dev/null 2>&1; then + systemctl is-active --quiet nginx && atlas_validation_result PASS "nginx service is active" || atlas_validation_result FAIL "nginx service is not active" + sudo -n nginx -t >/dev/null 2>&1 && atlas_validation_result PASS "nginx configuration is valid" || atlas_validation_result FAIL "nginx configuration is invalid" + curl -fsS http://127.0.0.1/health >/dev/null 2>&1 && atlas_validation_result PASS "Atlas nginx health endpoint is reachable" || atlas_validation_result FAIL "Atlas nginx health endpoint is not reachable" + else + atlas_validation_result SKIPPED "nginx is not installed" + fi + + if command -v ufw >/dev/null 2>&1 && sudo -n ufw status | grep -q '^Status: active'; then + atlas_validation_result PASS "UFW is active" + sudo -n ufw status numbered | grep -q '22/tcp' && atlas_validation_result PASS "UFW allows SSH" || atlas_validation_result FAIL "UFW SSH rule missing" + sudo -n ufw status numbered | grep -q '80/tcp' && atlas_validation_result PASS "UFW allows HTTP" || atlas_validation_result FAIL "UFW HTTP rule missing" + sudo -n ufw status numbered | grep -q '443/tcp' && atlas_validation_result PASS "UFW allows HTTPS" || atlas_validation_result FAIL "UFW HTTPS rule missing" + sudo -n ufw status numbered | grep -q '1433/tcp.*192.168.10.0/24' && atlas_validation_result PASS "UFW restricts SQL Server to LAN subnet" || atlas_validation_result FAIL "UFW SQL LAN rule missing" + else + atlas_validation_result FAIL "UFW is not active" + fi + + if ss -ltn | awk '{print $4}' | grep -qx '192.168.10.10:1433'; then + atlas_validation_result PASS "SQL Server is bound to Atlas LAN address" + else + atlas_validation_result FAIL "SQL Server is not bound to Atlas LAN address" + fi + if ss -ltn | awk '{print $4}' | grep -Eq '(^0\.0\.0\.0:1433$|^\[::\]:1433$)'; then + atlas_validation_result FAIL "SQL Server is bound to all interfaces" + else + atlas_validation_result PASS "SQL Server is not bound to all interfaces" + fi +} + atlas_validation_reset printf 'Atlas Host Validation\n' @@ -290,6 +321,7 @@ check_free_space /sql "$MIN_SQL_FREE_GIB" check_failed_services check_docker_sql_state +check_nginx_ufw_state if [[ -f /var/run/reboot-required ]]; then atlas_validation_result WARNING "Reboot is pending"