diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..31027b8 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,27 @@ +{ + "permissions": { + "allow": [ + "Bash(ansible-playbook *)", + "Bash(ansible-vault *)", + "Bash(ansible *)", + "Bash(ansible-galaxy *)", + "Bash(docker run *)", + "Bash(docker exec *)", + "Bash(docker rm *)", + "Bash(docker pull *)", + "Bash(docker cp *)", + "Bash(cp vault/secrets.yml.example *)", + "Bash(git *)", + "Bash(gh *)", + "Bash(bash -n *)", + "Bash(python3 *)", + "Bash(tee *)", + "Bash(curl *)", + "Bash(mkdir *)", + "Bash(printf *)", + "Bash(chmod *)", + "Write(inventory/ci-local.ini)", + "Write(inventory/host_vars/local-test.yml)" + ] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 582153a..0ace98b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ vault/secrets.yml # Temporary files from local Docker test runs inventory/ci-local.ini +inventory/ci-test-runner.ini +inventory/hosts-docker-test.ini # Old vault passwords vault/.vault_pass_old diff --git a/configure_and_install.sh b/configure_and_install.sh index 8f3036b..b3de208 100755 --- a/configure_and_install.sh +++ b/configure_and_install.sh @@ -250,6 +250,7 @@ PREF_OPENRVDAS_AUTOSTART='${OPENRVDAS_AUTOSTART}' PREF_INSTALL_GUI='${INSTALL_GUI}' PREF_INSTALL_FIREWALLD='${INSTALL_FIREWALLD}' PREF_INSTALL_UFW='${INSTALL_UFW}' +PREF_UFW_LOCALHOST_ONLY='${UFW_LOCALHOST_ONLY}' PREF_TCP_PORTS_TO_OPEN='${TCP_PORTS_TO_OPEN}' PREF_UDP_PORTS_TO_OPEN='${UDP_PORTS_TO_OPEN}' PREF_INSTALL_SIMULATE_NBP='${INSTALL_SIMULATE_NBP}' @@ -535,11 +536,13 @@ fi section "Installation" ask INSTALL_ROOT "Installation root directory" "${PREF_INSTALL_ROOT:-/opt}" -if [[ "$INSTALL_ROOT" == /home/* ]]; then +if [[ "$INSTALL_ROOT" != /opt* && "$INSTALL_ROOT" != /srv* && "$INSTALL_ROOT" != /var* && "$INSTALL_ROOT" != /usr/local* ]]; then echo - echo "WARNING: Installing under a home directory can cause nginx 502 errors." - echo "nginx (www-data) cannot traverse home directories without world-execute" - echo "permission. The playbook will set this automatically, but /opt is safer." + echo "WARNING: Installing outside a standard system directory (/opt, /srv, etc.)." + echo "nginx and supervisord must be able to traverse every directory in the path." + echo "Restrictive permissions (e.g. /home dirs are chmod 750 on Ubuntu 24.04," + echo "/root is 700) will block access. The playbook will chmod o+x all parent" + echo "directories automatically, but /opt is safest for production vessel systems." echo fi if [ "$OS_TYPE" != "macos" ]; then @@ -585,6 +588,8 @@ ask_yn INSTALL_GUI "Install nginx + uWSGI web interface?" "${PREF_INSTALL_GUI:-y if [ "$OS_TYPE" = "centos" ] || [ "$OS_TYPE" = "rocky" ] || [ "$OS_TYPE" = "alma" ]; then ask_yn INSTALL_FIREWALLD "Configure firewalld?" "${PREF_INSTALL_FIREWALLD:-no}" INSTALL_UFW="no" + UFW_LOCALHOST_ONLY="no" + UFW_OPENRVDAS_SOURCE="any" if [ "$INSTALL_FIREWALLD" = "yes" ]; then ask TCP_PORTS_TO_OPEN "Extra TCP ports to open (space-separated, blank for none)" "${PREF_TCP_PORTS_TO_OPEN:-}" ask UDP_PORTS_TO_OPEN "Extra UDP ports to open (space-separated, blank for none)" "${PREF_UDP_PORTS_TO_OPEN:-}" @@ -596,15 +601,29 @@ elif [ "$OS_TYPE" = "ubuntu" ] || [ "$OS_TYPE" = "debian" ] || [ "$OS_TYPE" = "r ask_yn INSTALL_UFW "Configure ufw firewall?" "${PREF_INSTALL_UFW:-no}" INSTALL_FIREWALLD="no" if [ "$INSTALL_UFW" = "yes" ]; then + echo " UDP instrument data ports (6224-6226) will be opened automatically." + echo " CachedDataServer (8766) is accessed via the nginx proxy and is not opened." + echo " On an isolated vessel LAN the UDP ports can be open to all hosts; on a" + echo " laptop or dev machine restrict them to localhost only." + ask_yn UFW_LOCALHOST_ONLY "Restrict UDP instrument data ports to localhost only?" "${PREF_UFW_LOCALHOST_ONLY:-no}" + if [ "$UFW_LOCALHOST_ONLY" = "yes" ]; then + UFW_OPENRVDAS_SOURCE="127.0.0.1" + else + UFW_OPENRVDAS_SOURCE="any" + fi ask TCP_PORTS_TO_OPEN "Extra TCP ports to open (space-separated, blank for none)" "${PREF_TCP_PORTS_TO_OPEN:-}" ask UDP_PORTS_TO_OPEN "Extra UDP ports to open (space-separated, blank for none)" "${PREF_UDP_PORTS_TO_OPEN:-}" else + UFW_LOCALHOST_ONLY="no" + UFW_OPENRVDAS_SOURCE="any" TCP_PORTS_TO_OPEN="${PREF_TCP_PORTS_TO_OPEN:-}" UDP_PORTS_TO_OPEN="${PREF_UDP_PORTS_TO_OPEN:-}" fi else INSTALL_FIREWALLD="no" INSTALL_UFW="no" + UFW_LOCALHOST_ONLY="no" + UFW_OPENRVDAS_SOURCE="any" TCP_PORTS_TO_OPEN="${PREF_TCP_PORTS_TO_OPEN:-}" UDP_PORTS_TO_OPEN="${PREF_UDP_PORTS_TO_OPEN:-}" fi @@ -792,6 +811,7 @@ openrvdas_autostart: $(yn_to_bool "$OPENRVDAS_AUTOSTART") install_gui: $(yn_to_bool "$INSTALL_GUI") install_firewalld: $(yn_to_bool "$INSTALL_FIREWALLD") install_ufw: $(yn_to_bool "$INSTALL_UFW") +ufw_openrvdas_source: "${UFW_OPENRVDAS_SOURCE}" tcp_ports_to_open: $(yaml_str_array "$TCP_PORTS_TO_OPEN") udp_ports_to_open: $(yaml_str_array "$UDP_PORTS_TO_OPEN") diff --git a/inventory/group_vars/all.yml b/inventory/group_vars/all.yml index 112a74a..2720d67 100644 --- a/inventory/group_vars/all.yml +++ b/inventory/group_vars/all.yml @@ -38,6 +38,19 @@ install_firewalld: false # configure firewalld (CentOS/RHEL only) # Set install_ufw: true/false in host_vars to skip the prompt, or pass -e install_ufw=true. # ufw_force_reset: true skips the "preserve existing rules?" prompt (useful in CI / non-interactive runs). +# ── OpenRVDAS data ports ────────────────────────────────────────────────────── +# CachedDataServer websocket port — used in supervisor templates only. +# ufw does NOT open this port: browser access is proxied via nginx /cds-ws, +# and logger processes connect via loopback. +openrvdas_data_port: 8766 +# UDP ports used for instrument data broadcast between loggers. +openrvdas_udp_ports: [6224, 6225, 6226] + +# ── ufw: UDP instrument data port source restriction ───────────────────────── +# "any" — open to all (isolated vessel LAN) +# "127.0.0.1" — localhost only (laptop / dev machine) +ufw_openrvdas_source: "any" + # ── Additional firewall ports ───────────────────────────────────────────────── tcp_ports_to_open: [] udp_ports_to_open: [] diff --git a/roles/common/tasks/directories.yml b/roles/common/tasks/directories.yml index 2f04454..a96ceb9 100644 --- a/roles/common/tasks/directories.yml +++ b/roles/common/tasks/directories.yml @@ -1,4 +1,23 @@ --- +# nginx (www-data) and supervisord must be able to traverse every directory +# component of install_root. Standard locations (/opt, /srv, /var) are 755 by +# default, but home directories are 750 on Ubuntu 24.04, /root is 700, and +# arbitrary custom paths may have been created with restrictive permissions. +# Ensure all parent components are world-executable (o+x is idempotent on +# already-755 dirs and harmless on standard system directories). +- name: Ensure all parent directories of install_root are world-traversable + ansible.builtin.file: + path: "{{ item }}" + mode: "o+x" + loop: >- + {%- set parts = install_root.rstrip('/').split('/') -%} + {%- set result = [] -%} + {%- for i in range(2, parts | length) -%} + {%- set _ = result.append('/'.join(parts[:i])) -%} + {%- endfor -%} + {{ result }} + when: ansible_os_family != 'Darwin' + - name: Create install root directory ansible.builtin.file: path: "{{ install_root }}" diff --git a/roles/firewall/tasks/ufw.yml b/roles/firewall/tasks/ufw.yml index bb2f3d5..76e0b22 100644 --- a/roles/firewall/tasks/ufw.yml +++ b/roles/firewall/tasks/ufw.yml @@ -114,6 +114,24 @@ loop: "{{ udp_ports_to_open }}" tags: [firewall] +# ── OpenRVDAS UDP instrument data ports ─────────────────────────────────────── +# CachedDataServer (8766/tcp) is intentionally NOT opened here — browser access +# goes through the nginx /cds-ws proxy and local logger processes use loopback. +# UDP ports carry instrument data between loggers; they need to be reachable +# from the network on a vessel LAN, or from localhost only on a dev machine. +# ufw_openrvdas_source controls the allowed source: +# "any" — open to all (isolated vessel LAN) +# "127.0.0.1" — localhost only (laptop / dev machine) + +- name: Open OpenRVDAS UDP instrument data ports + community.general.ufw: + rule: allow + port: "{{ item }}" + proto: udp + from_ip: "{{ ufw_openrvdas_source }}" + loop: "{{ openrvdas_udp_ports }}" + tags: [firewall] + - name: Enable ufw community.general.ufw: state: enabled diff --git a/roles/nginx/tasks/main.yml b/roles/nginx/tasks/main.yml index baac5e1..96872eb 100644 --- a/roles/nginx/tasks/main.yml +++ b/roles/nginx/tasks/main.yml @@ -24,12 +24,3 @@ notify: reload supervisor tags: [nginx] -- name: Grant nginx world-execute on home directory so it can reach the uwsgi socket - ansible.builtin.file: - path: "{{ install_root | regex_replace('^(/home/[^/]+).*', '\\1') }}" - mode: "o+x" - become: true - when: - - ansible_os_family != "Darwin" - - install_root is match('^/home/') - tags: [nginx] diff --git a/roles/supervisor/templates/openrvdas_cached_data.conf.j2 b/roles/supervisor/templates/openrvdas_cached_data.conf.j2 index 47932e1..25a5707 100644 --- a/roles/supervisor/templates/openrvdas_cached_data.conf.j2 +++ b/roles/supervisor/templates/openrvdas_cached_data.conf.j2 @@ -2,7 +2,7 @@ ; Do not edit by hand; re-run the Ansible playbook to regenerate. [program:cached_data_server] -command={{ install_root }}/openrvdas/venv/bin/python server/cached_data_server.py --port 8766 --disk_cache /var/tmp/openrvdas/disk_cache --max_records 8640 -v +command={{ install_root }}/openrvdas/venv/bin/python server/cached_data_server.py --port {{ openrvdas_data_port }} --disk_cache /var/tmp/openrvdas/disk_cache --max_records 8640 -v directory={{ install_root }}/openrvdas autostart={{ openrvdas_autostart | lower }} autorestart=true diff --git a/roles/supervisor/templates/openrvdas_logger_manager.conf.j2 b/roles/supervisor/templates/openrvdas_logger_manager.conf.j2 index d52bc37..42558b4 100644 --- a/roles/supervisor/templates/openrvdas_logger_manager.conf.j2 +++ b/roles/supervisor/templates/openrvdas_logger_manager.conf.j2 @@ -2,7 +2,7 @@ ; Do not edit by hand; re-run the Ansible playbook to regenerate. [program:logger_manager] -command={{ install_root }}/openrvdas/venv/bin/python server/logger_manager.py --database django --data_server_websocket :8766 -v -V --no-console +command={{ install_root }}/openrvdas/venv/bin/python server/logger_manager.py --database django --data_server_websocket :{{ openrvdas_data_port }} -v -V --no-console environment=PATH="{{ install_root }}/openrvdas/venv/bin:/usr/bin:/usr/local/bin" directory={{ install_root }}/openrvdas autostart={{ openrvdas_autostart | lower }}