Skip to content

Problem (and solution) with unifi deploy hook. #6916

Description

@btimby

I wanted to deploy the certificate to my Unifi UCG Fiber (Unifi OS 5.0.16) remotely. I did not want to install the deploy hook locally and tried emulating it via SSH (systemctl restart unifi-core, systemctl restart nginx, unifi-os restart`) My certificate would always revert after about 10-15 minutes.

I inspected the communication from the web console and came up with the following script that cleanly applies the certificate using that method. Then whatever internal magic they do is performed as necessary and the cert sticks.

I am not sure how or if this can be adopted into acme.sh, but perhaps others find this useful (I saw reports about certs reverting like I was experiencing.)

#!/bin/sh
# Deploy cert to UniFi OS via API
# Usage: deploy-unifi.sh <host> <username> <password> <cert_file> <key_file>

UNIFI_HOST="$1"
UNIFI_USER="$2"
UNIFI_PASS="$3"
CERT_FILE="$4"
KEY_FILE="$5"

COOKIE_JAR=$(mktemp)

# Login and get TOKEN cookie
curl -sk -c "$COOKIE_JAR" \
  -H "Content-Type: application/json" \
  -d "{\"username\":\"$UNIFI_USER\",\"password\":\"$UNIFI_PASS\",\"rememberMe\":false}" \
  "https://$UNIFI_HOST/api/auth/login" > /dev/null

# Extract TOKEN from cookie jar
TOKEN=$(grep -m1 'TOKEN' "$COOKIE_JAR" | awk '{print $7}')
if [ -z "$TOKEN" ]; then
  echo "[ERROR] login failed — no TOKEN cookie received"
  rm -f "$COOKIE_JAR"
  exit 1
fi

# Extract CSRF token from JWT payload (base64url decode middle section)
PAYLOAD=$(echo "$TOKEN" | cut -d'.' -f2)
PAD=$(( ${#PAYLOAD} % 4 ))
[ "$PAD" -ne 0 ] && PAYLOAD="${PAYLOAD}$(printf '%0.s=' $(seq 1 $((4 - PAD))))"
CSRF=$(echo "$PAYLOAD" | tr '_-' '/+' | base64 -d 2>/dev/null | grep -o '"csrfToken":"[^"]*"' | cut -d'"' -f4)

if [ -z "$CSRF" ]; then
  echo "[ERROR] could not extract CSRF token from JWT"
  rm -f "$COOKIE_JAR"
  exit 1
fi

# Escape cert and key newlines for JSON
CERT=$(sed ':a;N;$!ba;s/\n/\\n/g' "$CERT_FILE")
KEY=$(sed ':a;N;$!ba;s/\n/\\n/g' "$KEY_FILE")

# Upload certificate
UPLOAD_RESPONSE=$(curl -sk \
  -b "$COOKIE_JAR" \
  -H "Content-Type: application/json" \
  -H "X-Csrf-Token: $CSRF" \
  -d "{\"name\":\"$UNIFI_HOST\",\"cert\":\"$CERT\",\"key\":\"$KEY\"}" \
  "https://$UNIFI_HOST/api/userCertificates")

# Extract cert ID from response
CERT_ID=$(echo "$UPLOAD_RESPONSE" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)

if [ -z "$CERT_ID" ]; then
  # Check if it's a duplicate — cert already exists, look it up by name
  if echo "$UPLOAD_RESPONSE" | grep -q 'USER_CERTIFICATE_DUPLICATE'; then
    echo "[INFO] certificate already exists, looking up by name..."
    LIST_RESPONSE=$(curl -sk \
      -b "$COOKIE_JAR" \
      -H "X-Csrf-Token: $CSRF" \
      "https://$UNIFI_HOST/api/userCertificates")
    CERT_ID=$(echo "$LIST_RESPONSE" | sed 's/},{/}\n{/g' | grep '"name":"'"$UNIFI_HOST"'"' | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
  fi
fi

if [ -z "$CERT_ID" ]; then
  echo "[ERROR] upload failed or no ID returned: $UPLOAD_RESPONSE"
  rm -f "$COOKIE_JAR"
  exit 1
fi

echo "[OK] certificate ready (id: $CERT_ID)"

# Activate the certificate
ACTIVATE_RESPONSE=$(curl -sk -w "\n%{http_code}" \
  -X PUT \
  -b "$COOKIE_JAR" \
  -H "Content-Type: application/json" \
  -H "X-Csrf-Token: $CSRF" \
  -d '{"active":true}' \
  "https://$UNIFI_HOST/api/userCertificates/$CERT_ID/status")

HTTP_CODE=$(echo "$ACTIVATE_RESPONSE" | tail -n1)
BODY=$(echo "$ACTIVATE_RESPONSE" | head -n-1)

rm -f "$COOKIE_JAR"

if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
  echo "[OK] certificate activated on $UNIFI_HOST (HTTP $HTTP_CODE)"
else
  echo "[ERROR] activation failed (HTTP $HTTP_CODE): $BODY"
  exit 1
fi

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions