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