How we searched for a practical way to communicate with a locally hosted AI without exposing the workstation or sending confidential data through an unsuitable channel
Running an AI model locally solves only part of the privacy problem.
Our local AI stack already consisted of:
- a Qwen 27B model running through
llama.cpp, - Hermes Agent as the orchestration layer,
- Docker-isolated tool execution,
- a Telegram bot for remote access,
- and systemd services for reliable operation.
The model itself ran entirely on the workstation. Prompts were processed locally, and the agent’s tools executed inside a restricted Docker environment.
That was a solid foundation.
However, one concern remained:
How could we communicate with the local agent from another device without placing confidential information into Telegram, exposing the workstation to the internet, or building an unnecessarily complex remote-access system?
What followed was a surprisingly long architectural journey through web interfaces, HTTPS, WireGuard, local-only applications, Threema, and finally Signal.
This article documents that journey, the reasons behind each rejected approach, and how Signal ultimately became the most practical secure channel for our local AI.
The starting point: Telegram worked, but it was not the right channel for everything
Telegram was our first remote interface because it was simple and worked well with Hermes.
The architecture looked like this:
Telegram
│
▼
Hermes Gateway
│
▼
Local Qwen model
│
▼
Docker-isolated tools
This was extremely convenient.
We could:
- send prompts from a smartphone,
- receive service notifications,
- check whether the model was online,
- start agent tasks remotely,
- and use the workstation without opening inbound network ports.
For operational messages, Telegram was perfectly adequate.
Examples included:
/status
or:
Check whether the local model is healthy.
The problem appeared when we considered sending:
- customer information,
- proprietary source code,
- screenshots of internal systems,
- contracts,
- infrastructure diagrams,
- debugging logs,
- or other confidential business data.
The local model might be private, but the communication channel into it was a separate security boundary.
That distinction became central to the project:
Local inference does not automatically imply private transport.
We therefore began looking for an additional channel specifically for confidential work.
Telegram would remain available, but it would no longer be the preferred path for sensitive information.
Option 1: A ChatGPT-style web interface
The most obvious idea was to build or install a browser-based interface under a domain such as:
https://ki.jr-it-services.de
A web interface would provide several advantages:
- a familiar ChatGPT-style user experience,
- long-form text editing,
- easy code copying,
- image pasting,
- document uploads,
- conversation history,
- and potentially multiple users.
Open WebUI appeared to be a suitable frontend, while Hermes could expose the actual agent through an OpenAI-compatible API.
The architecture would have been:
Browser
│
▼
Reverse proxy
│
▼
Open WebUI
│
▼
Hermes API
│
▼
Local model and tools
Technically, this was entirely feasible.
The problem was not functionality. It was exposure.
A remotely accessible web interface would require at least one of the following:
- port forwarding to the workstation,
- a public reverse proxy,
- a VPN,
- a cloud tunnel,
- or an external relay.
Even with HTTPS, the application would become part of the public attack surface.
We would need to maintain:
- TLS certificates,
- authentication,
- login protection,
- rate limiting,
- user management,
- application updates,
- proxy configuration,
- upload limits,
- and potentially intrusion monitoring.
HTTPS protects data in transit, but it does not by itself prevent unauthorized users from attempting to access or exploit the application.
For a public business service, that operational work would be justified. For a personal remote channel to a local agent, it felt disproportionate.
Option 2: WireGuard
The next idea was to keep the web interface private and access it through WireGuard.
The resulting architecture would be strong:
Remote device
│
│ WireGuard tunnel
▼
Private network
│
▼
Local WebUI
│
▼
Hermes
Nothing except the VPN endpoint would be exposed, and the web interface could remain private.
From a security perspective, this was an excellent solution.
From a practical perspective, however, it introduced another infrastructure layer:
- WireGuard profiles,
- device keys,
- routing,
- internal DNS,
- client configuration,
- certificate handling for the private web interface,
- and device revocation procedures.
Using the FRITZ!Box as the WireGuard endpoint would have simplified the server side, but every remote client would still need to connect to the VPN before accessing the AI.
The solution was secure, but it did not match our actual requirement.
We did not need full remote network access.
We needed a simple, secure conversation channel.
WireGuard solved a larger problem than the one we had.
Option 3: Public HTTPS without a VPN
We then considered the pragmatic compromise:
Internet
│
│ HTTPS
▼
Caddy or Nginx
│
▼
Open WebUI
This would have been simpler than WireGuard.
Caddy could automatically manage TLS certificates, and Open WebUI could provide local user authentication.
Only TCP port 443 would need to be forwarded.
However, one requirement remained non-negotiable:
We did not want to expose any incoming port on the production workstation.
The workstation hosted the AI model, development environments, local repositories, credentials, and business data. Even with a carefully configured reverse proxy, turning it into a public web server was not an attractive design choice.
The HTTPS approach was therefore rejected as well.
Option 4: A local-only WebUI
The safest web-based option was a UI bound exclusively to loopback:
http://127.0.0.1:3000
For example:
ports:
- "127.0.0.1:3000:8080"
This would prevent access from:
- the local network,
- the internet,
- other devices,
- and external scanners.
It would be ideal for confidential work while sitting at the workstation.
But it did not solve the original remote-access requirement.
A local UI remained a useful future addition, particularly for handling large files and long code blocks, but it could not replace a mobile communication channel.
Option 5: Threema
Threema was also considered.
It has a strong security reputation and offers a gateway product suitable for application integration.
However, a Hermes integration would have required custom development:
Threema
│
▼
Gateway callback
│
▼
Custom Hermes adapter
│
▼
Local agent
Incoming Threema Gateway messages are normally delivered to an HTTPS callback.
That would bring us back to one of the problems we were trying to avoid:
- a public endpoint,
- an external relay,
- or a tunnel.
We would also need to implement and maintain:
- message decryption,
- sender validation,
- attachment handling,
- response encryption,
- Hermes session mapping,
- delivery error handling,
- and the gateway lifecycle.
Threema was technically possible, but it was significantly more complex than necessary.
For a formal customer-facing business integration, that complexity might be justified.
For a private remote channel to one local AI agent, it was not.
The turning point: Signal
Signal eventually provided the best balance between:
- confidentiality,
- convenience,
- low infrastructure overhead,
- and direct Hermes integration.
Signal conversations are end-to-end encrypted by default. Signal also supports linked devices, where each linked device participates in the encrypted communication rather than receiving plaintext through a conventional bot API. (Signal Unterstützung)
Most importantly for our architecture, Hermes already supports Signal through signal-cli.
Hermes connects to a locally running signal-cli HTTP daemon, receives events through Server-Sent Events, and sends responses using JSON-RPC. (Hermes Agent)
The architecture became:
Signal app on smartphone
│
│ End-to-end encrypted
▼
Signal infrastructure
│
│ End-to-end encrypted
▼
signal-cli linked device
on the workstation
│
│ localhost only
▼
Hermes Gateway
│
▼
Local Qwen model
│
▼
Docker-isolated tools
This design met our most important requirements:
No incoming router port
No public web application
No VPN connection
No second server
No custom messaging adapter
No second phone required
Using the existing Signal account
We initially considered using a separate Signal account for the agent.
That would have provided a cleaner separation, but it would also have required:
- another phone number,
- an additional SIM or eSIM,
- and more account management.
Instead, we linked signal-cli to the existing Signal account as another device.
Signal officially supports linked devices, and the initial linking process can optionally synchronize chat history and recent media. We chose not to transfer the existing message history to the workstation. (Signal Unterstützung)
The agent is accessed through Signal’s Note to Self conversation.
Note to Self messages are regular end-to-end-encrypted Signal messages and are specifically designed for communication between a user’s linked devices. (Signal Unterstützung)
That gave us a simple interaction model:
Smartphone
│
▼
Signal → Note to Self
│
▼
Hermes responds in the same conversation
No additional contact, group, phone, SIM card, or bot account was required.
Linking signal-cli
The workstation was linked using:
signal-cli link -n "HermesAgent"
This produced a temporary Signal device-link URI.
The URI was rendered as a QR code and scanned from:
Signal
→ Settings
→ Linked Devices
→ Link New Device
After linking, the account was visible through:
signal-cli listAccounts
We then tested sending a message to Note to Self:
signal-cli \
--account "$SIGNAL_ACCOUNT" \
send \
--note-to-self \
--message "signal-cli was linked successfully."
The message appeared in Signal immediately.
That confirmed:
- the linked-device registration,
- outgoing message delivery,
- and the correct account configuration.
Running signal-cli as a local daemon
Hermes needs signal-cli to receive messages continuously.
The signal-cli documentation notes that messages should be received regularly and that daemon mode performs continuous reception. (GitHub)
We launched it on loopback only:
signal-cli \
--account "$SIGNAL_ACCOUNT" \
daemon \
--http 127.0.0.1:8090
Binding to:
127.0.0.1:8090
was critical.
The API was reachable by Hermes on the workstation, but not from:
- the LAN,
- another local device,
- the router,
- or the public internet.
The health check was simple:
curl \
--silent \
--output /dev/null \
--write-out '%{http_code}\n' \
http://127.0.0.1:8090/api/v1/check
Expected result:
200
The endpoint returned an empty response body, which initially looked suspicious because piping it through jq displayed nothing.
The service was nevertheless healthy. The relevant signal was the HTTP status code, not a JSON document.
Turning it into a systemd service
The manually launched daemon was then converted into a persistent systemd user service:
[Unit]
Description=signal-cli daemon for Hermes
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
Environment=HOME=%h
EnvironmentFile=%h/.config/signal-cli-hermes.env
ExecStart=/usr/local/bin/signal-cli \
--account ${SIGNAL_ACCOUNT} \
daemon \
--http 127.0.0.1:8090
Restart=on-failure
RestartSec=10
UMask=0077
NoNewPrivileges=true
[Install]
WantedBy=default.target
The account number was stored separately:
printf 'SIGNAL_ACCOUNT=%s\n' "$SIGNAL_ACCOUNT" \
> "$HOME/.config/signal-cli-hermes.env"
chmod 600 "$HOME/.config/signal-cli-hermes.env"
The service was enabled with:
systemctl --user daemon-reload
systemctl --user enable --now signal-cli.service
Verification:
systemctl --user status signal-cli.service
and:
ss -ltn | grep ':8090'
The expected listener was:
127.0.0.1:8090
Not:
0.0.0.0:8090
Connecting Hermes
Hermes was configured with:
SIGNAL_HTTP_URL=http://127.0.0.1:8090
SIGNAL_ACCOUNT=+49...
SIGNAL_ALLOWED_USERS=+49...
SIGNAL_ALLOW_ALL_USERS=false
The allowlist contains only the owner’s number.
Signal groups were left disabled.
This ensured that Hermes would not react to arbitrary Signal users or group messages.
The Hermes gateway was also configured to start after the Signal daemon:
[Unit]
Wants=signal-cli.service
After=signal-cli.service
We deliberately used Wants, not Requires.
This means:
- Hermes starts Signal when possible,
- but a Signal failure does not prevent Telegram from continuing to operate.
Hermes supports multiple messaging platforms through one gateway process, with platform-specific routing and sessions. (Hermes Agent)
The resulting setup was:
Telegram ─┐
├── Hermes Gateway ── Local AI
Signal ───┘
The Home Channel surprise
The first Signal prompt did not immediately reach the model.
Instead, Hermes replied:
No home channel is set for Signal.
The fix was straightforward:
/sethome
This registered the Note to Self conversation as the Signal home channel.
After that, the following prompt worked:
Reply exactly with:
Hermes is working locally through Signal.
The response travelled through the complete chain:
Signal
→ signal-cli
→ Hermes Gateway
→ local Qwen model
→ Hermes Gateway
→ signal-cli
→ Signal
No inbound port had been opened at any point.
Why Signal was the final choice
Signal did not win because it was theoretically perfect.
It won because it provided the best practical security-to-complexity ratio for our situation.
Compared with Telegram
Signal gave us a better channel for confidential content because Signal conversations are end-to-end encrypted by default. (Signal Unterstützung)
Telegram remained useful for:
- service notifications,
- status checks,
- uncritical prompts,
- and operational alerts.
Signal became the preferred channel for:
- confidential text,
- internal project information,
- customer details,
- source-code discussions,
- screenshots,
- and documents.
Compared with a public WebUI
Signal required:
- no public hostname,
- no reverse proxy,
- no TLS endpoint,
- no public login form,
- no port forwarding,
- and no exposed web application.
Compared with WireGuard
Signal required no VPN connection before each conversation.
The smartphone could communicate with the local agent using the same interaction model as any other messenger.
Compared with Threema
Signal already had direct Hermes support.
No custom adapter, callback endpoint, gateway account, or relay server was needed.
Compared with a local WebUI
Signal remained usable from remote clients.
It solved the actual mobility problem without turning the workstation into a server exposed to external networks.
Security does not end with Signal
Signal protects the communication path, but it does not make the complete system automatically safe.
Once a message reaches the workstation, it is decrypted and processed locally.
That introduces several additional trust boundaries.
The workstation
Anyone who compromises the workstation could potentially access:
- Signal linked-device data,
- Hermes session history,
- files sent to the agent,
- model prompts,
- and tool results.
The Signal data directory therefore received restrictive permissions:
chmod -R go-rwx "$HOME/.local/share/signal-cli"
The configuration files were protected as well:
chmod 600 "$HOME/.config/signal-cli-hermes.env"
chmod 600 "$HOME/.hermes/.env"
Signal states that message content is stored locally on user devices, while the service itself does not have access to end-to-end-encrypted message content. (Signal Unterstützung)
This makes disk encryption and endpoint security just as important as transport encryption.
Hermes session storage
Hermes may retain conversation history, tool calls, and tool outputs locally.
Sensitive prompts are therefore still sensitive after delivery.
Backups, logs, and state databases must be protected accordingly.
External tools
The local AI model may be private, while some agent tools are not.
Examples include:
- external web search,
- browser automation,
- cloud image generation,
- hosted speech recognition,
- third-party APIs,
- or remote vision services.
A confidential message could still leave the workstation if the agent passed it to one of those tools.
For sensitive Signal sessions, external tools should therefore be disabled or enabled only deliberately.
The desired trust boundary is:
Signal transport
│
▼
Local Hermes
│
├── Local model
├── Local files
├── Local Docker tools
└── No external service unless explicitly approved
Maintenance became part of the design
signal-cli is an unofficial command-line interface for Signal and provides daemon and JSON-RPC functionality. Its project documentation recommends keeping it current because protocol and server-side changes can make older versions stop working reliably. (GitHub)
We therefore extended the existing maintenance framework.
The new checks include:
Every ten minutes:
Verify signal-cli.service
Check HTTP status on 127.0.0.1:8090
Restart signal-cli if necessary
Reconnect Hermes after recovery
Regularly:
Compare installed and available signal-cli versions
Notify when an update is available
Before updating:
Back up the current binary
Back up Signal linked-device data
After updating:
Start signal-cli
Verify HTTP status 200
Restart Hermes Gateway
Send /status through Signal
On failure:
Restore the previous binary
Restore data if required
Restart the old version
The installed binary layout was:
/usr/local/bin/signal-cli
→ /opt/signal-cli
This made controlled updates and rollbacks straightforward.
Automatic update detection was enabled, while the actual binary replacement remained a supervised action.
That balance was intentional.
A messaging channel used for confidential work should not silently update itself and fail without a recovery path.
The completed architecture
The final communication architecture now looks like this:
┌──────────────────────┐
│ Telegram │
│ Status and operations│
└──────────┬───────────┘
│
▼
Hermes Gateway
│
▼
Local Qwen model
│
▼
Docker-isolated tools
▲
│
┌──────────┴───────────┐
│ Signal │
│ Confidential channel │
└──────────────────────┘
More precisely, the Signal path is:
Signal smartphone app
│
│ End-to-end encryption
▼
Signal linked-device transport
│
▼
signal-cli
127.0.0.1:8090
│
▼
Hermes Gateway
│
▼
llama.cpp
127.0.0.1:8080
│
▼
Local Qwen 27B model
The workstation exposes no inbound internet-facing service for this integration.
What we learned
The main lesson was that “secure remote access” does not always require a web server, VPN, or custom application.
The right solution depends on the actual interaction pattern.
We did not need remote desktop access.
We did not need full access to the home network.
We did not need a public SaaS-style application.
We needed a secure asynchronous message channel to a local agent.
Signal matched that problem unusually well.
Another important lesson was that security decisions are rarely based on one feature.
Signal was chosen not simply because it offers end-to-end encryption, but because the complete solution also provided:
Encrypted transport
No incoming ports
No public application
Existing mobile clients
Hermes integration
Linked-device support
Note to Self workflow
Attachment support
Simple revocation
Manageable maintenance
No other option offered the same combination with comparable effort.
Final Summary
Our journey began with a working Telegram integration and a locally hosted AI model.
Telegram was convenient, reliable, and excellent for operational communication, but it did not feel like the right place for confidential customer information, proprietary source code, internal screenshots, or business documents.
We investigated a full ChatGPT-style web interface, but that would have required either public port exposure, a reverse proxy, or an external tunnel.
WireGuard was secure but introduced more network infrastructure than the problem justified.
A local-only interface was private but not remotely accessible.
HTTPS without a VPN was simpler, but it still required turning the workstation into an internet-facing web server.
Threema was attractive from a privacy perspective but would have required a custom Hermes adapter and an externally reachable callback or relay.
Signal ultimately provided the most practical answer.
By linking signal-cli as an additional device to the existing Signal account, we gained an end-to-end-encrypted communication path to Hermes without:
- opening a router port,
- exposing the workstation,
- operating a VPN,
- buying another SIM card,
- running another phone,
- or developing a new gateway integration.
The existing Telegram channel remains in place for health notifications, updates, and uncritical requests.
Signal now serves as the more confidential remote channel.
The final result is not a claim of absolute security. The workstation, Hermes history, local files, agent tools, and backups still require careful protection.
But it is a major improvement in the most important area:
Confidential prompts can now travel from a remote device to the locally running AI through an end-to-end-encrypted channel, while the workstation remains closed to inbound internet traffic.
That makes Signal a particularly effective bridge between the convenience of a mobile assistant and the privacy of a locally operated AI system.
Views: 0
