Nginx Scripts and Tools
We use the following scripts for various tasks related to our web server. Most of them are run fully automated and regularly as cron jobs on the server, while others are called on demand.
Static Compression
Nginx can be set to serve pre-compressed versions of static files like HMTL- files, CSS stylesheets, script-files. etc. instead of compressing them on-the- fly while sending them to clients.
The following script will try to find static files in the website and pre- compress them for Nginx to to send out.
1#!/bin/bash
2#
3# Pre-compress static web resources
4#
5# To be started by cron every few hours
6#
7# For monitoring use the following command:
8#
9# journalctl -t ngx_pre_compress -p <level> --no-tail -f
10#
11# while <level> may be any of debug, info, notice or warning
12
13# Exit on errors
14set -e
15# Exit on undefined variable
16set -u
17
18NGINX_USER='www-data'
19NGINX_GROUP='www-data'
20NGINX_CMD='/usr/sbin/nginx'
21GZIP_CMD='/bin/gzip'
22BROTLI_CMD='/usr/bin/brotli'
23# See /etc/nginx/mime.types
24FILETYPES='en html htm shtml css xml js atom rss mml txt wml json doc ps rtf xls ppt run pl docx xlsx pptx'
25
26# Log to syslog
27log()
28{
29 logger -t ngx_pre_compress --priority user.${1} ${2}
30}
31
32# We need to be root
33if [[ ${UID} -gt 0 ]]; then
34 log 'error' "Sorry, need to be root"
35 echo "Sorry, need to be root"
36 exit 1
37fi
38
39# Fix sorting
40LC_ALL=C
41
42# Start
43log 'notice' "Starting pre-compression of files for Nginx."
44
45# Get document root and alias directories from current Nginx configuration
46webdirs=$( ${NGINX_CMD} -qT | \
47 grep --only-matching --extended-regexp \
48 "^\s*(root|alias)\s+((/*.)+)" | \
49 grep --only-matching --extended-regexp \
50 "(/[a-zA-Z0-9/_\.]+)" | \
51 sort --unique
52 )
53total_dirs=0
54total_files=0
55total_br_new=0
56total_gz_new=0
57total_br_updates=0
58total_gz_updates=0
59
60dir_files=0
61dir_br_new=0
62dir_br_updates=0
63dir_gz_new=0
64dir_gz_updates=0
65
66#
67# Create compressed versions
68
69# Walk directories
70for dir in ${webdirs};
71do
72
73 # Is this a directory?
74 if [[ -d ${dir} ]]; then
75
76 log 'info' "Looking for files to pre-compress in ${dir} ..."
77 total_dirs=$((total_dirs+1))
78 dir_files=0
79 dir_br_new=0
80 dir_br_updates=0
81 dir_gz_new=0
82 dir_gz_updates=0
83
84 #echo -n "Searching for "
85
86 # Walk the list of filetypes
87 for filetype in ${FILETYPES};
88 do
89 #echo -n "${filetype} "
90
91 # Find files
92 while IFS= read -r -d '' file;
93 do
94
95 dir_files=$((dir_files+1))
96
97 # Does it have a Brotli companion?
98 if [[ -f "${file}.br" ]]; then
99
100 # Is it outdated?
101 if [[ "${file}" -nt "${file}.br" ]]; then
102
103 # Refresh the Brotli companion
104 dir_br_updates=$((dir_br_updates+1))
105 ${BROTLI_CMD} --force --input "${file}" --output "${file}.br"
106
107 # Set file time, owner, access rights from original
108 chown --reference="${file}" "${file}.br"
109 chmod --reference="${file}" "${file}.br"
110 touch --reference="${file}" "${file}.br"
111 fi
112 else
113
114 # Create new Brotli companion
115 dir_br_new=$((dir_br_new+1))
116 ${BROTLI_CMD} --input "${file}" --output "${file}.br"
117
118 # Set file time, owner, access rights from original
119 chown --reference="${file}" "${file}.br"
120 chmod --reference="${file}" "${file}.br"
121 touch --reference="${file}" "${file}.br"
122 fi
123
124 # Does it have a Gzip companion?
125 if [[ -f "${file}.gz" ]]; then
126
127 # Is it outdated?
128 if [[ "${file}" -nt "${file}.gz" ]]; then
129
130 # Refresh the GZip companion
131 dir_gz_updates=$((dir_gz_updates+1))
132 ${GZIP_CMD} --keep "${file}"
133
134 # Set file time, owner, access rights as the original
135 chown --reference="${file}" "${file}.gz"
136 chmod --reference="${file}" "${file}.gz"
137 touch --reference="${file}" "${file}.gz"
138 fi
139 else
140
141 # Create a new GZip companion
142 dir_gz_new=$((dir_gz_new+1))
143 ${GZIP_CMD} --keep "${file}"
144
145 # Set file time, owner, access rights as the original
146 chown --reference="${file}" "${file}.gz"
147 chmod --reference="${file}" "${file}.gz"
148 touch --reference="${file}" "${file}.gz"
149 fi
150 done < <(find "${dir}" -type f -name "*.${filetype}" -print0)
151 done
152 #echo
153 log 'info' "Files processed: ${dir_files}"
154 log 'info' "New Brotlis created: ${dir_br_new}"
155 log 'info' "Outdated Brotlis updated: ${dir_br_updates}"
156 log 'info' "New GZips created: ${dir_gz_new}"
157 log 'info' "Outdated GZips updated: ${dir_gz_updates}"
158 total_files=$((total_files+dir_files))
159 total_br_new=$((total_br_new+dir_br_new))
160 total_br_updates=$((total_br_updates+dir_br_updates))
161 total_gz_new=$((total_gz_new+dir_gz_new))
162 total_gz_updates=$((total_gz_updates+dir_gz_updates))
163 else
164 echo "${dir} is not a directory."
165 fi
166done
167log 'info' "Total Directories processed: ${total_dirs}"
168log 'info' "Total Files processed: ${total_files}"
169log 'info' "Total New Brotlis created: ${total_br_new}"
170log 'info' "Total Outdated Brotlis updated: ${total_br_updates}"
171log 'info' "Total New GZips created: ${total_gz_new}"
172log 'info' "Total Outdated GZips updated: ${total_gz_updates}"
173
174# End
175log 'notice' " pre-compression of files for Nginx complete. Have a nice day."
OCSP Staples
1#!/bin/bash
2#
3# OCSP response files creation for stapling in Nginx
4#
5# To be started by cron every few hours
6#
7# For monitoring use the following command:
8#
9# journalctl -t ngx_ocsp_staples -p <level> --no-tail -f
10#
11# while <level> may be any of debug, info, notice or warning
12#
13
14# Exit on errors
15set -e
16
17CERTS_DIRS='/etc/dehydrated/ec_certs /etc/dehydrated/rsa_certs'
18OPENSSL_CMD='/opt/openssl-1.1.0e/bin/openssl'
19NGINX_CMD='/usr/sbin/nginx'
20OCSP_REQ_OPTIONS='-no_nonce -sha256'
21
22# Log to syslog
23log()
24{
25 logger -t ngx_ocsp_staples --priority user.${1} ${2}
26}
27
28log 'notice' "Starting to create Nginx OCSP repsonse files ..."
29
30# We need to be root
31if [[ ${UID} -gt 0 ]]; then
32 log 'error' "Sorry, need to be root"
33 echo "Sorry, need to be root"
34 exit 1
35fi
36
37# Reload Nginx only on changed repsonse files
38NGINX_RELOAD=false
39
40for certs_dir in ${CERTS_DIRS}; do
41
42 # Walk the certificates directory
43 for cert_dir in ${certs_dir}/*; do
44
45 # Is it a directory?
46 if [[ -d "${cert_dir}" ]]; then
47
48 log 'info' "Getting OCSP repsonse for ${cert_dir} ..."
49
50 cd ${cert_dir}
51
52 timestamp="$( date +%s )"
53 cert_file="cert.pem"
54 issuer_cert_file="chain.pem"
55 ca_cert_file="${issuer_cert_file}"
56 response_file="ocsp_response.der"
57
58 # Do we have a certificate?
59 if [[ -f "${cert_file}" ]]; then
60
61 cert_subject=$( ${OPENSSL_CMD} x509 -in ${cert_file} -noout -subject )
62 log 'info' "Certificate file for ${cert_subject} found."
63
64 else
65
66 log 'warning' "No certificate file in ${cert_dir} found, skipping."
67 continue
68
69 fi
70
71 # Do we have an issuer certficate chain file?
72 if [[ -f "${issuer_cert_file}" ]]; then
73
74 issuer_cert_subject=$(
75 ${OPENSSL_CMD} x509 -in ${issuer_cert_file} -noout -subject
76 )
77 log 'info' "Issuer certificate file of ${issuer_cert_subject} found."
78
79 else
80
81 log 'warning' "No issuer certificate file for ${cert_subject} found, skipping."
82 continue
83
84 fi
85
86 # Get the OCSP URI
87 ocsp_uri=$(
88 ${OPENSSL_CMD} x509 -in ${cert_file} -noout -ocsp_uri
89 )
90
91 # Get the Hostname out of the URI
92 ocsp_host=$(
93 echo ${ocsp_uri} | sed -e '\,^[hH][tT][tT][pP]://\([^/]*\).*,!d
94 s//\1/;s/.*@//
95 s/.*://'
96 )
97
98 # Do we already have an OCSP Response file?
99 if [[ -L "${response_file}" ]]; then
100
101 # Validate it
102 log 'info' "Existing OSCP response file found, validating ..."
103 ocsp_verification=$(
104 ${OPENSSL_CMD} ocsp \
105 -issuer ${issuer_cert_file} \
106 -VAfile ${ca_cert_file} \
107 -cert ${cert_file} \
108 -respin ${response_file} \
109 -no_nonce 2>&1
110 )
111 export RC=$?
112 log 'debug' "OpenSSL returns ${RC}: ${ocsp_verification}"
113
114 if [[ ${RC} -eq 0 ]] ; then
115
116 # Has it expired?
117 if [[ ${ocsp_verification} = *'WARNING: Status times invalid'* ]]
118 then
119 log 'notice' "OCSP repsonse file has expired, removing ..."
120 rm ${response_file}
121
122 else
123
124 log 'info' "Existing OSCP response file is valid, skipping."
125 continue
126 fi
127
128 else
129
130 log 'notice' "OCSP repsonse file does not validate, removing ..."
131 rm ${response_file}
132
133 fi
134
135 else
136
137 log 'info' "No previous OCSP repsonse file found."
138
139 fi
140
141 # Don't hammer the OCSP server
142 sleep 5
143
144 # Do the OCSP request
145 log 'notice' "Contacting ${ocsp_host} for validation of ${cert_subject} ..."
146 ocsp_response=$(
147 ${OPENSSL_CMD} ocsp \
148 -issuer ${issuer_cert_file} \
149 -CAfile ${ca_cert_file} \
150 -VAfile ${ca_cert_file} \
151 -cert ${cert_file} \
152 -url ${ocsp_uri} \
153 -header "HOST" "${ocsp_host}" \
154 ${OCSP_REQ_OPTIONS} \
155 -respout ocsp_response-${timestamp}.der 2>&1
156 )
157 export RC=$?
158 log 'debug' "OpenSSL returns ${RC}: ${ocsp_response}"
159
160 # Has a new response file been created?
161 if [[ -f "ocsp_response-${timestamp}.der" ]]; then
162
163 # Read, check and verify it
164 log 'info' "New OSCP response file ocsp_response-${timestamp}.der created, validating ..."
165 ocsp_verification=$(
166 ${OPENSSL_CMD} ocsp \
167 -issuer ${issuer_cert_file} \
168 -VAfile ${ca_cert_file} \
169 -cert ${cert_file} \
170 -respin ocsp_response-${timestamp}.der \
171 -no_nonce 2>&1
172 )
173 export RC=$?
174 log 'debug' "OpenSSL returns ${RC}: ${ocsp_verification}"
175 if [[ ${RC} -eq 0 ]] ; then
176
177 log 'notice' "New OSCP response file for ${cert_subject} is valid, installing..."
178
179 # Update symlink
180 ln --force --symbolic ocsp_response-${timestamp}.der \
181 ${response_file}
182
183 # Nginx will be reloaded
184 NGINX_RELOAD=true
185
186 else
187
188 log 'error' "Created OCSP repsonse file ocsp_response-${timestamp}.der does not validate, skipping."
189 continue
190
191 fi
192
193 else
194
195 log 'error' "OCSP response file ocsp_response-${timestamp}.der creation failed, skipping."
196 continue
197
198 fi
199
200 else
201
202 log 'info' "${cert_dir} is not a directory, skipping ..."
203
204 fi
205
206 done
207
208done
209
210# Do we have to reload Nginx?
211if [[ ${NGINX_RELOAD} == true ]]; then
212
213 # Test Server configuration
214 log 'info' "Checking Nginx server configuration ..."
215 ${NGINX_CMD} -tq
216
217 # Restart Nginx
218 log 'notice' "Relaoding Nginx server ..."
219 ${NGINX_CMD} -s reload
220
221else
222
223 log 'info' "Nginx server does not need to reload"
224
225fi
226
227log 'notice' "Nginx OCSP repsonse files creation complete. Have a nice day."
TLS Session Key Rotation
1#!/bin/bash
2#
3# SSL Session ticket key rotation
4# for Nginx virtual servers
5#
6# To be started by cron every few hours
7#
8# For monitoring use the following command:
9#
10# journalctl -t ngx_key_rotation -p <level> --no-tail -f
11#
12# while <level> may be any of debug, info, notice or warning
13#
14# See also:
15# https://github.com/Fleshgrinder/nginx-session-ticket-key-rotation
16#
17
18# Exit on errors
19set -e
20
21NGINX_USER='www-data'
22NGINX_GROUP='www-data'
23KEYS_DIR='/etc/nginx/tls_session_keys'
24KEY_DISCARD=24 # Time until a key will be discarded (in hours)
25KEY_EXPIRE=8 # Time until a fresh key will be created (in hours)
26KEY_SIZE=48 # 80 bytes for AES256 or 48 byte for AES128 encryption
27OPENSSL_CMD='/usr/bin/openssl'
28NGINX_CMD='/usr/sbin/nginx'
29
30# Add default site to servers list
31VIRTUAL_SERVERS="${VIRTUAL_SERVERS} default_server"
32
33# Log to syslog
34log()
35{
36 logger -t ngx_key_rotation --priority user.${1} ${2}
37}
38
39# Create new random key
40generate_key()
41{
42
43 ${OPENSSL_CMD} rand ${KEY_SIZE} > ${1}
44 chown ${NGINX_USER}:${NGINX_GROUP} ${1}
45 chmod 660 ${1}
46}
47
48# Start
49log 'notice' "Starting Nginx TLS/SSL session key rotation."
50
51# We need to be root
52if [[ ${UID} -gt 0 ]]; then
53 log 'error' "Sorry, need to be root"
54 echo "Sorry, need to be root"
55 exit 1
56fi
57
58# Fix sorting
59LC_ALL=C
60
61# Reload Nginx only on keys change
62NGINX_RELOAD=false
63
64# Get primary virtual server names from Nginx configuration
65VIRTUAL_SERVERS=$( ${NGINX_CMD} -qT | \
66 grep --only-matching --extended-regexp \
67 "^\s*server_name\s+((\w|-|\.)+)" | \
68 grep --only-matching --extended-regexp \
69 "(\w|-|\.)+$" | \
70 sort --unique
71 )
72
73# Create the directory for storing the keys
74mkdir -p ${KEYS_DIR}
75chown ${NGINX_USER}:${NGINX_GROUP} ${KEYS_DIR}
76chmod 770 ${KEYS_DIR}
77
78# Walk the list of virtual servers
79for SERVER_NAME in ${VIRTUAL_SERVERS}; do
80
81 log 'debug' "Checking Nginx TLS session keys for ${SERVER_NAME}"
82
83 # Walk trough keys
84 for KEY_NUM in 3 2 1 ; do
85
86 KEYFILE="${KEYS_DIR}/${SERVER_NAME}.${KEY_NUM}.key"
87
88 # Do we already have this key
89 if [[ -f ${KEYFILE} ]]; then
90
91 log 'debug' "Nginx session key ${KEY_NUM} for ${SERVER_NAME} found."
92 KEY_TIME=$( date +'%s' -r ${KEYFILE} )
93 KEY_EXPIRE_TIME=$( date +%s --date="@$(( ${KEY_TIME} + ( ${KEY_EXPIRE} * 60 * 60 * ${KEY_NUM} ) ))" )
94 KEY_DISCARD_TIME=$( date +%s --date="@$(( ${KEY_TIME} + ( ${KEY_DISCARD} * 60 * 60 ) ))" )
95
96 # Has it reached its end of life?
97 if [[ ${KEY_TIME} -ge $(( ${KEY_DISCARD_TIME} - 30 )) ]]; then
98 log 'info' "Nginx session key ${KEY_NUM} for ${SERVER_NAME} has reached EOL, removing..."
99 rm ${KEYFILE}
100
101 fi
102
103 # Key number lesser then 3?
104 if [[ ${KEY_NUM} -lt 3 ]]; then
105
106 # Has it reached its expiration time?
107 if [[ $( date +'%s') -ge $(( ${KEY_EXPIRE_TIME} - 30 )) ]]; then
108
109 log 'info' "Nginx session key ${KEY_NUM} for ${SERVER_NAME} has expired, moving..."
110 EXP_KEYFILE="${KEYS_DIR}/${SERVER_NAME}.$(( ${KEY_NUM} + 1 )).key"
111 cp --force --preserve=mode,ownership,timestamps ${KEYFILE} ${EXP_KEYFILE}
112 RELOAD_NGINX=true
113
114 # Key number 1?
115 if [[ ${KEY_NUM} -eq 1 ]]; then
116
117 log 'info' "Generating fresh Nginx session key ${KEY_NUM} for ${SERVER_NAME} ..."
118 generate_key ${KEYFILE}
119 RELOAD_NGINX=true
120
121 fi
122
123 fi
124
125 fi
126
127 else
128
129 log 'notice' "No Nginx session key ${KEY_NUM} for ${SERVER_NAME} found. Generating ..."
130 generate_key ${KEYFILE}
131 RELOAD_NGINX=true
132
133 fi
134
135 done
136
137 # Do we already have this key
138 if [[ ! -f ${KEYFILE} ]]; then
139
140 log 'warn' "No Nginx session key ${KEY_NUM} for ${SERVER_NAME} found. Generating ..."
141 generate_key ${KEYFILE}
142 RELOAD_NGINX=true
143
144 fi
145
146done
147
148# Cleanup, remove anything older then 48 hours
149#find ${KEYS_DIR} -type f -mmin +$(( ${KEY_DISCARD} * 2 * 60 ))
150
151# Do we have to reload Nginx?
152if [[ ${RELOAD_NGINX} == true ]]; then
153
154 log 'notice' "Nginx server needs to be reloaded ..."
155
156 # Test Server configuration
157 log 'info' "Checking Nginx server configuration ..."
158 ${NGINX_CMD} -tq
159
160 # Restart Nginx
161 log 'info' "Relaoding Nginx server ..."
162 ${NGINX_CMD} -s reload
163
164fi
165
166log 'notice' "Nginx TLS/SSL session key rotation complete. Have a nice day."
Tor Exit Nodes
1#!/bin/bash
2#
3# Create Nginx map file of Tor Exit Nodes
4#
5
6# Bail-out on errors
7#set -e
8
9# Settings
10MY_IP=$( curl -s --fail --ipv4 http://v4.ipv6-test.com/api/myip.php )
11MY_PORT=443
12FETCH_URL="https://check.torproject.org/cgi-bin/TorBulkExitList.py?ip=${MY_IP}&port=${MY_PORT}"
13MAP_FILE='/etc/nginx/tor-exit-nodes.map'
14
15# Log to syslog
16log()
17{
18 logger -t ngx_tor_exits --priority user.${1} ${2}
19}
20
21valid_ip()
22# Test an IP address for validity:
23# Usage:
24# valid_ip IP_ADDRESS
25# if [[ $? -eq 0 ]]; then echo good; else echo bad; fi
26# OR
27# if valid_ip IP_ADDRESS; then echo good; else echo bad; fi
28#
29{
30 local ip=$1
31 local stat=1
32
33 if [[ ${ip} =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
34 OIFS=${IFS}
35 IFS='.'
36 ip=(${ip})
37 IFS=${OIFS}
38 [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \
39 && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]]
40 stat=$?
41 fi
42 return ${stat}
43}
44
45TMP_DL_FILE=$( mktemp )
46TMP_MAP_FILE=$( mktemp )
47
48# Fetch the list
49log 'notice' "Fetching Tor Exit Nodes ..."
50the_list=$( curl -s --fail --remote-time --remote-time ${MAP_FILE} --url ${FETCH_URL} )
51log 'notice' "Done fetching Nodes"
52
53# Set the field separator to new line
54IFS=$'\n'
55
56while read -r line; do
57 valid_ip ${line}
58 if [[ $? -eq 0 ]]; then
59 line+=' true;'
60 else
61 line="##${line}"
62 fi
63 echo ${line} >> ${TMP_MAP_FILE}
64done <<< "${the_list}"
65
66# Remove old backups
67find /etc/nginx/ -name tor-exit-nodes.map.~* -delete
68
69# Install
70log 'notice' "Installing fresh node list ${MAP_FILE} ..."
71/usr/bin/install --backup=numbered --mode=0664 --preserve-timestamps \
72 ${TMP_MAP_FILE} ${MAP_FILE}
Connect Servers
1#!/bin/bash
2#
3# Connect to all our virutal servers
4#
5
6# Exit on errors
7set -e
8# Return pipe errors
9set -o pipefail
10# Error on undefined variables
11#set -u
12
13NGINX_CMD='/usr/sbin/nginx'
14OPENSSL_CMD='/opt/openssl-1.1.0e/bin/openssl'
15CURL_CMD='/usr/bin/curl'
16CONNECT_HOST='ip4.urown.net'
17
18# Fix sorting
19LC_ALL=C
20
21# Log to syslog
22log()
23{
24 logger -t ngx_connect --priority user.${1} ${2}
25 echo "${1}: ${2}"
26}
27
28# We need to be root
29if [[ ${UID} -gt 0 ]]; then
30 log 'error' "Sorry, need to be root"
31 echo "Sorry, need to be root"
32 exit 1
33fi
34
35# Get primary virtual server names from Nginx configuration
36VIRTUAL_SERVERS=$( ${NGINX_CMD} -qT | \
37 grep --only-matching --extended-regexp \
38 "^\s*server_name\s+((\w|-|\.)+)" | \
39 grep --only-matching --extended-regexp \
40 "(\w|-|\.)+$" | \
41 grep --extended-regexp --invert-match "\.onion$" | \
42 sort --unique
43 )
44
45# Walk the list of virtual servers
46for SERVER_NAME in ${VIRTUAL_SERVERS}; do
47
48 #log 'info' "Connecting to ${SERVER_NAME}"
49
50 echo -en "${SERVER_NAME}\t\tECDSA Key:\t\t"
51 sleep 1
52 ${OPENSSL_CMD} s_client -connect ${CONNECT_HOST}:443 -status \
53 -servername ${SERVER_NAME} -crlf -verify 2 \
54 -cipher 'ECDHE-ECDSA-AES128-GCM-SHA256' \
55 2>&1 < /dev/null | fgrep 'OCSP Response Status'||echo -e ' Failed'
56
57 echo -en "${SERVER_NAME}\t\tRSA Key:\t\t"
58 sleep 1
59 ${OPENSSL_CMD} s_client -servername ${SERVER_NAME} -status \
60 -connect ${CONNECT_HOST}:443 -crlf -verify 2 \
61 -cipher 'ECDHE-RSA-AES128-GCM-SHA256' \
62 2>&1 < /dev/null | fgrep 'OCSP Response Status'||echo -e ' Failed'
63 echo
64done