Tenodera / Documentation
GitHub

Tenodera Documentation

Complete reference for installation, configuration, and all features.

1Overview

Tenodera is a self-hosted Linux server administration panel. It provides a real-time web interface for managing local and remote Linux servers — terminal access, service control, user management, package management, networking, storage, containers, logs, and more.

Browser ──WSS──> Gateway (:9090) <──WS── tenodera-agent (remote host)
                                 <──WS── tenodera-agent (localhost)

Key design principles

2Requirements

Panel host

RequirementDetails
OSLinux (Debian, Ubuntu, RHEL, Fedora, Arch — tested on Debian 12, Fedora 43)
CPUx86_64
RAM512 MB minimum (1 GB recommended during build)
NetworkPort 9090 accessible from browsers and managed hosts

Managed hosts (agent only)

RequirementDetails
OSLinux (any distribution with systemd)
CPUx86_64
RAM~20 MB for the agent process
NetworkOutbound TCP to panel host on port 9090

3Installation

3.1 Panel (gateway + UI)

Run on the host that will serve the web interface:

curl -sSfL https://raw.githubusercontent.com/tenodera-io/tenodera/main/tenodera.sh | sudo bash

After install, log in at http://<host>:9090 with any PAM system user.

⚡ The panel starts in HTTP mode by default. Configure TLS before exposing to untrusted networks — see §4.2.

3.2 Agent (managed hosts)

Run on each host you want to manage:

curl -sSfL https://raw.githubusercontent.com/tenodera-io/tenodera/main/tenodera-agent.sh \
  | sudo bash -s -- --gateway http://<panel-host>:9090

The host enters pending state — approve it in Hosts → Pending.

For unattended installs, generate a bootstrap token in Hosts → Tokens:

curl -sSfL https://raw.githubusercontent.com/tenodera-io/tenodera/main/tenodera-agent.sh \
  | sudo bash -s -- --gateway http://<panel-host>:9090 --token <bootstrap-token>

3.3 Build from source

git clone https://github.com/tenodera-io/tenodera
cd Tenodera
cd panel && sudo make all   # panel host
cd agent && sudo make all   # managed hosts

4Panel Configuration

4.1 Gateway config reference

Edit /etc/tenodera/tenodera.cnf, then restart the service:

# ── Network ──────────────────────────────────────────────
TENODERA_BIND_ADDR=0.0.0.0        # Listen address
TENODERA_BIND_PORT=9090            # Listen port

# ── External URL (reverse proxy / public hostname) ───────
# TENODERA_EXTERNAL_URL=https://panel.example.com

# ── TLS ──────────────────────────────────────────────────
TENODERA_TLS_CERT=/etc/tenodera/tls/cert.pem
TENODERA_TLS_KEY=/etc/tenodera/tls/key.pem
# TENODERA_ALLOW_UNENCRYPTED=1   # dev only!

# ── Security ─────────────────────────────────────────────
TENODERA_IDLE_TIMEOUT=900          # Session idle timeout (seconds)
TENODERA_MAX_STARTUPS=20           # Max failed logins per IP per 5 min

# ── Logging ──────────────────────────────────────────────
RUST_LOG=tenodera_gateway=info

4.2 TLS setup

Self-signed (dev / testing)

cd panel && sudo make tls-selfsigned

Let's Encrypt

sudo certbot certonly --standalone -d panel.example.com

# Then in tenodera.cnf:
TENODERA_TLS_CERT=/etc/letsencrypt/live/panel.example.com/fullchain.pem
TENODERA_TLS_KEY=/etc/letsencrypt/live/panel.example.com/privkey.pem

# Restart on renewal:
echo 'systemctl restart tenodera' | sudo tee /etc/letsencrypt/renewal-hooks/deploy/tenodera.sh
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/tenodera.sh

4.3 Reverse proxy

Set TENODERA_EXTERNAL_URL in tenodera.cnf. nginx example:

location / {
    proxy_pass http://127.0.0.1:9090;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_read_timeout 3600s;
}

5Agent Configuration

Edit /etc/tenodera/agent.cnf on each managed host, then restart the agent:

# Gateway WebSocket endpoint
# http://  → plain WebSocket
# https:// → encrypted WebSocket
TENODERA_GATEWAY_URL=http://<panel-host>:9090

