Skip to content

Commit ea9b43a

Browse files
Deploy certificate to FortiGate firewall using API
1 parent f981c78 commit ea9b43a

File tree

1 file changed

+164
-0
lines changed

1 file changed

+164
-0
lines changed

deploy/fortigate.sh

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#!/usr/bin/env sh
2+
# Script to deploy a certificate to FortiGate via API and set it as the current web GUI certificate.
3+
#
4+
# FortiGate's native ACME integration does not support wildcard certificates or domain validation,
5+
# and is not supported if you have a custom management web port (eg. DNAT web traffic).
6+
#
7+
# REQUIRED:
8+
# export FGT_HOST="fortigate_hostname-or-ip"
9+
# export FGT_TOKEN="fortigate_api_token"
10+
#
11+
# OPTIONAL:
12+
# export FGT_PORT="10443" # Custom HTTPS port (defaults to 443 if not set)
13+
#
14+
# Run `acme.sh --deploy -d example.com --deploy-hook fortigate --insecure` to use this script.
15+
# `--insecure` is required on first run if not already using a valid SSL certificate on firewall.
16+
17+
# Function to parse response
18+
parse_response() {
19+
response="$1"
20+
func="$2"
21+
status=$(echo "$response" | grep -o '"status":[ ]*"[^"]*"' | sed 's/"status":[ ]*"\([^"]*\)"/\1/')
22+
if [ "$status" != "success" ]; then
23+
_err "[$func] Operation failed. Deploy with --insecure if current certificate is invalid. Try deploying with --debug to troubleshoot."
24+
return 1
25+
else
26+
_debug "[$func] Operation successful."
27+
return 0
28+
fi
29+
}
30+
31+
# Function to deploy base64-encoded certificate to firewall
32+
deployer() {
33+
cert_base64=$(_base64 <"$_cfullchain" | tr -d '\n')
34+
key_base64=$(_base64 <"$_ckey" | tr -d '\n')
35+
payload=$(
36+
cat <<EOF
37+
{
38+
"type": "regular",
39+
"scope": "global",
40+
"certname": "$_cdomain",
41+
"key_file_content": "$key_base64",
42+
"file_content": "$cert_base64"
43+
}
44+
EOF
45+
)
46+
url="https://${FGT_HOST}:${FGT_PORT}/api/v2/monitor/vpn-certificate/local/import"
47+
_debug "Uploading certificate via URL: $url"
48+
_H1="Authorization: Bearer $FGT_TOKEN"
49+
response=$(_post "$payload" "$url" "" "POST" "application/json")
50+
_debug "FortiGate API Response: $response"
51+
parse_response "$response" "Deploying certificate" || return 1
52+
}
53+
54+
# Function to upload CA certificate to firewall (FortiGate doesn't automatically extract CA from fullchain)
55+
upload_ca_cert() {
56+
ca_base64=$(_base64 <"$_cca" | tr -d '\n')
57+
payload=$(
58+
cat <<EOF
59+
{
60+
"import_method": "file",
61+
"scope": "global",
62+
"file_content": "$ca_base64"
63+
}
64+
EOF
65+
)
66+
url="https://${FGT_HOST}:${FGT_PORT}/api/v2/monitor/vpn-certificate/ca/import"
67+
_debug "Uploading CA certificate via URL: $url"
68+
_H1="Authorization: Bearer $FGT_TOKEN"
69+
response=$(_post "$payload" "$url" "" "POST" "application/json")
70+
_debug "FortiGate API CA Response: $response"
71+
# Handle response -328 (CA already exists)
72+
if echo "$response" | grep -q '"error":[ ]*-328'; then
73+
_debug "CA certificate already exists. Skipping CA upload."
74+
return 0
75+
fi
76+
parse_response "$response" "Deploying CA certificate" || return 1
77+
}
78+
79+
# Function to activate the new certificate
80+
set_active_web_cert() {
81+
payload=$(
82+
cat <<EOF
83+
{
84+
"admin-server-cert": "$_cdomain"
85+
}
86+
EOF
87+
)
88+
url="https://${FGT_HOST}:${FGT_PORT}/api/v2/cmdb/system/global"
89+
_debug "Setting GUI certificate..."
90+
_H1="Authorization: Bearer $FGT_TOKEN"
91+
response=$(_post "$payload" "$url" "" "PUT" "application/json")
92+
parse_response "$response" "Assigning active certificate" || return 1
93+
}
94+
95+
# Function to clean up previous certificate (if exists)
96+
cleanup_previous_certificate() {
97+
_getdeployconf FGT_LAST_CERT
98+
99+
if [ -n "$FGT_LAST_CERT" ] && [ "$FGT_LAST_CERT" != "$_cdomain" ]; then
100+
_debug "Found previously deployed certificate: $FGT_LAST_CERT. Deleting it."
101+
102+
url="https://${FGT_HOST}:${FGT_PORT}/api/v2/cmdb/vpn.certificate/local/${FGT_LAST_CERT}"
103+
104+
_H1="Authorization: Bearer $FGT_TOKEN"
105+
response=$(_post "" "$url" "" "DELETE" "application/json")
106+
_debug "Delete certificate API response: $response"
107+
108+
parse_response "$response" "Delete previous certificate" || return 1
109+
else
110+
_debug "No previous certificate found or new cert is the same as the previous one."
111+
fi
112+
}
113+
114+
# Main function
115+
fortigate_deploy() {
116+
# Create new certificate name with date appended (cannot directly overwrite old certificate)
117+
_cdomain="$(echo "$1" | sed 's/*/WILDCARD_/g')_$(date -u +"%Y-%m-%d")"
118+
_ckey="$2"
119+
_cca="$4"
120+
_cfullchain="$5"
121+
122+
if [ ! -f "$_ckey" ] || [ ! -f "$_cfullchain" ]; then
123+
_err "Valid key and/or certificate not found."
124+
return 1
125+
fi
126+
127+
# Save required environment variables if not already stored
128+
for var in FGT_HOST FGT_TOKEN FGT_PORT; do
129+
if [ "$(eval echo \$$var)" ]; then
130+
_debug "Detected ENV variable $var. Saving to file."
131+
_savedeployconf "$var" "$(eval echo \$$var)" 1
132+
else
133+
_debug "Attempting to load variable $var from file."
134+
_getdeployconf "$var"
135+
fi
136+
done
137+
138+
if [ -z "$FGT_HOST" ] || [ -z "$FGT_TOKEN" ]; then
139+
_err "FGT_HOST and FGT_TOKEN must be set."
140+
return 1
141+
fi
142+
143+
FGT_PORT="${FGT_PORT:-443}"
144+
_debug "Using FortiGate port: $FGT_PORT"
145+
146+
# Upload new certificate
147+
deployer || return 1
148+
149+
# Upload base64-encoded CA certificate
150+
if [ -n "$_cca" ] && [ -f "$_cca" ]; then
151+
upload_ca_cert || return 1
152+
else
153+
_debug "No CA certificate provided."
154+
fi
155+
156+
# Activate new certificate
157+
set_active_web_cert || return 1
158+
159+
# Delete previous certificate
160+
cleanup_previous_certificate
161+
162+
# Store new certificate name for cleanup on next renewal
163+
_savedeployconf "FGT_LAST_CERT" "$_cdomain" 1
164+
}

0 commit comments

Comments
 (0)