Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create fortigate.sh #6218

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/shellcheck.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
name: Shellcheck
on:
workflow_dispatch:
push:
branches:
- '*'
Expand Down
183 changes: 183 additions & 0 deletions deploy/arubacentral.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#!/usr/bin/env sh
# Aruba Central deploy hook for acme.sh

arubacentral_deploy() {
# Generate unique certificate name with a proper random number (5 digits)
_cdomain="$(echo "$1" | sed 's/*/WILDCARD_/g')_$(tr -dc '0-9' </dev/urandom | head -c 5)"
_ckey="$2"
_cca="$4"
_cfullchain="$5"
_cpfx="${_cfullchain%.cer}.pfx"
_passphrase="central123"

_debug "Generated certificate name: $_cdomain"

if [ ! -f "$_ckey" ] || [ ! -f "$_cfullchain" ]; then
_err "Valid key and/or certificate not found."
return 1
fi

for var in ARUBA_HOST ARUBA_CLIENT_ID ARUBA_CLIENT_SECRET ARUBA_REFRESH_TOKEN; do
if [ "$(eval echo \$$var)" ]; then
_debug "Detected ENV variable $var. Saving to file."
_savedeployconf "$var" "$(eval echo \$$var)" 1
else
_debug "Attempting to load variable $var from file."
_getdeployconf "$var"
fi
done

if [ -z "$ARUBA_HOST" ] || [ -z "$ARUBA_CLIENT_ID" ] || [ -z "$ARUBA_CLIENT_SECRET" ] || [ -z "$ARUBA_REFRESH_TOKEN" ]; then
_err "ARUBA_HOST, ARUBA_CLIENT_ID, ARUBA_CLIENT_SECRET, and ARUBA_REFRESH_TOKEN must be set."
return 1
fi

# Refresh Access Token Only If Needed
_refresh_access_token || return 1

# Delete old certificate before deploying a new one
_delete_old_certificate

# Convert certificate to PKCS12 using built-in `_toPkcs()`
_debug "Converting certificate to PKCS12 format using _toPkcs()..."
_toPkcs "$_cpfx" "$_ckey" "$_cfullchain" "$_cca" "$_passphrase" "$_cdomain"
if [ $? -ne 0 ]; then
_err "Failed to convert certificate to PKCS12."
return 1
fi

_debug "Encoding PKCS12 certificate in Base64..."
_pfx_base64=$(_base64 <"$_cpfx" | tr -d '\n')

_debug "Encoding passphrase in Base64..."
_passphrase_base64=$(printf "%s" "$_passphrase" | _base64 | tr -d '\n')

_upload_certificate || return 1
}

# Function to upload the certificate and retry if token is invalid
_upload_certificate() {
_debug "Preparing JSON payload..."
payload=$(
cat <<EOF
{
"cert_name": "$_cdomain",
"cert_type": "SERVER_CERT",
"cert_format": "PKCS12",
"passphrase": "$_passphrase_base64",
"cert_data": "$_pfx_base64"
}
EOF
)

url="${ARUBA_HOST}/configuration/v1/certificates"
_debug "Uploading certificate to Aruba Central: $url"

_H1="Authorization: Bearer $ARUBA_ACCESS_TOKEN"

response=$(_post "$payload" "$url" "" "POST" "application/json")
_debug "Aruba Central API Response: $response"

# If the token is invalid, refresh it and retry once
if echo "$response" | grep -q '"error":"invalid_token"'; then
_debug "❌ Access token is invalid. Refreshing and retrying..."
if _refresh_access_token; then
_H1="Authorization: Bearer $ARUBA_ACCESS_TOKEN"
response=$(_post "$payload" "$url" "" "POST" "application/json")
_debug "Retry API Response: $response"
else
_err "❌ Token refresh failed. Cannot upload certificate."
return 1
fi
fi

# Check if upload was successful
if echo "$response" | grep -q '"cert_md5_checksum"'; then
_debug "✅ Certificate uploaded successfully!"
_savedeployconf "ARUBA_LAST_CERT" "$_cdomain" 1
return 0
else
_err "❌ Failed to upload certificate. Deploy with --debug to troubleshoot."
return 1
fi
}