# Bootstrap token — skip pending approval on first connect
# TENODERA_BOOTSTRAP_TOKEN=<token>

# Skip TLS cert verification (self-signed certs only)
# TENODERA_AGENT_ACCEPT_INSECURE=1

# Host roles (grouping label)
# role=web
ScenarioConfig
Plain HTTPTENODERA_GATEWAY_URL=http://...
HTTPS with CA-signed certTENODERA_GATEWAY_URL=https://...
HTTPS with self-signed certhttps://... + TENODERA_AGENT_ACCEPT_INSECURE=1

6Authentication & Access Control

Tenodera uses PAM authentication — the same credentials as the system. No separate user database.

RoleWhoPermissions
AdminUsers in sudo, wheel, admin, or admins group (admins = FreeIPA default)Full read/write access
Read-onlyAll other PAM usersMonitor only — no write operations

Role is determined at login via sudo -l -U <user>. LDAP/SSSD/FreeIPA users work transparently if PAM is configured for them.

Account presence on managed hosts

Tenodera authenticates you against the panel host — managed hosts are separate machines and may not have your account. The panel checks via getent passwd (NSS-aware: local, SSSD, FreeIPA, LDAP). If your account is absent on a host:

SettingDefaultConfig key
Idle timeout15 minTENODERA_IDLE_TIMEOUT
Max session lifetime4 h
Brute-force protection20 / 5 min / IPTENODERA_MAX_STARTUPS

7Multi-Host Management

Each managed host runs tenodera-agent which connects outbound to the gateway. The host selector (top-left) switches context — all pages instantly show data from the selected host.

8Feature Reference

This section describes every page and sub-tab in the Tenodera UI. Write operations (Admin) require membership in sudo, wheel, admin, or admins group. Many write operations also require entering the superuser password — stored encrypted in browser sessionStorage for the session duration.

8.1 Dashboard

Live overview of the selected host's system state. All metrics streamed over WebSocket — no page refresh needed.

System information panel: hostname, OS (name/version/ID), uptime, boot time, timezone, CPU model, core/thread count, frequency (MHz), architecture, kernel version, CPU temperatures (per sensor, with critical threshold).

Real-time charts90-point rolling history:

ChartMetrics
CPUUser %, system % — stacked area chart
Load average1-min, 5-min, 15-min load
MemoryUsed % (MemTotal − MemAvailable)
Disk I/ORead KB/s, Write KB/s
Network I/ORX bytes/s, TX bytes/s (aggregated)

Disk partitions: device, mount point, filesystem type, total size, used, free, use%.

Network interfaces: name, state (up/down), MAC, speed (Mbps), IPv4/IPv6, RX/TX bytes and packets, error counts.

Top processes: PID, name, CPU%, memory%, user, state — sorted by CPU descending.

8.2 Terminal Admin

Full PTY running in the browser, powered by xterm.js. Visible only to admin users.

⚡ The terminal requires a valid system account on the managed host. If your account does not exist there, the PTY session will be rejected. See §6 for details.

8.3 Services

Manage systemd units on the selected host.

Services tab

Lists all loaded systemd units with:

ColumnDescription
UnitUnit filename (e.g. sshd.service)
DescriptionUnit description string
Activeactive / activating / deactivating / inactive / failed
Staterunning / exited / dead / waiting / mounted
Loadloaded / not-found / masked

Filter by unit name. Sort by Active or State. Color coding: green = active/running, red = failed. Admin actions: Start, Stop, Restart, Enable, Disable.

Timers tab

Lists all systemd timer units: unit name, description, active/sub state, next run, last run, enabled state, triggered unit.

8.4 Users & Groups

Manage system users and groups on the selected host.

Users tab

Lists all users visible through NSS (local /etc/passwd, LDAP, SSSD, FreeIPA):

FieldDescription
UsernameLogin name
UID / GIDNumeric user and primary group ID
Full nameGECOS field
HomeHome directory path
ShellLogin shell
GroupsAll groups the user belongs to
StatusActive or Locked
Last loginTimestamp of most recent login
Sourcelocal or LDAP/SSSD source

Admin actions: Lock account, Unlock account, Delete user, Change password.

Groups tab

Lists all NSS groups: group name, GID, member list, system group flag.

Create Account tab Admin

Create a new local user: username, password, full name, home directory, login shell, initial group memberships.

