From 5b84844e9786e55ec38fe564b13bd39562d3207b Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Tue, 3 Feb 2026 09:07:06 -0600 Subject: [PATCH] doc update --- CLAUDE.md | 3 +- lib/towerops/devices/device.ex | 5 +- lib/towerops/organizations/organization.ex | 5 +- lib/towerops/sites/site.ex | 5 +- lib/towerops_web/live/help_live/index.ex | 409 +++++++++++++++++- .../live/help_live/index.html.heex | 12 +- ...60203144612_encrypt_mikrotik_usernames.exs | 32 ++ 7 files changed, 461 insertions(+), 10 deletions(-) create mode 100644 priv/repo/migrations/20260203144612_encrypt_mikrotik_usernames.exs 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 +
  • + <.link + patch={~p"/help?section=mikrotik"} + class={[ + "block px-3 py-2 rounded-md text-sm font-medium transition-colors", + if @active_section == "mikrotik" do + "bg-blue-50 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300" + else + "text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800" + end + ]} + > + MikroTik + +
  • @@ -626,6 +701,338 @@ defmodule ToweropsWeb.HelpLive.Index do + <% "mikrotik" -> %> +
    +

    + MikroTik Configuration +

    + +
    +

    + 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. +

    + +
    +
    +
    + <.icon + name="hero-information-circle" + class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" + /> +
    +

    + Read-Only Access +

    +

    + 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. +

    +
    +
    +
    + +
    +
    + <.icon + name="hero-server" + class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" + /> +
    +

    + Cloud or Agent-Based Connections +

    +

    + 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. +

    +
    +
    +
    +
    + +

    + Security Best Practices +

    + +

    + 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. +

    + +

    + Creating a Read-Only User +

    + +

    + 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 %> +
    +
    +
    + <.button + type="button" + phx-click="generate_password" + disabled={@password_generating} + > + <%= if @password_generating do %> + <.icon name="hero-arrow-path" class="h-4 w-4 mr-1 animate-spin" /> + Generating... + <% else %> + <.icon name="hero-sparkles" class="h-4 w-4 mr-1" /> + Regenerate Random Password + <% end %> + + <%= if @generated_password do %> +

    + ⚠️ 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 + + + +
    +
    +
    +
    + +

    + Permissions Explained +

    + +

    + 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 +
    +
    + +
    +
    + <.icon + name="hero-information-circle" + class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" + /> +
    +

    + Important Notes +

    +
      +
    • The read-only user cannot modify device configuration
    • +
    • No write, reboot, or sensitive permissions are granted
    • +
    • Use a strong, unique password for the monitoring account
    • +
    • Consider restricting SSH access by source IP if possible
    • +
    +
    +
    +
    + +

    + Configuring in Towerops +

    + +

    + After creating the read-only user, configure SSH credentials in Towerops: +

    + +
    +
    +

    + Organization-Level Configuration +

    +

    + Navigate to + <.code>Settings + → + <.code>Organization + to set default + SSH credentials for all MikroTik devices in your organization. +

    +
    + +
    +

    + Site-Level Configuration +

    +

    + Navigate to + <.code>Sites + → select a site → + <.code>Edit + to override + credentials for all devices at a specific location. +

    +
    + +
    +

    + Device-Level Configuration +

    +

    + When editing a MikroTik device, you can specify unique SSH credentials that override + organization and site defaults. +

    +
    +
    + +
    +
    + <.icon + name="hero-shield-check" + class="h-5 w-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" + /> +
    +

    + Security Recommendations +

    +
      +
    • Always use SSL/TLS for SSH connections (API-SSL on port 8729)
    • +
    • Store credentials at the organization or site level when possible
    • +
    • Rotate passwords periodically following your security policies
    • +
    • Monitor access logs for unauthorized SSH connection attempts
    • +
    • + Consider using SSH keys instead of passwords (if supported by your setup) +
    • +
    +
    +
    +
    +
    +
    <% _ -> %>

    diff --git a/lib/towerops_web/live/help_live/index.html.heex b/lib/towerops_web/live/help_live/index.html.heex index 54a12edc..c38b9a07 100644 --- a/lib/towerops_web/live/help_live/index.html.heex +++ b/lib/towerops_web/live/help_live/index.html.heex @@ -4,12 +4,20 @@ current_scope={@current_scope} active_page="help" > - <.help_content active_section={@active_section} /> + <.help_content + active_section={@active_section} + generated_password={@generated_password} + password_generating={@password_generating} + /> <% else %>
    - <.help_content active_section={@active_section} /> + <.help_content + active_section={@active_section} + generated_password={@generated_password} + password_generating={@password_generating} + />
    <% end %> diff --git a/priv/repo/migrations/20260203144612_encrypt_mikrotik_usernames.exs b/priv/repo/migrations/20260203144612_encrypt_mikrotik_usernames.exs new file mode 100644 index 00000000..4435f4af --- /dev/null +++ b/priv/repo/migrations/20260203144612_encrypt_mikrotik_usernames.exs @@ -0,0 +1,32 @@ +defmodule Towerops.Repo.Migrations.EncryptMikrotikUsernames do + use Ecto.Migration + + def up do + # Change mikrotik_username from string to binary for encryption + # Note: This will clear any existing usernames since we're moving from + # plaintext string to encrypted binary. Users will need to re-enter credentials. + + # Organizations + execute "ALTER TABLE organizations ALTER COLUMN mikrotik_username TYPE bytea USING NULL" + + # Sites + execute "ALTER TABLE sites ALTER COLUMN mikrotik_username TYPE bytea USING NULL" + + # Devices + execute "ALTER TABLE devices ALTER COLUMN mikrotik_username TYPE bytea USING NULL" + end + + def down do + # Revert back to string type + # Note: Encrypted data will be cleared when reverting + + # Organizations + execute "ALTER TABLE organizations ALTER COLUMN mikrotik_username TYPE varchar USING NULL" + + # Sites + execute "ALTER TABLE sites ALTER COLUMN mikrotik_username TYPE varchar USING NULL" + + # Devices + execute "ALTER TABLE devices ALTER COLUMN mikrotik_username TYPE varchar USING NULL" + end +end