# Function to refresh API token only if expired
_refresh_access_token() {
_getdeployconf "ARUBA_ACCESS_TOKEN"
_getdeployconf "ARUBA_REFRESH_TOKEN"

_debug "Checking if the access token is still valid..."
check_url="${ARUBA_HOST}/configuration/v1/certificates?limit=1"
_H1="Authorization: Bearer $ARUBA_ACCESS_TOKEN"
response=$(_post "" "$check_url" "" "GET" "application/json")

if echo "$response" | grep -q '"error":"invalid_token"'; then
_debug "❌ Access token is invalid, refreshing..."
else
_debug "✅ Access token is still valid, skipping refresh."
return 0
fi

# Refresh token if it's invalid
_debug "Refreshing Aruba Central API token..."
refresh_url="${ARUBA_HOST}/oauth2/token"

payload=$(
cat <<EOF
{
"client_id": "$ARUBA_CLIENT_ID",
"client_secret": "$ARUBA_CLIENT_SECRET",
"grant_type": "refresh_token",
"refresh_token": "$ARUBA_REFRESH_TOKEN"
}
EOF
)

response=$(_post "$payload" "$refresh_url" "" "POST" "application/json")
_debug "Token Refresh Response: $response"

new_token=$(echo "$response" | grep -o '"access_token":[ ]*"[^"]*"' | sed 's/"access_token":[ ]*"\([^"]*\)"/\1/')
new_refresh_token=$(echo "$response" | grep -o '"refresh_token":[ ]*"[^"]*"' | sed 's/"refresh_token":[ ]*"\([^"]*\)"/\1/')

if [ -n "$new_token" ]; then
_debug "✅ Token refreshed successfully!"
_savedeployconf "ARUBA_ACCESS_TOKEN" "$new_token" 1
ARUBA_ACCESS_TOKEN="$new_token"

if [ -n "$new_refresh_token" ]; then
_debug "🔄 Updating refresh token..."
_savedeployconf "ARUBA_REFRESH_TOKEN" "$new_refresh_token" 1
ARUBA_REFRESH_TOKEN="$new_refresh_token"
else
_debug "⚠️ Aruba Central did not return a new refresh token! Keeping the old one."
fi
else
_err "❌ Failed to refresh API token. Please manually generate a new one."
return 1
fi
}

# Function to delete the previous certificate
_delete_old_certificate() {
_getdeployconf "ARUBA_LAST_CERT"

if [ -n "$ARUBA_LAST_CERT" ]; then
_debug "Found previous certificate: $ARUBA_LAST_CERT. Deleting it..."
delete_url="${ARUBA_HOST}/configuration/v1/certificates/${ARUBA_LAST_CERT}"
_H1="Authorization: Bearer $ARUBA_ACCESS_TOKEN"

response=$(_post "" "$delete_url" "" "DELETE" "application/json")
_debug "Delete certificate API response: $response"

if echo "$response" | jq -e '.description | test("not present")' >/dev/null 2>&1; then
_debug "✅ Previous certificate not found - skipping."
elif echo "$response" | jq -e '.error_code' >/dev/null 2>&1; then
_err "❌ Failed to delete previous certificate: $(echo "$response" | jq -r '.description')"
else
_debug "✅ Previous certificate deleted successfully."
fi
else
_debug "No previous certificate found. Skipping deletion."
fi
}
166 changes: 166 additions & 0 deletions deploy/fortigate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/usr/bin/env sh
# Script to deploy a certificate to FortiGate via API and set it as the current web GUI certificate.
#
# FortiGate's native ACME integration does not support wildcard certificates,
# and is not supported if you have a custom management web port (eg. DNAT web traffic).
#
# REQUIRED:
# export FGT_HOST="fortigate_hostname-or-ip"
# export FGT_TOKEN="fortigate_api_token"
#
# OPTIONAL:
# export FGT_PORT="10443" # Custom HTTPS port (defaults to 443 if not set)
#
# This script is intended for use as an acme.sh deploy hook.
#
# Run `acme.sh --deploy -d example.com --deploy-hook fortigate --insecure` to use this script.
# `--insecure` is required to allow acme.sh to connect to the FortiGate API over HTTPS without a pre-existing valid certificate.

# Function to parse response
parse_response() {
response="$1"
func="$2"
status=$(echo "$response" | grep -o '"status":[ ]*"[^"]*"' | sed 's/"status":[ ]*"\([^"]*\)"/\1/')
if [ "$status" != "success" ]; then
_err "[$func] Operation failed. Deploy with --insecure if current certificate is invalid. Try deploying with --debug to troubleshoot."
return 1
else
_debug "[$func] Operation successful."
return 0
fi
}