8.5 Packages

Manage software packages. Backend auto-detected: apt (Debian/Ubuntu), dnf (RHEL/Fedora), pacman (Arch).

8.6 Storage

Monitor block devices and disk I/O on the selected host.

Block device treehierarchical lsblk-style tree: device name, size, type (disk/part/lvm/raid), mount points, used/free/use% for mounted devices.

Disk I/O chartreal-time read/write KB/s. Configurable interval: 1 s, 5 s, 10 s, 30 s, 1 min, 5 min, 10 min, 30 min. 90-point rolling history. Interval saved in localStorage.

8.7 Networking

Monitor and manage network configuration.

8.8 Containers

Manage Docker and Podman containers on the selected host.

Containers tab

Lists all containers from both user namespace (rootless) and root namespace:

ColumnDescription
NameContainer name
ImageImage the container was created from
Staterunning / paused / restarting / created / exited / dead
StatusHuman-readable status
PortsExposed port mappings
Owneruser (rootless) or root

Sorted: running first, then user before root. Admin: Start, Stop, Remove.

Images tab Admin

Repository, tags, size, creation date, owner. Remove image (fails gracefully if in use).

8.9 Files

File manager: browse, view, edit, create, and delete files.

Access modes

ModeWhenScope
LimitedNo superuser password activeHome directory only (/home/<user>)
AdministrativeSuperuser password activeFull filesystem via sudo

Restriction enforced at the agent level. Symlink targets are resolved (canonicalized) before applying the home-directory restriction.

Features: navigation breadcrumb, path autocomplete (Administrative mode), sorted listing (dirs first), 200-line paginated viewer, inline editor, new file creation, delete with confirmation.

⚡ Directory deletion is not yet implemented — use the Terminal for recursive directory removal.

8.10 Logs (Journal)

Query the systemd journal on the selected host.

8.11 Log Files

Browse and search plain-text log files in /var/log.

File list: all files in /var/log recursively; filename, path, size, last-modified. Root-owned non-world-readable files require superuser password.

Tail viewlast N lines (default: 100, configurable).

Search viewfull-text search with Before/After context lines, max results, date/time range. When date/time range is set without a keyword, all lines within the window are returned.

Tenodera audit log

Events: login attempt (username, IP, success/failure), logout (duration), privilege escalation (superuser password activation).

Log rotation (/etc/logrotate.d/tenodera): daily, keep 3, max 1 GB, gzip (delaycompress), copytruncate — no service restart required.

8.12 Cron Jobs

