Deploying Open Terminal for Open WebUI
Giving Open WebUI hands
Open WebUI is great as a chat interface, but by default it does not magically have access to administer the host systems around it. This becomes especially obvious when Open WebUI is running in Docker. Giving a container access to sudo inside the container does not mean it can administer the baremetal host. Container permissions and host permissions are separate things.
What I set up was a very stupid, but very workable approach: Open Terminal running baremetal on the target machine.
Instead of trying to make the Open WebUI container itself privileged, I installed open-terminal directly on the Linux host. Open WebUI can then connect to that Open Terminal service over HTTP using an API key. Open Terminal runs commands locally on the host, under a real Linux user, with a controlled sudoers allowlist. This is really handy for Operating Systems like Proxmox.
The result is that Open WebUI can perform useful admin tasks on the machine without needing the Open WebUI container itself to be fully privileged.
Final architecture
The setup looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
Open WebUI
|
| HTTP API request with API key
v
Open Terminal service
|
| Runs commands as csadmin
v
Linux host
|
| Optional passwordless sudo for allowlisted commands
v
Privileged host operations
On the host, Open Terminal is configured as a normal systemd service:
1
/etc/systemd/system/open-terminal.service
The API key is stored in:
1
/etc/open-terminal.env
The broad sudo allowlist is stored in:
1
/etc/sudoers.d/open-terminal-broad
By default, the Open Terminal service listens on:
1
0.0.0.0:8054
That means it accepts connections on all network interfaces. In Open WebUI, the endpoint should be added as either the hostname or IP of the target machine:
1
http://network-services:8054
or:
1
http://192.168.0.105:8054
Why baremetal matters
Open WebUI may be running in Docker, but the terminal capability I wanted was host-level command execution. If OpenWe bUI is containerized, there are a few ways to approach that problem:
- give the container more privileges,
- mount the Docker socket,
- SSH from the container to the host,
- or run a host-side command execution service.
The repeatable option here was the last one: run Open Terminal directly on the host.
That keeps Open WebUI itself from needing privileged container access. Open WebUI only needs to know the Open Terminal URL and API key.
How the service is configured
The deployment script creates a systemd service similar to this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Unit]
Description=Open Terminal for Open WebUI
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=username
WorkingDirectory=/home/username
Environment=PATH=/home/username/.local/bin:/usr/local/bin:/usr/bin:/bin
EnvironmentFile=/etc/open-terminal.env
ExecStart=/home/username/.local/bin/open-terminal run --host 0.0.0.0 --port 8054 --api-key ${OPEN_TERMINAL_API_KEY}
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
The important pieces are:
Service user
Open Terminal runs as:
1
username
This means normal commands execute with the permissions of username.
Working directory
The service uses the user’s home directory:
1
/home/username
Open Terminal executable
The Open Terminal executable is installed into the user’s local binary path:
1
/home/username/.local/bin/open-terminal
Listen address
The service binds to:
1
0.0.0.0
This allows Open WebUI to reach the service from another host or from inside a container.
Listen port
The default port is:
1
8054
API key
The API key is generated during deployment and stored here:
1
/etc/open-terminal.env
The file contains:
1
OPEN_TERMINAL_API_KEY=p_generatedkeyhere
The API key is then passed into the Open Terminal service when it starts.
How sudo access is configured
The setup intentionally does not use full unrestricted sudo like this:
1
username ALL=(ALL) NOPASSWD: ALL
That would effectively give Open WebUI full root access to the host.
Instead, the script creates a broad but allowlisted sudoers file:
1
/etc/sudoers.d/open-terminal-broad
The allowlist includes command groups for common administrative tasks:
- Docker management
- Journal log access
- selected
systemctlcommands - APT package management
- reading files under
/var/log - network and firewall inspection/changes
- basic system maintenance commands
This is still powerful, but it is safer than blanket NOPASSWD: ALL.
The sudoers file is validated during deployment with:
1
2
visudo -cf /etc/sudoers
visudo -cf /etc/sudoers.d/open-terminal-broad
That step is important because a broken sudoers file can lock out administrative access.
What the sudo allowlist permits
The broad allowlist created by the script includes these command aliases:
1
2
3
4
5
6
7
8
OT_DOCKER
OT_JOURNAL
OT_SYSTEMCTL_READ
OT_SYSTEMCTL_SERVICES
OT_APT
OT_LOGS
OT_NETWORK
OT_MAINT
In practical terms, that means Open Terminal can run commands such as:
1
sudo docker ps
1
sudo journalctl -n 100
1
sudo systemctl status docker
1
sudo apt update
1
sudo tail -n 100 /var/log/syslog
1
sudo ss -ltnp
1
sudo df -h
It does not grant unrestricted access to every command on the system.
Why this setup is useful
This gives Open WebUI a repeatable way to manage other machines in the environment.
For example, after deploying Open Terminal to a server, Open WebUI can be configured to connect to that server and perform allowlisted actions such as:
1
docker ps
1
journalctl -u some-service
1
systemctl status docker
1
apt update
Because Open Terminal is installed baremetal, these commands run against the real host, not inside the Open WebUI container.
Deployment script
Copy and paste the following script into the terminal on the target Linux host.
This script is designed to be tolerant of broken third-party APT repositories. If apt-get update fails because of a repository signing issue, the script continues as long as the required tools are already installed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
sudo bash -s <<'SCRIPT'
set -euo pipefail
# ============================================================
# Open Terminal baremetal replication script for Open WebUI
# APT-repo-error-tolerant version
# ============================================================
TERMINAL_USER="${TERMINAL_USER:-${SUDO_USER:-REPLACE_WITH_YOUR_DESIRED_USERNAME}}"
OPEN_TERMINAL_HOST="${OPEN_TERMINAL_HOST:-0.0.0.0}"
OPEN_TERMINAL_PORT="${OPEN_TERMINAL_PORT:-8054}"
INSTALL_SUDOERS="${INSTALL_SUDOERS:-1}"
ADD_DOCKER_GROUP="${ADD_DOCKER_GROUP:-1}"
echo "==> Target Open Terminal user: ${TERMINAL_USER}"
echo "==> Listen address: ${OPEN_TERMINAL_HOST}:${OPEN_TERMINAL_PORT}"
if [[ -z "${OPEN_TERMINAL_API_KEY:-}" ]]; then
OPEN_TERMINAL_API_KEY="$(python3 - <<'PY'
import secrets, string
alphabet = string.ascii_letters + string.digits
print('p_' + ''.join(secrets.choice(alphabet) for _ in range(32)))
PY
)"
fi
echo "==> Checking prerequisites"
if command -v apt-get >/dev/null 2>&1; then
echo "==> Running apt-get update, but continuing if third-party repos fail"
apt-get update || echo "WARNING: apt-get update failed. Continuing anyway."
echo "==> Installing prerequisites, best effort"
DEBIAN_FRONTEND=noninteractive apt-get install -y \
curl \
ca-certificates \
sudo \
openssl \
python3 \
python3-venv \
python3-pip \
iproute2 || echo "WARNING: apt-get install had problems. Continuing if required tools already exist."
fi
echo "==> Verifying required commands"
if ! command -v python3 >/dev/null 2>&1; then
echo "ERROR: python3 is required but not installed."
exit 1
fi
if ! command -v curl >/dev/null 2>&1; then
echo "ERROR: curl is required but not installed."
echo "Fix APT or install curl manually, then rerun."
exit 1
fi
if ! command -v sudo >/dev/null 2>&1; then
echo "ERROR: sudo is required but not installed."
exit 1
fi
echo "==> Ensuring user exists: ${TERMINAL_USER}"
if ! id "${TERMINAL_USER}" >/dev/null 2>&1; then
useradd --create-home --shell /bin/bash "${TERMINAL_USER}"
fi
USER_HOME="$(getent passwd "${TERMINAL_USER}" | cut -d: -f6)"
if [[ -z "${USER_HOME}" || ! -d "${USER_HOME}" ]]; then
echo "ERROR: Could not determine home directory for ${TERMINAL_USER}"
exit 1
fi
echo "==> User home: ${USER_HOME}"
echo "==> Adding ${TERMINAL_USER} to sudo group"
usermod -aG sudo "${TERMINAL_USER}" || true
if [[ "${ADD_DOCKER_GROUP}" == "1" ]] && getent group docker >/dev/null 2>&1; then
echo "==> Adding ${TERMINAL_USER} to docker group"
usermod -aG docker "${TERMINAL_USER}" || true
fi
echo "==> Installing uv for ${TERMINAL_USER}, if needed"
sudo -H -u "${TERMINAL_USER}" bash -lc '
set -e
export PATH="$HOME/.local/bin:$PATH"
if ! command -v uv >/dev/null 2>&1 && [[ ! -x "$HOME/.local/bin/uv" ]]; then
curl -LsSf https://astral.sh/uv/install.sh | sh
fi
'
echo "==> Installing/upgrading open-terminal for ${TERMINAL_USER}"
sudo -H -u "${TERMINAL_USER}" bash -lc '
set -e
export PATH="$HOME/.local/bin:$PATH"
uv tool install --upgrade open-terminal
'
if [[ ! -x "${USER_HOME}/.local/bin/open-terminal" ]]; then
echo "ERROR: open-terminal was not installed at ${USER_HOME}/.local/bin/open-terminal"
exit 1
fi
echo "==> open-terminal installed at ${USER_HOME}/.local/bin/open-terminal"
if [[ "${INSTALL_SUDOERS}" == "1" ]]; then
echo "==> Installing broad allowlisted sudoers policy"
cat > /etc/sudoers.d/open-terminal-broad <<EOF
# Broad allowlisted sudo access for Open Terminal / Open WebUI.
# This is intentionally not full NOPASSWD: ALL.
# Review and reduce for production.
Cmnd_Alias OT_DOCKER = /usr/bin/docker *
Cmnd_Alias OT_JOURNAL = /usr/bin/journalctl *
Cmnd_Alias OT_SYSTEMCTL_READ = /usr/bin/systemctl status *, /usr/bin/systemctl list-units *, /usr/bin/systemctl list-unit-files *, /usr/bin/systemctl is-active *, /usr/bin/systemctl is-enabled *
Cmnd_Alias OT_SYSTEMCTL_SERVICES = /usr/bin/systemctl start docker, /usr/bin/systemctl stop docker, /usr/bin/systemctl restart docker, /usr/bin/systemctl reload docker, /usr/bin/systemctl start nginx, /usr/bin/systemctl stop nginx, /usr/bin/systemctl restart nginx, /usr/bin/systemctl reload nginx
Cmnd_Alias OT_APT = /usr/bin/apt update, /usr/bin/apt upgrade *, /usr/bin/apt install *, /usr/bin/apt remove *, /usr/bin/apt purge *, /usr/bin/apt autoremove *
Cmnd_Alias OT_LOGS = /usr/bin/tail /var/log/*, /usr/bin/tail -n * /var/log/*, /usr/bin/head /var/log/*, /usr/bin/head -n * /var/log/*, /usr/bin/cat /var/log/*
Cmnd_Alias OT_NETWORK = /usr/sbin/ufw status *, /usr/sbin/ufw allow *, /usr/sbin/ufw deny *, /usr/sbin/ufw delete *, /usr/sbin/ufw reload, /usr/bin/ss *, /usr/bin/netstat *, /usr/sbin/ip *
Cmnd_Alias OT_MAINT = /usr/bin/df *, /usr/bin/du *, /usr/bin/free *, /usr/bin/ps *, /usr/bin/top *, /usr/bin/htop *, /usr/bin/kill *, /usr/bin/pkill *
${TERMINAL_USER} ALL=(root) NOPASSWD: OT_DOCKER, OT_JOURNAL, OT_SYSTEMCTL_READ, OT_SYSTEMCTL_SERVICES, OT_APT, OT_LOGS, OT_NETWORK, OT_MAINT
EOF
chmod 0440 /etc/sudoers.d/open-terminal-broad
echo "==> Validating sudoers policy"
visudo -cf /etc/sudoers
visudo -cf /etc/sudoers.d/open-terminal-broad
fi
echo "==> Writing API key environment file"
cat > /etc/open-terminal.env <<EOF
OPEN_TERMINAL_API_KEY=${OPEN_TERMINAL_API_KEY}
EOF
chmod 0600 /etc/open-terminal.env
echo "==> Writing systemd service"
cat > /etc/systemd/system/open-terminal.service <<EOF
[Unit]
Description=Open Terminal for Open WebUI
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=${TERMINAL_USER}
WorkingDirectory=${USER_HOME}
Environment=PATH=${USER_HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin
EnvironmentFile=/etc/open-terminal.env
ExecStart=${USER_HOME}/.local/bin/open-terminal run --host ${OPEN_TERMINAL_HOST} --port ${OPEN_TERMINAL_PORT} --api-key \${OPEN_TERMINAL_API_KEY}
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
chmod 0644 /etc/systemd/system/open-terminal.service
echo "==> Enabling and starting open-terminal.service"
systemctl daemon-reload
systemctl enable open-terminal.service
systemctl restart open-terminal.service
sleep 3
echo "==> Service status"
systemctl --no-pager --full status open-terminal.service || true
echo
echo "==> Listening socket check"
ss -ltnp | grep ":${OPEN_TERMINAL_PORT}" || true
echo
echo "==> Testing sudo allowlist"
if [[ "${INSTALL_SUDOERS}" == "1" ]]; then
sudo -u "${TERMINAL_USER}" sudo -n /usr/bin/systemctl status open-terminal.service >/dev/null && \
echo "sudo systemctl status test: OK" || \
echo "sudo systemctl status test: FAILED"
if command -v docker >/dev/null 2>&1; then
sudo -u "${TERMINAL_USER}" sudo -n /usr/bin/docker ps >/dev/null && \
echo "sudo docker ps test: OK" || \
echo "sudo docker ps test: FAILED"
fi
fi
echo
echo "============================================================"
echo "Open Terminal deployment complete."
echo
echo "Service user: ${TERMINAL_USER}"
echo "Listen URL: http://<TARGET_MACHINE_IP>:${OPEN_TERMINAL_PORT}"
echo "Bind host: ${OPEN_TERMINAL_HOST}"
echo "Port: ${OPEN_TERMINAL_PORT}"
echo "API key: ${OPEN_TERMINAL_API_KEY}"
echo
echo "Add this to Open WebUI using:"
echo " URL: http://<TARGET_MACHINE_IP>:${OPEN_TERMINAL_PORT}"
echo " API key: ${OPEN_TERMINAL_API_KEY}"
echo
echo "Useful commands:"
echo " sudo systemctl status open-terminal.service"
echo " sudo journalctl -u open-terminal.service -f"
echo " sudo -l -U ${TERMINAL_USER}"
echo "============================================================"
SCRIPT
What the script creates
After the script completes, the target host has a few important files and services.
Open Terminal executable
For the default user, Open Terminal is installed here:
1
/home/username/.local/bin/open-terminal
The script installs it with uv:
1
uv tool install --upgrade open-terminal
API key environment file
The generated API key is stored here:
1
/etc/open-terminal.env
The file contains a value like this:
1
OPEN_TERMINAL_API_KEY=p_generatedapikeyhere
The file permissions are locked down to:
1
0600
systemd service
The Open Terminal service is created here:
1
/etc/systemd/system/open-terminal.service
The service is enabled at boot with:
1
systemctl enable open-terminal.service
It is started or restarted with:
1
systemctl restart open-terminal.service
sudoers allowlist
The sudoers allowlist is written here:
1
/etc/sudoers.d/open-terminal-broad
It is validated with:
1
visudo -cf /etc/sudoers.d/open-terminal-broad
The permissions are set to:
1
0440
Expected deployment output
A successful deployment should end with output similar to this:
1
2
3
4
5
6
7
8
9
10
11
Open Terminal deployment complete.
Service user: username
Listen URL: http://<TARGET_MACHINE_IP>:8054
Bind host: 0.0.0.0
Port: 8054
API key: p_generatedapikeyhere
Add this to Open WebUI using:
URL: http://<TARGET_MACHINE_IP>:8054
API key: p_generatedapikeyhere
The service status should show:
1
Active: active (running)
The listening socket check should show something similar to:
1
LISTEN 0 2048 0.0.0.0:8054 0.0.0.0:*
The sudo allowlist checks should show:
1
2
sudo systemctl status test: OK
sudo docker ps test: OK
The Docker test only appears if Docker is installed.
Verifying the installation
After the script finishes, check the service:
1
sudo systemctl status open-terminal.service
A healthy service should show:
1
Active: active (running)
Then check that the port is listening:
1
ss -ltnp | grep 8054
Expected output should include something like:
1
LISTEN 0 2048 0.0.0.0:8054 0.0.0.0:*
Verifying sudo access
Check the sudo rules for the service user:
1
sudo -l -U username
Test a systemd command without an interactive password prompt:
1
sudo -u username sudo -n /usr/bin/systemctl status open-terminal.service
If Docker is installed, test Docker access:
1
sudo -u username sudo -n /usr/bin/docker ps
If the commands work without asking for a password, the sudo allowlist is functioning.
Adding it to Open WebUI
Once Open Terminal is running, add it to Open WebUI using the URL and API key printed by the script.
Example URL by hostname:
1
http://network-services:8054
Example URL by IP:
1
http://192.168.0.105:8054
The script prints the generated key at the end:
1
API key: p_generatedapikeyhere
Use that key in Open WebUI’s Open Terminal configuration.
Changing the default user, port, or API key
The script supports environment variable overrides.
To run Open Terminal as a different user:
1
sudo TERMINAL_USER=myuser bash -s < deploy-open-terminal.sh
To use a different port:
1
sudo OPEN_TERMINAL_PORT=8055 bash -s < deploy-open-terminal.sh
To provide your own API key:
1
sudo OPEN_TERMINAL_API_KEY='p_mycustomapikey' bash -s < deploy-open-terminal.sh
To skip installing the sudoers allowlist:
1
sudo INSTALL_SUDOERS=0 bash -s < deploy-open-terminal.sh
To skip adding the user to the Docker group:
1
sudo ADD_DOCKER_GROUP=0 bash -s < deploy-open-terminal.sh
If using the copy/paste heredoc version, place the environment variable before the command, like this:
1
2
3
sudo OPEN_TERMINAL_PORT=8055 bash -s <<'SCRIPT'
# paste the deployment script here
SCRIPT
Useful commands
Restart Open Terminal:
1
sudo systemctl restart open-terminal.service
Stop Open Terminal:
1
sudo systemctl stop open-terminal.service
Start Open Terminal:
1
sudo systemctl start open-terminal.service
View service logs:
1
sudo journalctl -u open-terminal.service -f
Check service status:
1
sudo systemctl status open-terminal.service
Check listening port:
1
ss -ltnp | grep 8054
Check sudo permissions:
1
sudo -l -U csadmin
View the API key file:
1
sudo cat /etc/open-terminal.env
Validate the sudoers file:
1
sudo visudo -cf /etc/sudoers.d/open-terminal-broad
Troubleshooting
APT update fails
One issue we encountered was a broken third-party APT repository.
The error looked like this:
1
E: The repository 'http://www.deb-multimedia.org trixie InRelease' is not signed.
The script is designed to continue past this because third-party repo failures should not necessarily block Open Terminal deployment.
That said, the repository should still be fixed later by either installing the proper keyring, updating the repo configuration, or removing the repository if it is no longer needed.
Open Terminal is not running
Check the service status:
1
sudo systemctl status open-terminal.service
Then check logs:
1
sudo journalctl -u open-terminal.service -n 100 --no-pager
If needed, restart the service:
1
sudo systemctl restart open-terminal.service
Port 8054 is already in use
Check what is using the port:
1
ss -ltnp | grep 8054
If something else is already using it, rerun the deployment with a different port:
1
2
3
sudo OPEN_TERMINAL_PORT=8055 bash -s <<'SCRIPT'
# paste the deployment script here
SCRIPT
Then configure Open WebUI to use the new port.
Sudo still asks for a password
Validate the sudoers file:
1
sudo visudo -cf /etc/sudoers.d/open-terminal-broad
Check the user’s sudo rules:
1
sudo -l -U username
The allowed commands should show NOPASSWD.
Also check the sudoers file permissions:
1
ls -l /etc/sudoers.d/open-terminal-broad
The expected mode is:
1
0440
Docker commands fail
If Docker commands fail, confirm Docker is installed:
1
docker --version
Confirm the Docker service is running:
1
sudo systemctl status docker
Confirm the service user is in the Docker group:
1
id username
If the user was just added to the Docker group, the user may need to log out and back in for group membership to fully apply in interactive shells. The sudo allowlist should still allow sudo docker commands if configured correctly.
Open WebUI cannot connect
First verify that Open Terminal is listening:
1
ss -ltnp | grep 8054
Then test from the Open WebUI host:
1
curl http://network-services:8054
If the hostname fails, try the IP address:
1
curl http://192.168.0.105:8054
If the IP works but the hostname does not, the problem is DNS or local name resolution.
API key does not work
Confirm the API key stored on the host:
1
sudo cat /etc/open-terminal.env
Restart the service after changing the key:
1
sudo systemctl restart open-terminal.service
Then update the key in Open WebUI.
Security considerations
This setup is convenient, but it should be treated carefully.
The Open Terminal endpoint can run commands on the host. The API key should be protected like a password. The service should only be reachable from trusted systems, such as the Open WebUI host or a private management network.
The sudoers policy is not full root access, but it is still broad. Review the allowed command aliases before deploying this widely.
Good follow-up improvements would be:
- restrict the Open Terminal listener with firewall rules,
- limit access to a management VLAN or VPN,
- rotate API keys periodically,
- reduce the sudoers allowlist per machine,
- avoid exposing port
8054publicly, - and document which Open WebUI users are allowed to use terminal tools.
One important warning from the service output is that Open Terminal may allow all CORS origins by default:
1
CORS is set to '*' (allow all origins)
For a private LAN or VPN-only deployment, this may be acceptable. For anything more exposed, restrict access with firewall rules or configure allowed origins if supported by the Open Terminal version in use.
Final thoughts
The important part of this setup is that Open Terminal runs baremetal on the host. That gives Open WebUI a clean way to interact with real host services without making the Open WebUI container itself privileged.
To replicate this on another machine:
- SSH into the target host.
- Paste and run the deployment script.
- Save the generated API key.
- Add the endpoint to Open WebUI.
- Verify command execution.
- Review and tighten the sudoers allowlist if needed.
This gives Open WebUI enough access to be useful for administration while avoiding the most dangerous option: giving it blanket unrestricted root access.