# Function to deploy base64-encoded certificate to firewall
deployer() {
cert_base64=$(_base64 <"$_cfullchain" | tr -d '\n')
key_base64=$(_base64 <"$_ckey" | tr -d '\n')
payload=$(
cat <<EOF
{
"type": "regular",
"scope": "global",
"certname": "$_cdomain",
"key_file_content": "$key_base64",
"file_content": "$cert_base64"
}
EOF
)
url="https://${FGT_HOST}:${FGT_PORT}/api/v2/monitor/vpn-certificate/local/import"
_debug "Uploading certificate via URL: $url"
_H1="Authorization: Bearer $FGT_TOKEN"
response=$(_post "$payload" "$url" "" "POST" "application/json")
_debug "FortiGate API Response: $response"
parse_response "$response" "Deploying certificate" || return 1
}

# Function to upload CA certificate to firewall (FortiGate doesn't automatically extract CA from fullchain)
upload_ca_cert() {
ca_base64=$(_base64 <"$_cca" | tr -d '\n')
payload=$(
cat <<EOF
{
"import_method": "file",
"scope": "global",
"file_content": "$ca_base64"
}
EOF
)
url="https://${FGT_HOST}:${FGT_PORT}/api/v2/monitor/vpn-certificate/ca/import"
_debug "Uploading CA certificate via URL: $url"
_H1="Authorization: Bearer $FGT_TOKEN"
response=$(_post "$payload" "$url" "" "POST" "application/json")
_debug "FortiGate API CA Response: $response"
# Handle response -328 (CA already exists)
if echo "$response" | grep -q '"error":[ ]*-328'; then
_debug "CA certificate already exists. Skipping CA upload."
return 0
fi
parse_response "$response" "Deploying CA certificate" || return 1
}

# Function to activate the new certificate
set_active_web_cert() {
payload=$(
cat <<EOF
{
"admin-server-cert": "$_cdomain"
}
EOF
)
url="https://${FGT_HOST}:${FGT_PORT}/api/v2/cmdb/system/global"
_debug "Setting GUI certificate..."
_H1="Authorization: Bearer $FGT_TOKEN"
response=$(_post "$payload" "$url" "" "PUT" "application/json")
parse_response "$response" "Assigning active certificate" || return 1
}

# Function to clean up previous certificate (if exists)
cleanup_previous_certificate() {
_getdeployconf FGT_LAST_CERT

if [ -n "$FGT_LAST_CERT" ] && [ "$FGT_LAST_CERT" != "$_cdomain" ]; then
_debug "Found previously deployed certificate: $FGT_LAST_CERT. Deleting it."

url="https://${FGT_HOST}:${FGT_PORT}/api/v2/cmdb/vpn.certificate/local/${FGT_LAST_CERT}"

_H1="Authorization: Bearer $FGT_TOKEN"
response=$(_post "" "$url" "" "DELETE" "application/json")
_debug "Delete certificate API response: $response"

parse_response "$response" "Delete previous certificate" || return 1
else
_debug "No previous certificate found or new cert is the same as the previous one."
fi
}

# Main function.
fortigate_deploy() {
# Create new certificate name with date appended (cannot directly overwrite old certificate)
_cdomain="$(echo "$1" | sed 's/*/WILDCARD_/g')_$(date -u +"%Y-%m-%d")"
_ckey="$2"
_cca="$4"
_cfullchain="$5"

if [ ! -f "$_ckey" ] || [ ! -f "$_cfullchain" ]; then
_err "Valid key and/or certificate not found."
return 1
fi

# Save required environment variables if not already stored.
for var in FGT_HOST FGT_TOKEN FGT_PORT; do
if [ "$(eval echo \$$var)" ]; then
_debug "Detected ENV variable $var. Saving to file."
_savedeployconf "$var" "$(eval echo \$$var)" 1
else
_debug "Attempting to load variable $var from file."
_getdeployconf "$var"
fi
done

if [ -z "$FGT_HOST" ] || [ -z "$FGT_TOKEN" ]; then
_err "FGT_HOST and FGT_TOKEN must be set."
return 1
fi

FGT_PORT="${FGT_PORT:-443}"
_debug "Using FortiGate port: $FGT_PORT"

# Upload new certificate
deployer || return 1

# Upload base64-encoded CA certificate
if [ -n "$_cca" ] && [ -f "$_cca" ]; then
upload_ca_cert || return 1
else
_debug "No CA certificate provided."
fi

# Set new certificate as active
set_active_web_cert || return 1

# Cleanup previous certificate
cleanup_previous_certificate

# Store new certificate name for cleanup on next renewal
_savedeployconf "FGT_LAST_CERT" "$_cdomain" 1
}