View and manage cron jobs. Sources: /etc/crontab, /etc/cron.d/*, per-user crontabs.

Each entry: source, schedule (raw + human-readable description), user, command, comment.

Admin: click a source to open in editor; save writes back to the file.

8.13 Kernel Dump (kdump)

Monitor kernel crash dump configuration.

Status panel

FieldDescription
InstalledWhether kdump is installed (kdump-tools on Debian, kexec-tools on RHEL)
ServiceService name, active state, enabled state
Crash kernel loadedWhether a secondary kernel is loaded in reserved memory
Reserved memoryBytes reserved for the crash kernel
Kernel versionRunning kernel version

Crash dumps browser: lists directories (typically /var/crash) with name, type, size, vmcore/dmesg presence, timestamp. Expand to see files. View dmesg inline.

8.14 DNS

Manage DNS configuration.

8.15 Certificates

Manage TLS certificates on the selected host.

Certificates tab

Scans common certificate locations:

FieldDescription
CNCertificate subject Common Name
IssuerIssuer CN and organization
Valid from / untilnotBefore / notAfter dates
Days remainingDays until expiry (highlighted red when close)
SANsSubject Alternative Names
Is CAWhether this is a CA certificate
SourceFile path where certificate was found

Admin: Import certificate (paste PEM cert + key; validated before saving); Remove certificate (requires superuser password).

Trust Store tab Admin

List trusted CA certificates; add/remove trusted CAs.

Let's Encrypt tab

Lists Certbot-managed certificates: domain, covered domains, expiry, days remaining, cert and key paths.

Self-Signed tab Admin

Generate a self-signed TLS certificate: Common Name, validity period. Cert and key saved to specified paths.

8.16 Management Admin

Admin-only panel for managing all connected agents. Three tabs: Enrolled, Pending, and Tokens.

Enrolled tab

All hosts that have completed the TOFU handshake:

ColumnDescription
HostnameAgent hostname from Hello handshake
StatusOnline / offline indicator
IPRemote IP of the agent connection
AddedDate first enrolled
UptimeAgent process uptime (online only)
RolesRole labels

Badges: local (green) = panel host; active (blue) = currently selected; no account (yellow) = your account absent on this host. Distro name shown in card corner.

Actions: Switch (make active), Role (tag-style input), Restart (sends restart to agent service), Remove (host re-enters pending on next connect).

Pending tab

Agents connected but not yet approved. Refreshes every 5 s. Shows hostname, fingerprint (SHA-256), IP, waiting time. Approve: optionally enter display name; gateway saves key and sends HelloAck. Entries time out after 24 h if not approved.

Tokens tab

Bootstrap tokens for unattended enrollment. Options: TTL (1 h–30 days; default 24 h), single-use, hostname binding, re-enroll (key rotation). After creation, ready-to-use install command generated. Revoke: immediately invalidate a token.

9Service Management

# Panel host
sudo systemctl status tenodera
sudo systemctl restart tenodera
journalctl -u tenodera -f

# Agent (all hosts)
sudo systemctl status tenodera-agent
sudo systemctl restart tenodera-agent
journalctl -u tenodera-agent -f

Installed files

PathDescription
/usr/local/bin/tenodera-gatewayGateway binary
/usr/local/bin/tenodera-pam-helperPAM helper (setuid root)
/usr/local/bin/tenodera-agentAgent binary (setuid root)
/usr/share/tenodera/ui/Built UI assets
/etc/tenodera/tenodera.cnfGateway configuration
/etc/tenodera/agent.cnfAgent configuration
/var/lib/tenodera-gw/hosts.jsonEnrolled hosts registry + Ed25519 public keys
/var/log/tenodera_audit.logAudit log

10Health & Monitoring

GET /api/health        # → { status, sessions, uptime_secs, version }
GET /api/health/ready  # → 200 OK | 503 (readiness probe)

GET /api/health

Returns a JSON object:

{
  "status": "ok",
  "sessions": 2,
  "uptime_secs": 3600,
  "version": "0.1.0"
}

GET /api/health/ready

Returns 200 OK when the agent binary exists and is executable, 503 Service Unavailable otherwise. Use as a readiness probe in load balancers or container orchestration (e.g. Kubernetes).

11Architecture

                  [ Browser ]
                       |
                       | HTTPS / WSS  (channel-multiplexed JSON)
                       |
                  [ Gateway ]   Axum HTTP/WS · PAM auth · session management
                 /      |      \
   outbound WS  /       |       \  outbound WS
               /        |        \
        [ Agent ]  [ Agent ]  [ Agent ]   ...
          host-1      host-2     localhost

Wire protocol

Messages are JSON objects with a type field:

TypeDirectionDescription
helloagent → gatewayAgent announces hostname, protocol version, and Ed25519 public key
challengegateway → agentGateway sends a 32-byte random nonce for the agent to sign
challenge_responseagent → gatewayAgent returns the Ed25519 signature of the challenge
hello_ackgateway → agentGateway acknowledges after successful authentication; carries gateway ID
openbrowser → gatewayOpen a named channel (e.g. system_info, terminal_pty)
readyagent → browserAgent acknowledges streaming channel
databidirectionalChannel payload
controlbidirectionalSignals (PTY resize, etc.)
closebidirectionalClean or error channel close
auth / authresultbrowser → gatewaySession authentication phase
ping / pongbidirectionalKeepalive

Current protocol version: 2

Project structure

panel/                   Central server (gateway + UI)
  crates/gateway/        Axum HTTP/WS gateway, PAM auth, agent registry
  ui/                    React 19 + TypeScript SPA (Vite 6)
  Makefile               Build & install
  systemd/               tenodera.service

agent/                   Agent binary (deployed to managed hosts)
  src/handlers/          28 handler modules (39 registered handlers)
  Makefile               Build & install
  systemd/               tenodera-agent.service

protocol/                Shared message types (Rust library crate)

packaging/               RPM spec files (tenodera.spec, tenodera-agent.spec)

12Security

See SECURITY.md on GitHub for the full security model.

13Docker / Containerization

Tenodera cannot run correctly inside a standard Docker container. This is a fundamental architectural constraint — three independent mechanisms each break when containerized.

Why containerization fails

1. PAM authentication requires the host user database

Tenodera authenticates every login through the Linux PAM stack (/etc/pam.d/tenodera). PAM resolves users against NSS — /etc/passwd, /etc/shadow, and optionally SSSD, FreeIPA, or LDAP via their sockets and shared libraries. Inside a Docker container, PAM sees the container's own minimal user database, not the host's.

Even if you bind-mount /etc/passwd and /etc/shadow from the host, the following issues remain:

2. Setuid root subprocess conflicts with Docker's security model

PAM authentication and PTY user-switching run through tenodera-pam-helper, a binary that must be setuid root. The gateway spawns it as the unprivileged tenodera-gw user. Docker prevents this in two ways:

3. The panel's purpose requires unrestricted host access

Tenodera manages the OS it runs on. Full feature set requires access to:

FeatureRequired host access
Servicessystemd D-Bus socket (/run/systemd/private)
Packages/var/lib/dpkg, /var/lib/rpm, package manager lock files
StorageBlock device nodes (/dev/sd*, /dev/nvme*), /proc/mounts
Networking/proc/net/*, nftables/iptables netlink sockets
ContainersDocker / Podman socket (/var/run/docker.sock)
FilesRoot filesystem (/etc, /var, /home, arbitrary paths)
Logs/var/log/*, /run/log/journal/*
TerminalPTY devices (/dev/pts/*), process namespace
Users/etc/passwd, /etc/shadow, /etc/group (write)

Granting all of this requires --privileged. A --privileged container has the same kernel access as a root process on the host — container isolation is entirely removed.

Why --privileged is not a solution

Even with --privileged: PAM still sees the wrong user database unless /etc is fully bind-mounted; SSSD / FreeIPA sockets are still unavailable; library version mismatches persist. The result is a --privileged container that is harder to debug, slower to start, and less reliable than the binary running natively — with no isolation benefit.

What Docker can be used for: build environments

Docker is appropriate as a build environment — for CI/CD pipelines, cross-compilation, and release artifacts. The resulting binaries are deployed and run directly on the host, not inside Docker.

FROM rust:1.82-bookworm
RUN apt-get update && apt-get install -y libpam0g-dev nodejs npm
WORKDIR /build
COPY panel/ ./panel/
COPY protocol/ ./protocol/
RUN cd panel && cargo build --release
# docker cp <container>:/build/panel/target/release/tenodera-gateway ./

Recommended production setup

Install directly on the host using the one-line installer. For TLS, either configure the gateway directly (§4.2) or place nginx/Caddy in front of it (§4.3).

Host OS (bare metal or VM)
├── tenodera-gateway    (systemd, drops to tenodera-gw after bind)
├── tenodera-pam-helper (setuid root, spawned by gateway for auth)
├── tenodera-agent      (systemd, connects outbound to gateway)
└── nginx / Caddy       (optional reverse proxy for TLS termination)

14Uninstall

# Panel host (removes everything)
curl -sSfL https://raw.githubusercontent.com/tenodera-io/tenodera/main/tenodera.sh \
  | sudo bash -s -- --uninstall

# Managed hosts (agent only)
curl -sSfL https://raw.githubusercontent.com/tenodera-io/tenodera/main/tenodera-agent.sh \
  | sudo bash -s -- --uninstall

15Troubleshooting

Panel not starting

journalctl -u tenodera -e

Agent not connecting to gateway

journalctl -u tenodera-agent -e

Agent not in Hosts → Pending

Agent challenge rejected

Gateway logs show challenge verification failed. The agent's key store at /var/lib/tenodera/ may be corrupted or the key was rotated manually.

Fix: delete the agent's key store and restart:

sudo systemctl stop tenodera-agent
sudo rm -rf /var/lib/tenodera/
sudo systemctl start tenodera-agent

Then approve the host again in Hosts → Pending. If previously enrolled, remove it first via Hosts → Enrolled → Remove so the new key is accepted.

Host not appearing in UI

Enable debug logging

# In /etc/tenodera/tenodera.cnf:
RUST_LOG=tenodera_gateway=debug
sudo systemctl restart tenodera
journalctl -u tenodera -f

Login fails