diff --git a/CLAUDE.md b/CLAUDE.md index 1cf76d3d..e47602c9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -940,4 +940,5 @@ When writing new LiveView code or JavaScript hooks: - Compare - memory should not continuously grow - assets are rebuilt automatically on filesystem change - never try to run npm, it's not included in phoenix -- assets build themselves on file change, you do not need to ever run mix assets.build \ No newline at end of file +- assets build themselves on file change, you do not need to ever run mix assets.build +- never write custom one-off tests, if you're going to write a test, write a proper exunit test \ No newline at end of file diff --git a/lib/towerops/devices/device.ex b/lib/towerops/devices/device.ex index 9dde5b0d..4714ca90 100644 --- a/lib/towerops/devices/device.ex +++ b/lib/towerops/devices/device.ex @@ -14,6 +14,7 @@ defmodule Towerops.Devices.Device do alias Ecto.Association.NotLoaded alias Towerops.Agents.AgentAssignment + alias Towerops.Encrypted.Binary alias Towerops.Sites.Site alias Towerops.Snmp.Device, as: SnmpDevice @@ -40,8 +41,8 @@ defmodule Towerops.Devices.Device do field :last_snmp_poll_at, :utc_datetime # MikroTik API credentials (device-level overrides) - field :mikrotik_username, :string - field :mikrotik_password, Towerops.Encrypted.Binary + field :mikrotik_username, Binary + field :mikrotik_password, Binary field :mikrotik_port, :integer field :mikrotik_ssh_port, :integer field :mikrotik_use_ssl, :boolean diff --git a/lib/towerops/organizations/organization.ex b/lib/towerops/organizations/organization.ex index 89e1cdee..4d1df0d0 100644 --- a/lib/towerops/organizations/organization.ex +++ b/lib/towerops/organizations/organization.ex @@ -15,6 +15,7 @@ defmodule Towerops.Organizations.Organization do alias Ecto.Association.NotLoaded alias Towerops.Agents.AgentToken + alias Towerops.Encrypted.Binary alias Towerops.Organizations.Invitation alias Towerops.Organizations.Membership @@ -30,8 +31,8 @@ defmodule Towerops.Organizations.Organization do field :snmp_community, :string # MikroTik API credentials (organization-level defaults cascade to site → device) - field :mikrotik_username, :string - field :mikrotik_password, Towerops.Encrypted.Binary + field :mikrotik_username, Binary + field :mikrotik_password, Binary field :mikrotik_port, :integer, default: 8729 field :mikrotik_ssh_port, :integer, default: 22 field :mikrotik_use_ssl, :boolean, default: true diff --git a/lib/towerops/sites/site.ex b/lib/towerops/sites/site.ex index 0225755e..429d7974 100644 --- a/lib/towerops/sites/site.ex +++ b/lib/towerops/sites/site.ex @@ -11,6 +11,7 @@ defmodule Towerops.Sites.Site do alias Ecto.Association.NotLoaded alias Towerops.Agents.AgentToken alias Towerops.Devices.Device + alias Towerops.Encrypted.Binary alias Towerops.Organizations.Organization alias Towerops.Sites.Site @@ -27,8 +28,8 @@ defmodule Towerops.Sites.Site do field :snmp_community, :string # MikroTik API credentials (overrides organization default) - field :mikrotik_username, :string - field :mikrotik_password, Towerops.Encrypted.Binary + field :mikrotik_username, Binary + field :mikrotik_password, Binary field :mikrotik_port, :integer field :mikrotik_ssh_port, :integer field :mikrotik_use_ssl, :boolean diff --git a/lib/towerops_web/live/help_live/index.ex b/lib/towerops_web/live/help_live/index.ex index ab3551bf..058bb2e5 100644 --- a/lib/towerops_web/live/help_live/index.ex +++ b/lib/towerops_web/live/help_live/index.ex @@ -36,6 +36,13 @@ defmodule ToweropsWeb.HelpLive.Index do |> assign(:is_authenticated, is_authenticated) |> assign(:current_scope, current_scope) |> assign(:timezone, if(current_user, do: current_user.timezone, else: "UTC")) + |> assign(:generated_password, nil) + |> assign(:password_generating, false) + + # Generate initial password + if connected?(socket) do + send(self(), :fetch_random_password) + end {:ok, socket} end @@ -43,7 +50,58 @@ defmodule ToweropsWeb.HelpLive.Index do @impl true def handle_params(params, _url, socket) do section = Map.get(params, "section", "about") - {:noreply, assign(socket, active_section: section)} + + socket = + socket + |> assign(:active_section, section) + |> assign(:generated_password, nil) + |> assign(:password_generating, false) + + {:noreply, socket} + end + + @impl true + def handle_event("generate_password", _params, socket) do + socket = assign(socket, :password_generating, true) + send(self(), :fetch_random_password) + {:noreply, socket} + end + + @impl true + def handle_info(:fetch_random_password, socket) do + case generate_random_password() do + {:ok, password} -> + {:noreply, + socket + |> assign(:generated_password, password) + |> assign(:password_generating, false) + |> put_flash(:info, "Password generated successfully from random.org")} + + {:error, reason} -> + {:noreply, + socket + |> assign(:password_generating, false) + |> put_flash(:error, "Failed to generate password: #{reason}")} + end + end + + defp generate_random_password do + # Generate a 24-character truly random password from random.org + # Format: uppercase, lowercase, and digits + url = + "https://www.random.org/strings/?num=1&len=24&digits=on&upperalpha=on&loweralpha=on&unique=on&format=plain&rnd=new" + + case Req.get(url) do + {:ok, %{status: 200, body: body}} -> + password = String.trim(body) + {:ok, password} + + {:ok, %{status: status}} -> + {:error, "random.org returned status #{status}"} + + {:error, error} -> + {:error, Exception.message(error)} + end end defp get_current_user(socket, session) do @@ -69,6 +127,8 @@ defmodule ToweropsWeb.HelpLive.Index do end attr :active_section, :string, required: true + attr :generated_password, :string, default: nil + attr :password_generating, :boolean, default: false defp help_content(assigns) do ~H""" @@ -148,6 +208,21 @@ defmodule ToweropsWeb.HelpLive.Index do Remote Pollers +
+ Towerops supports read-only monitoring of MikroTik RouterOS devices via SSH. + In addition to SNMP polling, SSH access allows Towerops to retrieve detailed system information + and create configuration backups of your MikroTik devices for disaster recovery and audit purposes. +
+ ++ Towerops currently operates in read-only mode for MikroTik devices. Configuration changes, + reboots, and other administrative actions are not supported. This ensures your device + configurations remain unchanged by monitoring operations. +
++ Both SNMP polling and SSH connections to MikroTik devices can be performed from either + Towerops cloud infrastructure or your remote poller (agent). For publicly accessible devices, + cloud polling is the simplest option. For devices on private networks or when you prefer + to keep SSH credentials on your local network, deploy a remote poller. The device assignment + determines which poller handles both SNMP and SSH operations. +
++ For security purposes, it is strongly recommended to create a dedicated read-only user account + for Towerops rather than using an administrator account. This follows the principle of least + privilege and minimizes potential security risks. Even though Towerops only performs read-only + operations, limiting the account permissions provides defense in depth. +
+ ++ Connect to your MikroTik device via SSH or terminal and execute the following commands to + create a read-only user named <.code>towerops: +
+ ++ Step 1: Create a new user group with read-only permissions +
+
+ /user group add name=readonly policy=ssh,read,test,api
+
+
+
+ + Step 2: Create the monitoring user with a strong password +
+
+ /user add name=towerops password={if @generated_password,
+ do: @generated_password,
+ else: "YOUR_STRONG_PASSWORD"} group=readonly
+
+ <%= if @generated_password do %>
+
+
+ <% end %>
+ + ⚠️ This truly random password from random.org will only be shown once! +
+ <% end %> ++ Step 3: Verify the user was created successfully +
+
+ /user print detail where name=towerops
+
+
+
+ + The + <.code>readonly + group includes the following permissions. Note that while these + permissions are granted to the user account, Towerops only performs read operations and does + not make any configuration changes: +
+ +| + Permission + | ++ Description + | +
|---|---|
| + ssh + | ++ Allow SSH access to the device + | +
| + read + | ++ Allow viewing configuration and status (read-only) + | +
| + test + | ++ Allow executing diagnostic commands (ping, traceroute, etc.) + | +
| + api + | ++ Allow API access for automated monitoring + | +
+ After creating the read-only user, configure SSH credentials in Towerops: +
+ ++ Navigate to + <.code>Settings + → + <.code>Organization + to set default + SSH credentials for all MikroTik devices in your organization. +
++ Navigate to + <.code>Sites + → select a site → + <.code>Edit + to override + credentials for all devices at a specific location. +
++ When editing a MikroTik device, you can specify unique SSH credentials that override + organization and site defaults. +
+