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