]> git.friedersdorff.com Git - max/notes.git/blob - content/apache_restart.rst
3312a8ae63064e57c69916fac87884455e68880e
[max/notes.git] / content / apache_restart.rst
1 Fixing nightly Apache crashes and improving the letsencrypt renewal configuration
2 =================================================================================
3
4 :date: 2019-03-03 10:21
5 :category: System Administration
6 :tags: apache, system administration, letsencrypt
7 :authors: Maximilian Friedersdorff
8 :summary: My apache web server is stopping every night.  I investigate why and fix it.
9
10 Over the last few days the Apache web server that runs on my home
11 server has been acting up again.  Every morning I noticed that it 
12 had stopped running at some point in the night.
13
14 This is not the first time this has happened.  In the past, I
15 just restarted the server in the morning and did not think about
16 it too much.  After a week or so the issue would typically sort 
17 itself out.   It's time to fix it properly.
18
19 Since the behaviour is intermittent I'm guessing that Apache is 
20 crashing, so let's take a look at the error log at 
21 ``/var/log/httpd/error_log``.  I'm only really interested at 
22 events that are happening over night, since that is when the
23 server is crashing.  There are ways to `filter a log file by a 
24 date range`_, but since the number of lines to go through is
25 small, I didn't think it was worth the effort.  Here are the
26 lines of interest for two consecutive days::
27
28    [Tue Feb 26 04:20:04.029627 2019] [core:error] [pid 5539:tid 140104264849280] (2)No such file or directory: AH00095: failed to remove PID file /var/run/httpd.pid
29    [Tue Feb 26 04:20:04.076544 2019] [mpm_event:notice] [pid 5539:tid 140104264849280] AH00491: caught SIGTERM, shutting down
30    [Wed Feb 27 04:20:02.324497 2019] [core:error] [pid 11281:tid 140662696130432] (2)No such file or directory: AH00095: failed to remove PID file /var/run/httpd.pid
31    [Wed Feb 27 04:20:02.324674 2019] [mpm_event:notice] [pid 11281:tid 140662696130432] AH00491: caught SIGTERM, shutting down
32
33 On both days, Apache receives a SIGTERM signal, it tries (and fails) to delete 
34 a PID file and then shuts down.  In both cases this happens within seconds of 
35 04:20.  This is clearly a shutdown triggered by some external process, rather 
36 than a crash. It's also happening at a similar time every night, close to a 
37 round number.  I suspect that this is caused by some cronjob.  Let's take a 
38 look::
39
40    # Run hourly cron jobs at 47 minutes after the hour:
41    47 * * * * /usr/bin/run-parts /etc/cron.hourly 1> /dev/null
42    #
43    # Run daily cron jobs at 4:40 every day:
44    40 4 * * * /usr/bin/run-parts /etc/cron.daily 1> /dev/null
45    #
46    # Run weekly cron jobs at 4:30 on the first day of the week:
47    30 4 * * 0 /usr/bin/run-parts /etc/cron.weekly 1> /dev/null
48    #
49    # Run monthly cron jobs at 4:20 on the first day of the month:
50    20 4 1 * * /usr/bin/run-parts /etc/cron.monthly 1> /dev/null
51
52    # Renew ssl certificates
53    20 4 * * * /bin/sh -c "/etc/rc.d/rc.httpd stop && letsencrypt renew && /etc/rc.d/rc.httpd start" 1> /dev/null 2>&1
54
55 This looks promising, there is a single cronjob running nightly at 04:20 that
56 attempts to renew letsencrypt SSL certificates, and it is shutting down Apache
57 in order to do so.  Unfortunately I've been optimistic and redirected all output
58 from that cronjob to ``/dev/null``.  Fortunately, letsencrypt is keeping a log
59 of all renewal attempts at ``/var/log/letsencrypt``.  Here is the relevant line::
60
61    StandaloneBindError: Problem binding to port 80: Could not bind to IPv4 or IPv6.
62
63 That's a bit strange.  Apache is being stopped before the renewal attempt, so 
64 there shouldn't be anything still bound to port 80.  I can use ``netstat`` to 
65 take a look at what is bound to port 80:
66
67 .. code-block:: bash
68
69    # netstat -nlp | grep ':80' | grep -v tcp6
70    tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      11525/nginx: master
71
72 I'm using netstat to list listening (``-l``) ports numericaly (``-n``), along 
73 with the process that owns them (``-p``).  I'm grepping for port 80 and 
74 excluding any IPv6 results.  
75
76 Why is nginx running?  I need to have a word with my past self.
77
78 Nginx is only listening on port 80 and is configured to always respond with a 
79 redirect to https::
80
81    worker_processes  1;
82
83    events {
84            worker_connections  1024;
85    }
86
87    http {
88            include       mime.types;
89            default_type  application/octet-stream;
90
91            keepalive_timeout  65;
92
93            server {
94                    listen 80 default_server;
95                    listen [::]:80 default_server;
96                    server_name _;
97                    return 301 https://$host$request_uri;
98            }
99    }
100
101 I'm not sure what my thought process was when I set this up.  It would be much 
102 better to configure Apache to do perform this redirect instead.  I'm using 
103 Slackware on this server, it doesn't even package nginx so I'm compiling this
104 with a slackbuild from https://slackbuilds.org.  Uninstalling it would be 
105 desirable.
106
107 To perform the same redirect in Apache instead, I've added the following lines 
108 to the configuration file (thanks to `Gordon on Stackoverflow`_)::
109
110    Listen 80
111
112    <VirtualHost *:80>
113    RewriteEngine On
114    RewriteCond %{HTTPS} !=on
115    RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
116    </VirtualHost>
117
118 This allows Apache to respond to requests on port 80 and adds a default 
119 VirtualHost (there are no others for port 80) that responds with a permanent
120 redirect to the https version of the same URL.
121
122 The cronjob can now renew the SSL certificates and successfully restart Apache
123 afterwards.  For additional robustness, the cronjob should restart Apache whether
124 or not the actual renewal was successful::
125
126    # Renew ssl certificates
127    20 4 * * * /bin/sh -c "/etc/rc.d/rc.httpd stop && letsencrypt renew; /etc/rc.d/rc.httpd start" 1> /dev/null 2>&1
128
129 I actually think that I can do one better than that.  Certbot has a mature Apache 
130 plugin that should be able to handle the renewal process using Apache. I wasn't
131 actually expecting this to work.  I changed the value of the ``authenticator``
132 configuration option from ``standalone`` to ``apache`` in the renewal 
133 configuration of letsencrypt. Running ``certbot renew --dry-run`` confirms that 
134 this works successfully.
135
136 I can now make a final change to the cronjob::
137
138    # Renew ssl certificates
139    20 4 * * * certbot renew /dev/null 2>&1
140
141
142 .. _filter a log file by a date range: https://stackoverflow.com/questions/7706095/filter-log-file-entries-based-on-date-range
143 .. _Gordon on Stackoverflow: https://stackoverflow.com/a/4399158