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
- No inbound ports on managed hosts — agents connect outbound to the gateway via WebSocket
- No extra daemon required — the agent is a single binary managed by systemd
- PAM authentication — uses existing system accounts (local, LDAP, SSSD, FreeIPA)
- Role-based access — admin vs. read-only, based on sudo group membership
2Requirements
Panel host
| Requirement | Details |
|---|---|
| OS | Linux (Debian, Ubuntu, RHEL, Fedora, Arch — tested on Debian 12, Fedora 43) |
| CPU | x86_64 |
| RAM | 512 MB minimum (1 GB recommended during build) |
| Network | Port 9090 accessible from browsers and managed hosts |
Managed hosts (agent only)
| Requirement | Details |
|---|---|
| OS | Linux (any distribution with systemd) |
| CPU | x86_64 |
| RAM | ~20 MB for the agent process |
| Network | Outbound 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.
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
| Scenario | Config |
|---|---|
| Plain HTTP | TENODERA_GATEWAY_URL=http://... |
| HTTPS with CA-signed cert | TENODERA_GATEWAY_URL=https://... |
| HTTPS with self-signed cert | https://... + TENODERA_AGENT_ACCEPT_INSECURE=1 |
6Authentication & Access Control
Tenodera uses PAM authentication — the same credentials as the system. No separate user database.
| Role | Who | Permissions |
|---|---|---|
| Admin | Users in sudo, wheel, admin, or admins group (admins = FreeIPA default) | Full read/write access |
| Read-only | All other PAM users | Monitor 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:
- A warning banner appears at the top of the UI when that host is active
- A no account badge appears on the host card in Management → Enrolled
- Read-only features (Dashboard, Storage, Networking, Logs) still work — the agent collects data independently
- Operations requiring
sudo(service restart, package install, firewall changes) will fail - The Terminal is non-functional — PTY session requires a valid system account
| Setting | Default | Config key |
|---|---|---|
| Idle timeout | 15 min | TENODERA_IDLE_TIMEOUT |
| Max session lifetime | 4 h | — |
| Brute-force protection | 20 / 5 min / IP | TENODERA_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.
- Hosts enter pending state on first connect — approve in Hosts → Pending
- Use bootstrap tokens for unattended enrollment — Hosts → Tokens
- Agents reconnect automatically with exponential backoff on disconnect
- Host roles: set in
agent.cnfor via the Management UI (no restart required)
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 charts — 90-point rolling history:
| Chart | Metrics |
|---|---|
| CPU | User %, system % — stacked area chart |
| Load average | 1-min, 5-min, 15-min load |
| Memory | Used % (MemTotal − MemAvailable) |
| Disk I/O | Read KB/s, Write KB/s |
| Network I/O | RX 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.
- Opens a shell as the authenticated user — agent drops to user's UID/GID (no root shell exposed)
- 10 000-line scrollback buffer
- Font: JetBrains Mono → Fira Code → Cascadia Code → monospace, 14 px
- Auto-copy: selecting text copies to clipboard (requires HTTPS or localhost)
- Auto-resize with browser window (xterm.js FitAddon)
- Connection is a WebSocket channel multiplexed through the gateway — no direct SSH
8.3 Services
Manage systemd units on the selected host.
Services tab
Lists all loaded systemd units with:
| Column | Description |
|---|---|
| Unit | Unit filename (e.g. sshd.service) |
| Description | Unit description string |
| Active | active / activating / deactivating / inactive / failed |
| State | running / exited / dead / waiting / mounted |
| Load | loaded / 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):
| Field | Description |
|---|---|
| Username | Login name |
| UID / GID | Numeric user and primary group ID |
| Full name | GECOS field |
| Home | Home directory path |
| Shell | Login shell |
| Groups | All groups the user belongs to |
| Status | Active or Locked |
| Last login | Timestamp of most recent login |
| Source | local 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).
- Installed tab — lists all installed packages: name, version, repository source; filter by name
- Search tab Admin — search available packages; install from results
- Updates tab Admin — lists available updates; Update all runs full system upgrade; live output streamed to UI
- Repositories tab Admin — view and manage configured repositories; enable/disable repos
8.6 Storage
Monitor block devices and disk I/O on the selected host.
Block device tree — hierarchical lsblk-style tree: device name, size, type (disk/part/lvm/raid), mount points, used/free/use% for mounted devices.
Disk I/O chart — real-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.
- Overview tab — real-time traffic charts per interface: RX and TX lines (up to 8 interfaces, color-coded), configurable interval, 90-point rolling history
- Firewall tab Admin — all detected backends (ufw, firewalld, nftables): active state, rules; add/remove rules; supports mixed environments
- Interfaces tab Admin — state, MAC, MTU, flags, IPv4/IPv6; bring interface up/down; VPN connections listed separately
- Logs tab — network-related log entries from the system journal
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:
| Column | Description |
|---|---|
| Name | Container name |
| Image | Image the container was created from |
| State | running / paused / restarting / created / exited / dead |
| Status | Human-readable status |
| Ports | Exposed port mappings |
| Owner | user (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
| Mode | When | Scope |
|---|---|---|
| Limited | No superuser password active | Home directory only (/home/<user>) |
| Administrative | Superuser password active | Full 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.
8.10 Logs (Journal)
Query the systemd journal on the selected host.
- Unit filter — filter by unit name (debounced 400 ms)
- Line count — configurable (default: 100)
- Each entry: timestamp, unit name, priority, message text
- Superuser password enables access to protected journal entries
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 view — last N lines (default: 100, configurable).
Search view — full-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
| Field | Description |
|---|---|
| Installed | Whether kdump is installed (kdump-tools on Debian, kexec-tools on RHEL) |
| Service | Service name, active state, enabled state |
| Crash kernel loaded | Whether a secondary kernel is loaded in reserved memory |
| Reserved memory | Bytes reserved for the crash kernel |
| Kernel version | Running 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.
- Resolver tab Admin — display and edit
/etc/resolv.conf: nameservers, search domains - /etc/hosts tab Admin — display and edit
/etc/hosts - Lookup tab — interactive
diglookup: hostname or IP, query type (A, AAAA, MX, NS, TXT, CNAME, PTR, SOA, SRV) - systemd-resolved tab Admin — shows systemd-resolved status, configuration, allows service restart
8.15 Certificates
Manage TLS certificates on the selected host.
Certificates tab
Scans common certificate locations:
| Field | Description |
|---|---|
| CN | Certificate subject Common Name |
| Issuer | Issuer CN and organization |
| Valid from / until | notBefore / notAfter dates |
| Days remaining | Days until expiry (highlighted red when close) |
| SANs | Subject Alternative Names |
| Is CA | Whether this is a CA certificate |
| Source | File 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:
| Column | Description |
|---|---|
| Hostname | Agent hostname from Hello handshake |
| Status | Online / offline indicator |
| IP | Remote IP of the agent connection |
| Added | Date first enrolled |
| Uptime | Agent process uptime (online only) |
| Roles | Role 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
| Path | Description |
|---|---|
/usr/local/bin/tenodera-gateway | Gateway binary |
/usr/local/bin/tenodera-pam-helper | PAM helper (setuid root) |
/usr/local/bin/tenodera-agent | Agent binary (setuid root) |
/usr/share/tenodera/ui/ | Built UI assets |
/etc/tenodera/tenodera.cnf | Gateway configuration |
/etc/tenodera/agent.cnf | Agent configuration |
/var/lib/tenodera-gw/hosts.json | Enrolled hosts registry + Ed25519 public keys |
/var/log/tenodera_audit.log | Audit 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
- Gateway — Rust, Axum 0.8. Serves React UI, authenticates via PAM subprocess, routes WebSocket channels. Starts as root, drops to
tenodera-gwuser after binding. - Agent — Rust, Tokio. ~20 MB. 39 operation types. Setuid root, drops to user UID/GID for terminal.
- Protocol — Shared Rust library. JSON messages with type field. Protocol version 2.
Wire protocol
Messages are JSON objects with a type field:
| Type | Direction | Description |
|---|---|---|
hello | agent → gateway | Agent announces hostname, protocol version, and Ed25519 public key |
challenge | gateway → agent | Gateway sends a 32-byte random nonce for the agent to sign |
challenge_response | agent → gateway | Agent returns the Ed25519 signature of the challenge |
hello_ack | gateway → agent | Gateway acknowledges after successful authentication; carries gateway ID |
open | browser → gateway | Open a named channel (e.g. system_info, terminal_pty) |
ready | agent → browser | Agent acknowledges streaming channel |
data | bidirectional | Channel payload |
control | bidirectional | Signals (PTY resize, etc.) |
close | bidirectional | Clean or error channel close |
auth / authresult | browser → gateway | Session authentication phase |
ping / pong | bidirectional | Keepalive |
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.
- PAM auth via isolated
tenodera-pam-helpersubprocess — gateway never handles PAM directly - Ed25519 TOFU agent authentication — 32-byte challenge-response; SECURITY ALERT on key mismatch
- TLS required by default; HSTS (
max-age=63072000; includeSubDomains) - WebSocket Origin validation, CSRF protection on all mutating HTTP requests
- Full HTTP security headers: CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy
- Gateway injects
_user/_roleinto every channel message — agent never trusts client-supplied identity - Core dumps disabled; systemd hardening: NoNewPrivileges, PrivateTmp, ProtectSystem=strict
- Audit log: all logins, logouts, privilege escalations →
/var/log/tenodera_audit.log
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:
- SSSD / FreeIPA / LDAP authenticate via Unix sockets (
/var/run/sssd/public/sss_cli) and shared libraries (pam_sss.so,pam_winbind.so). Mounting only the user files is not enough — you also need the live sockets, PAM modules, and matching library versions. - PAM modules (
pam_unix.so,pam_sss.so) are shared libraries linked against specific glibc versions. A container built on a different base image will have the wrong library versions, causingpam_authenticate()to segfault or silently fail. /etc/pam.d/tenoderais installed on the host by the Tenodera installer. Inside the container this file does not exist unless explicitly mounted.
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:
- User namespace remapping — when Docker remaps UIDs (default in rootless Docker), the setuid bit is ignored because the file is owned by a remapped UID, not real root.
no-new-privileges— Docker's default seccomp profile and most Kubernetes pod security policies setPR_SET_NO_NEW_PRIVS, which is inherited by all child processes and prevents setuid binaries from gaining elevated privileges.
3. The panel's purpose requires unrestricted host access
Tenodera manages the OS it runs on. Full feature set requires access to:
| Feature | Required host access |
|---|---|
| Services | systemd D-Bus socket (/run/systemd/private) |
| Packages | /var/lib/dpkg, /var/lib/rpm, package manager lock files |
| Storage | Block device nodes (/dev/sd*, /dev/nvme*), /proc/mounts |
| Networking | /proc/net/*, nftables/iptables netlink sockets |
| Containers | Docker / Podman socket (/var/run/docker.sock) |
| Files | Root filesystem (/etc, /var, /home, arbitrary paths) |
| Logs | /var/log/*, /run/log/journal/* |
| Terminal | PTY 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
- TLS certificate not found — set
TENODERA_ALLOW_UNENCRYPTED=1or runcd panel && sudo make tls-selfsigned - Port 9090 in use — change
TENODERA_BIND_PORTintenodera.cnf
Agent not connecting to gateway
journalctl -u tenodera-agent -e
- Wrong gateway URL — check
TENODERA_GATEWAY_URLin/etc/tenodera/agent.cnf - Firewall blocking — managed host must reach panel on TCP 9090 (outbound only)
- TLS cert failing — set
TENODERA_AGENT_ACCEPT_INSECURE=1for self-signed certs
Agent not in Hosts → Pending
- Verify
TENODERA_GATEWAY_URL - Bootstrap token invalid or expired — check Hosts → Tokens
- Pending limit reached (max 100)
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
- Check agent is running:
sudo systemctl status tenodera-agent - Check agent logs:
journalctl -u tenodera-agent -f - Look for
Hellohandshake in the logs — if it's there, the gateway is connected - Verify
TENODERA_GATEWAY_URLpoints to the correct panel host
Enable debug logging
# In /etc/tenodera/tenodera.cnf:
RUST_LOG=tenodera_gateway=debug
sudo systemctl restart tenodera
journalctl -u tenodera -f
Login fails
- Verify the user exists on the panel host (not the managed host)
- Verify PAM:
pamtester tenodera <username> authenticate - Check audit log:
sudo tail -f /var/log/tenodera_audit.log