license gpl, per gnu license recommendations of >300 lines of code
[basic-https-conf] / web-conf
1 #!/bin/bash
2 # This file is part of web-conf which configures web servers
3 # Copyright (C) 2024 Ian Kelling
4
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18 # SPDX-License-Identifier: GPL-3.0-or-later
19
20 [[ $EUID == 0 ]] || exec sudo -E "$BASH_SOURCE" "$@"
21
22 set -eE -o pipefail
23 trap 'echo "$0:$LINENO:error: \"$BASH_COMMAND\" returned $?" >&2' ERR
24
25 readonly this_file="$(readlink -f -- "${BASH_SOURCE[0]}")"
26 readonly this_dir="${this_file%/*}"
27
28 shopt -s nullglob # used in apache config file expansion
29
30 usage() {
31 cat <<EOF
32 Usage: ${0##*/} [OPTIONS] [EXTRA_SETTINGS_FILE] apache2|nginx DOMAIN
33 apache/nginx config & let's encrypt
34
35 If using tls then it expects certbot to be installed and in PATH. Also,
36 certbot cronjob should be taken care of outside this script. In the
37 debian package, it installs a systemd timer. If a script exists (I
38 expect it only on my , Ian Kelling's sytem) we install a systemd timer
39 to on failure. You can see the relevant script in my git repo
40 distro-setup, and log-quiet.
41
42
43 /a/bin/distro-setup/certbot-renew-hook
44
45
46
47 EXTRA_SETTINGS_FILE can be - for stdin
48 -a IPv4_ADDR IP address to listen on. Default all addresses.
49 ipv6 address support could be added to this script.
50 -c CERT_FOLDER No letsencrypt. use fullchain.pem and privkey.pem in this folder.
51 -e EMAIL Contact address for let's encrypt. Default is
52 root@\$(hostname --fqdn')
53 which is root@$(hostname --fqdn) on this host.
54 -f [ADDR:]PORT Enable proxy to [ADDR:]PORT. ADDR default is 127.0.0.1
55 -i Insecure, no ssl.
56 -p PORT Main port to listen on, default 443. 80 implies -i.
57 -r DIR DocumentRoot
58 -s Allow symlinks from the doucment root
59 -t No settings on documentroot.
60 -h|--help Print help and exit
61
62 Note: Uses GNU getopt options parsing style
63 EOF
64 exit $1
65 }
66
67 ##### begin command line parsing ########
68
69 symlinkarg=-
70 ssl=true
71 extra_settings=
72 port=443
73 do_root_settings=true
74 temp=$(getopt -l help a:c:e:if:p:r:sth "$@") || usage 1
75 vhostip='*'
76 eval set -- "$temp"
77 while true; do
78 case $1 in
79 -a)
80 listenip="$2:"
81 vhostip="$2"
82 shift 2 ;;
83 -c) oob_cert_dir="$2"; shift 2 ;;
84 -e) email="$2"; shift 2 ;;
85 -f) proxy="$2"; shift 2 ;;
86 -i) ssl=false; shift ;;
87 -p) port="$2"; shift 2 ;;
88 -r) root="$2"; shift 2 ;;
89 -t) do_root_settings=false; shift ;;
90 -s) symlinkarg=+; shift ;;
91 --) shift; break ;;
92 -h|--help) usage ;;
93 *) echo "$0: Internal error!" ; exit 1 ;;
94 esac
95 done
96
97 # t = type, h = host
98 if (( ${#@} == 3 )); then
99 read -r extra_settings t h <<<"${@}"
100 else
101 read -r t h <<<"${@}"
102 fi
103
104 case $t in
105 apache2|nginx) : ;;
106 *) echo "$0: error: expected apache2 or nginx arg"; usage 1 ;;
107 esac
108
109 if [[ ! $h ]]; then
110 echo "$0: error: expected domain and type arg"
111 usage 1
112 fi
113
114 if [[ ! $root ]]; then
115 root=/var/www/$h/html
116 fi
117
118 if [[ $proxy ]]; then
119 [[ $proxy == *:* ]] || proxy=127.0.0.1:$proxy
120 fi
121
122 if [[ ! $email ]]; then
123 email=root@$(hostname --fqdn)
124 fi
125
126
127 ##### end command line parsing ########
128
129 se=/etc/$t/sites-enabled
130 if [[ $oob_cert_dir ]]; then
131 cert_dir="$oob_cert_dir"
132 else
133 cert_dir=/etc/letsencrypt/live/$h
134 fi
135
136 mkdir -p $root
137 case $port in
138 80|443)
139 vhost_file=$se/$h.conf
140 ;;
141 *)
142 vhost_file=$se/$h-$port.conf
143 ;;
144 esac
145 redir_file=$se/$h-redir.conf
146
147 if [[ $port == 80 ]]; then
148 ssl=false
149 # remove any thats hanging around
150 rm -f $redir_file
151 fi
152
153
154 if [[ ! $oob_cert_dir ]] && $ssl; then
155
156 $this_dir/certbot-setup $t
157
158 f=$cert_dir/fullchain.pem
159 threedays=259200 # in seconds
160 if [[ ! -e $f ]] || ! openssl x509 -checkend $threedays -noout -in $f >/dev/null; then
161 # cerbot needs an existing virtualhost.
162 $0 -p 80 $t $h
163 # when generating an example config, add all relevant security options:
164 # --hsts --staple-ocsp --uir --must-staple
165 certbot certonly -n --email $email --no-self-upgrade \
166 --agree-tos --${t%2} -d $h
167 # cleanup the call to ourselves a short bit ago
168 rm $se/$h.conf
169 fi
170 # these scripts only run on renew, that is kinda dumb.
171 export RENEWED_LINEAGE=/etc/letsencrypt/live/$h
172 for script in /etc/letsencrypt/renewal-hooks/deploy/*; do
173 if [[ -x $script ]]; then
174 "$script"
175 fi
176 done
177 fi
178
179
180 if [[ $t == apache2 ]]; then
181 rm -f $se/000-default.conf
182 # note, we exepct ServerRoot of /etc/apache2
183 # apache requires exactly 1 listen directive per port (when no ip is also given),
184 # so we have to parse the config to do it programatically.
185 listen_80=false
186 listen_port=false
187 cd /etc/apache2
188 conf_files=(apache2.conf)
189
190
191 for (( i=0; i < ${#conf_files[@]}; i++ )); do
192 f="${conf_files[i]}"
193 # note: globs are expanded here.
194 conf_files+=( $(sed -rn "s,^\s*Include(Optional)?\s+(\S+).*,\2,p" "$f") )
195 case $(readlink -f "$f") in
196 $vhost_file|$redir_file) continue ;;
197 esac
198 for p in $(sed -rn "s,^\s*listen\s+(\S+).*,\1,Ip" "$f"); do
199 case $p in
200 80) listen_80=true ;;&
201 $port) listen_port=true ;;
202 esac
203 done
204 done
205
206 echo "$0: creating $vhost_file"
207 cat >$vhost_file <<EOF
208 <VirtualHost $vhostip:$port>
209 ServerName $h
210 ServerAlias www.$h
211 DocumentRoot $root
212 EOF
213 if $do_root_settings; then
214 cat >>$vhost_file <<EOF
215 <Directory $root>
216 Options -Indexes ${symlinkarg}FollowSymlinks
217 </Directory>
218 EOF
219 fi
220
221 if [[ $extra_settings ]]; then
222 cat -- $extra_settings >>$vhost_file
223 fi
224
225 # go faster!
226 if [[ -e /etc/apache2/mods-available/http2.load ]]; then
227 # https://httpd.apache.org/docs/2.4/mod/mod_http2.html
228 a2enmod -q http2
229 cat >>$vhost_file <<EOF
230 Protocols h2 http/1.1
231 EOF
232 fi
233
234 if [[ $proxy ]]; then
235 a2enmod -q proxy proxy_http
236 # fyi: trailing slash is important
237 # reference: https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html
238 # retry=0: https://stackoverflow.com/questions/683052/why-am-i-getting-an-apache-proxy-503-error
239 cat >>$vhost_file <<EOF
240 ProxyPass "/" "http://$proxy/" retry=0
241 ProxyPassReverse "/" "http://$proxy/"
242 EOF
243 fi
244
245
246 if $ssl; then
247 a2enmod -q headers
248 https_arg=" https"
249 common_ssl_conf=/etc/apache2/common-ssl.conf
250 cat >>$vhost_file <<EOF
251 SSLCertificateFile $cert_dir/fullchain.pem
252 SSLCertificateKeyFile $cert_dir/privkey.pem
253 Include $common_ssl_conf
254 # From cerbot generated config example, taken 4/2017,
255 # should be rechecked once a year or so.
256 Header always set Strict-Transport-Security "max-age=31536000"
257 SSLUseStapling on
258 Header always set Content-Security-Policy upgrade-insecure-requests
259 EOF
260
261 if (( port == 443 )); then
262 echo "$0: creating $redir_file"
263
264 # note, alternatively:
265 cat >/dev/null <<'EOF'
266 #https://webmasters.stackexchange.com/questions/124635/apache-redirect-http-to-https-without-preventing-http
267 <If "%{req:Upgrade-Insecure-Requests} == '1'">
268 Redirect permanent "/" "https://mydomain.ltd/"
269 </If>
270 # or, with generic rewrite, we use this on gnu.org
271 RewriteEngine on
272 RewriteCond %{HTTP:Upgrade-Insecure-Requests} "^1$"
273 RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,QSA,R=307]
274 EOF
275
276 cat >$redir_file <<EOF
277 <VirtualHost *:80>
278 ServerName $h
279 ServerAdmin webmaster@localhost
280 DocumentRoot /var/www/html
281
282 ErrorLog \${APACHE_LOG_DIR}/error.log
283 CustomLog \${APACHE_LOG_DIR}/access.log vhost_time_combined
284
285 RewriteEngine on
286 RewriteCond %{SERVER_NAME} =$h
287 RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,QSA,R=permanent]
288 </VirtualHost>
289 EOF
290 if ! $listen_80; then
291 cat >>$redir_file <<'EOF'
292 Listen 80
293 EOF
294 fi
295 fi
296
297 # this is a copy of a file certbot, see below.
298 echo "$0: creating $common_ssl_conf"
299 cat >$common_ssl_conf <<'EOF'
300 # This file contains important security parameters. If you modify this file
301 # manually, Certbot will be unable to automatically provide future security
302 # updates. Instead, Certbot will print and log an error message with a path to
303 # the up-to-date file that you will need to refer to when manually updating
304 # this file. Contents are based on https://ssl-config.mozilla.org
305
306 SSLEngine on
307
308 # Intermediate configuration, tweak to your needs
309 SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
310 SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
311 SSLHonorCipherOrder off
312 SSLSessionTickets off
313
314 SSLOptions +StrictRequire
315
316 # Add vhost name to log entries:
317 LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" vhost_combined
318 LogFormat "%v %h %l %u %t \"%r\" %>s %b" vhost_common
319 EOF
320
321 upstream=https://raw.githubusercontent.com/certbot/certbot/master/certbot-apache/certbot_apache/_internal/tls_configs/current-options-ssl-apache.conf
322 if ! diff -u <(wget -q -O - $upstream) $common_ssl_conf; then
323 cat <<EOF
324 WARNING!!!!!!!!!
325 WARNING!!!!!!!!!
326 WARNING!!!!!!!!!
327 WARNING!!!!!!!!!
328 WARNING!!!!!!!!!
329 upstream ssl settings differ from the snapshot we have taken!!!
330 We diffed with this command:
331 diff -c <(wget -q -O - $upstream) $common_ssl_conf
332 Update this script to take care this warning!!!!!
333 EOF
334 sleep 1
335 fi
336 fi # end if $ssl
337
338 cat >>$vhost_file <<'EOF'
339 ErrorLog ${APACHE_LOG_DIR}/error.log
340 CustomLog ${APACHE_LOG_DIR}/access.log vhost_time_combined
341 </VirtualHost>
342 EOF
343
344 if ! $listen_port; then
345 # reference: https://httpd.apache.org/docs/2.4/mod/mpm_common.html#listen
346 cat >>$vhost_file <<EOF
347 listen ${listenip}${port}${https_arg}
348 EOF
349 fi
350
351
352 a2enmod -q ssl rewrite # rewrite needed for httpredir
353 service apache2 restart
354
355 # I rarely look at how much traffic I get, so let's keep that info
356 # around for longer than the default of 2 weeks.
357 sed -ri --follow-symlinks 's/^(\s*rotate\s).*/\1 365/' /etc/logrotate.d/apache2
358 fi ###### end if apache
359
360 if [[ $t == nginx ]]; then
361 common_ssl_conf=/etc/nginx/common-ssl.conf
362
363 rm -f $se/default
364 cd /etc/nginx
365 [[ -e dh2048.pem ]] || openssl dhparam -out dh2048.pem 2048
366
367 if $ssl; then
368 ssl_arg=ssl
369 if nginx -V |& grep -- '--with-http_v2_module\b' &>/dev/null; then
370 # fun fact: nginx can be configured to do http2 without ssl.
371 ssl_arg+=" http2"
372 fi
373 fi
374
375 cat >$common_ssl_conf <<'EOF'
376 # let's encrypt gives us a bad nginx config, so use this:
377 # https://mozilla.github.io/server-side-tls/ssl-config-generator/
378 # using modern config. last checked 2017/4/22
379 ssl_session_timeout 1d;
380 ssl_session_cache shared:SSL:50m;
381 ssl_session_tickets off;
382
383 # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
384 ssl_dhparam /etc/nginx/dh2048.pem;
385
386 # modern configuration. tweak to your needs.
387 ssl_protocols TLSv1.2;
388 ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
389 ssl_prefer_server_ciphers on;
390
391 # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
392 add_header Strict-Transport-Security max-age=15768000;
393
394 # OCSP Stapling ---
395 # fetch OCSP records from URL in ssl_certificate and cache them
396 ssl_stapling on;
397 ssl_stapling_verify on;
398
399 ## verify chain of trust of OCSP response using Root CA and Intermediate certs
400 # ian: commented out, unnecessary for le certs or my nginx ver.
401 #ssl_trusted_certificate $cert_dir/fullchain.pem;;
402
403 # ian: commented out, our local dns is expected to work fine.
404 #resolver <IP DNS resolver>;
405 EOF
406 cat >$vhost_file <<EOF
407 server {
408 server_name $h www.$h;
409 root $root;
410 listen $listenip$port $ssl_arg;
411 EOF
412 if [[ ! $listenip ]]; then
413 cat >>$vhost_file <<EOF
414 listen [::]:$port $ssl_arg;
415 EOF
416 fi
417 if $do_root_settings; then
418 cat >>$vhost_file <<EOF
419 location $root {
420 autoindex off;
421 }
422 EOF
423 fi
424 if $ssl; then
425 cat >>$vhost_file <<EOF
426 ssl_certificate $cert_dir/fullchain.pem;
427 ssl_certificate_key $cert_dir/privkey.pem;
428 include $common_ssl_conf;
429 EOF
430
431 if (( port == 443 )); then
432 cat >$redir_file <<EOF
433 server {
434 server_name $h www.$h;
435 listen 80 $http2_arg;
436 listen [::]:80 $http2_arg;
437 return 301 https://\$server_name\$request_uri;
438 }
439 EOF
440 fi
441 fi # end if $ssl
442
443 if [[ $extra_settings ]]; then
444 cat $extra_settings >>$vhost_file
445 fi
446
447 if [[ $proxy ]]; then
448 cat >>$vhost_file <<EOF
449 location / {
450 proxy_set_header Host \$host;
451 proxy_set_header X-Real-IP \$remote_addr;
452 proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
453 proxy_set_header X-Forwarded-Ssl on;
454 proxy_set_header X-Forwarded-Port $port;
455 proxy_pass http://$proxy;
456 }
457 EOF
458 fi
459
460 cat >>$vhost_file <<EOF
461 }
462 EOF
463
464
465 service nginx restart
466
467 fi ####### end if nginx
468
469 cat >/etc/apache2/conf-enabled/local-custom.conf <<'EOF'
470 # vhost_combined with %D (request time in microseconds)
471 # this file is just a convenient place to drop it.
472 LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\" %D" vhost_time_combined
473 SSLStaplingCache shmcb:/var/run/apache2/stapling_cache(128000)
474 EOF