Add an opt-in insecure-TLS mode for the agent, for self-signed certs

Installing the agent against a Homelab Manager instance with a
self-signed cert failed: curl verifies TLS by default on the install
download, the report-tasks.sh fetch inside install.sh, and every
periodic check-in — not just the outer one-liner, so passing -k to only
that first curl wasn't enough. Mirrors the existing Proxmox/Synology
"insecure" toggle pattern already in this app.

- install.sh and report-tasks.sh accept API_INSECURE=true, adding -k to
  their own curl calls; install.sh persists it into the agent's env
  file so the periodic systemd timer picks it up too.
- The Servers & Tasks page has a new checkbox next to the generated
  install/uninstall commands that adds -k and API_INSECURE=true for
  you, so the outer one-liner (which install.sh's own logic can't
  touch) also skips verification.

Off by default — only for a trusted LAN.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
bobbanandClaude Sonnet 5 committed 2026-09-15 18:26:19 +02:00
1 parent f5d3c25c89
commit 130212baec
3 files changed
+55 -10

No files matched your search

+20 -1
View File
@@ -6,16 +6,30 @@
# curl -fsSL https://homelab.example.lan/agent/linux/install.sh | \ # curl -fsSL https://homelab.example.lan/agent/linux/install.sh | \
# sudo API_URL=https://homelab.example.lan API_TOKEN=hlm_xxx bash # sudo API_URL=https://homelab.example.lan API_TOKEN=hlm_xxx bash
# #
# If Homelab Manager is served with a self-signed certificate, also pass
# API_INSECURE=true (skips TLS verification for every request this agent
# makes — only do this on a trusted LAN) AND add -k to the outer curl
# above, since that first fetch of this very script also hits the
# self-signed endpoint before any of this script's logic can run:
# curl -fsSL -k https://homelab.example.lan/agent/linux/install.sh | \
# sudo API_URL=https://homelab.example.lan API_TOKEN=hlm_xxx API_INSECURE=true bash
#
set -euo pipefail set -euo pipefail
: "${API_URL:?Set API_URL to your Homelab Manager URL, e.g. https://homelab.example.lan}" : "${API_URL:?Set API_URL to your Homelab Manager URL, e.g. https://homelab.example.lan}"
: "${API_TOKEN:?Set API_TOKEN to the per-server token generated on the Servers & Tasks page}" : "${API_TOKEN:?Set API_TOKEN to the per-server token generated on the Servers & Tasks page}"
API_INSECURE="${API_INSECURE:-false}"
INSTALL_DIR="/usr/local/bin" INSTALL_DIR="/usr/local/bin"
CONFIG_DIR="/etc" CONFIG_DIR="/etc"
SYSTEMD_DIR="/etc/systemd/system" SYSTEMD_DIR="/etc/systemd/system"
INTERVAL_MINUTES="${INTERVAL_MINUTES:-15}" INTERVAL_MINUTES="${INTERVAL_MINUTES:-15}"
CURL_INSECURE_FLAG=()
case "${API_INSECURE,,}" in
1|true|yes) CURL_INSECURE_FLAG=(-k) ;;
esac
if [[ "$EUID" -ne 0 ]]; then if [[ "$EUID" -ne 0 ]]; then
echo "This installer must be run as root (it installs a systemd timer)." >&2 echo "This installer must be run as root (it installs a systemd timer)." >&2
echo "If you're piping from curl, put sudo right after the pipe so it elevates bash, not curl:" >&2 echo "If you're piping from curl, put sudo right after the pipe so it elevates bash, not curl:" >&2
@@ -55,13 +69,14 @@ fi
echo "Installing Homelab Manager agent from $API_URL ..." echo "Installing Homelab Manager agent from $API_URL ..."
curl -fsSL "$API_URL/agent/linux/report-tasks.sh" -o "$INSTALL_DIR/homelab-manager-agent.sh" curl -fsSL "${CURL_INSECURE_FLAG[@]}" "$API_URL/agent/linux/report-tasks.sh" -o "$INSTALL_DIR/homelab-manager-agent.sh"
chmod 755 "$INSTALL_DIR/homelab-manager-agent.sh" chmod 755 "$INSTALL_DIR/homelab-manager-agent.sh"
umask 077 umask 077
cat > "$CONFIG_DIR/homelab-manager-agent.env" <<EOF cat > "$CONFIG_DIR/homelab-manager-agent.env" <<EOF
API_URL=$API_URL API_URL=$API_URL
API_TOKEN=$API_TOKEN API_TOKEN=$API_TOKEN
API_INSECURE=$API_INSECURE
EOF EOF
chmod 600 "$CONFIG_DIR/homelab-manager-agent.env" chmod 600 "$CONFIG_DIR/homelab-manager-agent.env"
@@ -95,4 +110,8 @@ echo "Installed. Running an initial report now..."
"$INSTALL_DIR/homelab-manager-agent.sh" "$INSTALL_DIR/homelab-manager-agent.sh"
echo "Done. The agent reports every ${INTERVAL_MINUTES} minute(s) via the 'homelab-manager-agent.timer' systemd timer." echo "Done. The agent reports every ${INTERVAL_MINUTES} minute(s) via the 'homelab-manager-agent.timer' systemd timer."
if [[ ${#CURL_INSECURE_FLAG[@]} -gt 0 ]]; then
echo "To remove it later: curl -fsSL -k $API_URL/agent/linux/uninstall.sh | sudo bash"
else
echo "To remove it later: curl -fsSL $API_URL/agent/linux/uninstall.sh | sudo bash" echo "To remove it later: curl -fsSL $API_URL/agent/linux/uninstall.sh | sudo bash"
fi
+10 -1
View File
@@ -5,6 +5,9 @@
# #
# API_URL=https://homelab.example.lan API_TOKEN=hlm_xxx ./report-tasks.sh --dry-run # API_URL=https://homelab.example.lan API_TOKEN=hlm_xxx ./report-tasks.sh --dry-run
# #
# Set API_INSECURE=true (also written to the env file by install.sh when
# passed there) if Homelab Manager uses a self-signed certificate.
#
set -euo pipefail set -euo pipefail
ENV_FILE="${ENV_FILE:-/etc/homelab-manager-agent.env}" ENV_FILE="${ENV_FILE:-/etc/homelab-manager-agent.env}"
@@ -15,6 +18,7 @@ fi
API_URL="${API_URL:-}" API_URL="${API_URL:-}"
API_TOKEN="${API_TOKEN:-}" API_TOKEN="${API_TOKEN:-}"
API_INSECURE="${API_INSECURE:-false}"
DRY_RUN=0 DRY_RUN=0
[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=1 [[ "${1:-}" == "--dry-run" ]] && DRY_RUN=1
@@ -23,6 +27,11 @@ if [[ -z "$API_URL" || -z "$API_TOKEN" ]]; then
exit 1 exit 1
fi fi
CURL_INSECURE_FLAG=()
case "${API_INSECURE,,}" in
1|true|yes) CURL_INSECURE_FLAG=(-k) ;;
esac
suggest_package_install() { suggest_package_install() {
local pkg="$1" local pkg="$1"
if command -v apt-get >/dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then
@@ -240,7 +249,7 @@ if [[ "$DRY_RUN" == "1" ]]; then
exit 0 exit 0
fi fi
response=$(curl -sS -o /tmp/hlm-agent-response.json -w "%{http_code}" \ response=$(curl -sS "${CURL_INSECURE_FLAG[@]}" -o /tmp/hlm-agent-response.json -w "%{http_code}" \
-X POST "$API_URL/api/agent/report" \ -X POST "$API_URL/api/agent/report" \
-H "Authorization: Bearer $API_TOKEN" \ -H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
+25 -8
View File
@@ -22,12 +22,17 @@ const SCHEDULE_TYPE_OPTIONS: { value: ScheduleType; label: string }[] = [
{ value: "manual", label: "Other / manual" }, { value: "manual", label: "Other / manual" },
]; ];
function installCommand(token: string): string { function installCommand(token: string, insecure: boolean): string {
return `curl -fsSL ${window.location.origin}/agent/linux/install.sh | sudo API_URL=${window.location.origin} API_TOKEN=${token} bash`; const curlFlags = insecure ? "-fsSL -k" : "-fsSL";
const envVars = insecure
? `API_URL=${window.location.origin} API_TOKEN=${token} API_INSECURE=true`
: `API_URL=${window.location.origin} API_TOKEN=${token}`;
return `curl ${curlFlags} ${window.location.origin}/agent/linux/install.sh | sudo ${envVars} bash`;
} }
function uninstallCommand(): string { function uninstallCommand(insecure: boolean): string {
return `curl -fsSL ${window.location.origin}/agent/linux/uninstall.sh | sudo bash`; const curlFlags = insecure ? "-fsSL -k" : "-fsSL";
return `curl ${curlFlags} ${window.location.origin}/agent/linux/uninstall.sh | sudo bash`;
} }
const emptyTaskForm = { const emptyTaskForm = {
@@ -64,6 +69,7 @@ export default function ServersTasks({ user }: { user: CurrentUser }) {
const [serverDescription, setServerDescription] = useState(""); const [serverDescription, setServerDescription] = useState("");
const [newToken, setNewToken] = useState<{ server: ServerRecord; token: string } | null>(null); const [newToken, setNewToken] = useState<{ server: ServerRecord; token: string } | null>(null);
const [uninstallFor, setUninstallFor] = useState<ServerRecord | null>(null); const [uninstallFor, setUninstallFor] = useState<ServerRecord | null>(null);
const [insecureAgent, setInsecureAgent] = useState(false);
const loadServers = useCallback(() => { const loadServers = useCallback(() => {
return api.servers return api.servers
@@ -266,12 +272,23 @@ export default function ServersTasks({ user }: { user: CurrentUser }) {
<code className="form-control">{newToken.token}</code> <code className="form-control">{newToken.token}</code>
<CopyButton text={newToken.token} /> <CopyButton text={newToken.token} />
</div> </div>
<label className="form-check mb-2">
<input
type="checkbox"
className="form-check-input"
checked={insecureAgent}
onChange={(e) => setInsecureAgent(e.target.checked)}
/>
<span className="form-check-label">
This Homelab Manager instance uses a self-signed certificate (skip TLS verification on the agent)
</span>
</label>
<p className="text-secondary"> <p className="text-secondary">
Install the agent on the server (run as root — put sudo right after the pipe, not before curl): Install the agent on the server (run as root — put sudo right after the pipe, not before curl):
</p> </p>
<div className="input-group"> <div className="input-group">
<pre className="form-control text-wrap mb-0">{installCommand(newToken.token)}</pre> <pre className="form-control text-wrap mb-0">{installCommand(newToken.token, insecureAgent)}</pre>
<CopyButton text={installCommand(newToken.token)} /> <CopyButton text={installCommand(newToken.token, insecureAgent)} />
</div> </div>
</div> </div>
<div className="card-footer"> <div className="card-footer">
@@ -290,8 +307,8 @@ export default function ServersTasks({ user }: { user: CurrentUser }) {
<div className="card-body"> <div className="card-body">
<p className="text-secondary">Run this on the server as root to stop and remove the agent (its entry here is kept):</p> <p className="text-secondary">Run this on the server as root to stop and remove the agent (its entry here is kept):</p>
<div className="input-group"> <div className="input-group">
<pre className="form-control text-wrap mb-0">{uninstallCommand()}</pre> <pre className="form-control text-wrap mb-0">{uninstallCommand(insecureAgent)}</pre>
<CopyButton text={uninstallCommand()} /> <CopyButton text={uninstallCommand(insecureAgent)} />
</div> </div>
</div> </div>
<div className="card-footer"> <div className="card-footer">