Accueil Solution du challenge Melbourne de SadServers.com
Post
Annuler

Solution du challenge Melbourne de SadServers.com

Scenario: “Melbourne”: WSGI with Gunicorn

Level: Medium

Type: Fix

Tags: gunicorn nginx realistic-interviews

Description: There is a Python WSGI web application file at /home/admin/wsgi.py , the purpose of which is to serve the string “Hello, world!”. This file is served by a Gunicorn server which is fronted by an nginx server (both servers managed by systemd). So the flow of an HTTP request is: Web Client (curl) -> Nginx -> Gunicorn -> wsgi.py . The objective is to be able to curl the localhost (on default port :80) and get back “Hello, world!”, using the current setup.

Test: curl -s http://localhost returns Hello, world! (serving the wsgi.py file via Gunicorn and Nginx)

Time to Solve: 20 minutes.

Voyons voir pourquoi ce serveur web ne fonctionne pas :)

1
2
3
4
5
6
7
8
admin@i-0488d0c89dff19acb:/$ curl -v http://localhost 
*   Trying 127.0.0.1:80...
* connect to 127.0.0.1 port 80 failed: Connection refused
* Failed to connect to localhost port 80: Connection refused
* Closing connection 0
curl: (7) Failed to connect to localhost port 80: Connection refused
admin@i-0488d0c89dff19acb:/$ ps aux | grep -i "nginx|gunicorn"
admin        864  0.0  0.1   5276   704 pts/0    S<+  15:50   0:00 grep -i nginx|gunicorn

Pour le moment, rien n’est lancé !

Le fichier wsgi.py est le suivant :

1
2
3
def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html'), ('Content-Length', '0'), ])
    return [b'Hello, world!']

La directive proxy_pass définie dans /etc/nginx/sites-enabled/default m’a semblé étrange avec ses deux schemes mais elle est finalement légitime.

1
2
3
4
5
6
7
8
server {
    listen 80;

    location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn.socket;
    }
}

J’ai trouvé une documentation qui correspond parfaitement à notre situation :

Deploying Gunicorn — Gunicorn 21.2.0 documentation

Le fichier de configuration proxy_params n’apporte rien de bien intéressant :

1
2
3
4
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

Allons voir du côté de systemd avec le fichier /etc/systemd/system/gunicorn.service :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target

[Service]
User=admin
Group=admin
WorkingDirectory=/home/admin
ExecStart=/usr/local/bin/gunicorn \
          --bind unix:/run/gunicorn.sock \
          wsgi
Restart=on-failure

[Install]
WantedBy=multi-user.target

Essayons de lancer la commande manuellement :

1
2
3
4
5
6
7
8
admin@i-0488d0c89dff19acb:~$ /usr/local/bin/gunicorn --bind unix:/run/gunicorn.sock wsgi
[2024-03-02 15:59:08 +0000] [963] [INFO] Starting gunicorn 20.1.0
[2024-03-02 15:59:08 +0000] [963] [ERROR] Retrying in 1 second.
[2024-03-02 15:59:09 +0000] [963] [ERROR] Retrying in 1 second.
[2024-03-02 15:59:10 +0000] [963] [ERROR] Retrying in 1 second.
[2024-03-02 15:59:11 +0000] [963] [ERROR] Retrying in 1 second.
[2024-03-02 15:59:12 +0000] [963] [ERROR] Retrying in 1 second.
[2024-03-02 15:59:13 +0000] [963] [ERROR] Can't connect to /run/gunicorn.sock

L’entrée systemd repose sur une autre unité qui est /etc/systemd/system/gunicorn.socket :

1
2
3
4
5
6
7
8
9
[Unit]
Description=gunicorn socket

[Socket]
ListenStream=/run/gunicorn.sock
# SocketUser=nginx

[Install]
WantedBy=sockets.target

Comme SocketUser est commenté, systemctl va créer /run/gunicorn.sock avec le compte root :

1
2
3
4
5
6
7
8
9
10
11
admin@i-0488d0c89dff19acb:~$ sudo systemctl enable --now gunicorn.socket
admin@i-0488d0c89dff19acb:~$ ls -al /run/gunicorn.sock
srw-rw-rw- 1 root root 0 Mar  2 15:48 /run/gunicorn.sock
admin@i-0488d0c89dff19acb:~$ /usr/local/bin/gunicorn --bind unix:/run/gunicorn.sock wsgi
[2024-03-02 16:01:23 +0000] [992] [INFO] Starting gunicorn 20.1.0
[2024-03-02 16:01:23 +0000] [992] [ERROR] Retrying in 1 second.
[2024-03-02 16:01:24 +0000] [992] [ERROR] Retrying in 1 second.
[2024-03-02 16:01:25 +0000] [992] [ERROR] Retrying in 1 second.
[2024-03-02 16:01:26 +0000] [992] [ERROR] Retrying in 1 second.
[2024-03-02 16:01:27 +0000] [992] [ERROR] Retrying in 1 second.
[2024-03-02 16:01:28 +0000] [992] [ERROR] Can't connect to /run/gunicorn.sock

Pour obtenir plus d’information, on relance la commande avec --log-level DEBUG :

1
2
3
[2024-03-02 16:23:51 +0000] [870] [DEBUG] connection to /run/gunicorn.sock failed: [Errno 13] Permission denied: '/run/gunicorn.sock'
[2024-03-02 16:23:51 +0000] [870] [ERROR] Retrying in 1 second.
[2024-03-02 16:23:52 +0000] [870] [ERROR] Can't connect to /run/gunicorn.sock

Je peux éditer /etc/systemd/system/gunicorn.socket pour mettre l’utilisateur admin comme SocketUser :

1
2
3
4
5
6
7
8
admin@i-097995a6b9078f235:/$ sudo systemctl disable --now gunicorn.socket
Removed /etc/systemd/system/sockets.target.wants/gunicorn.socket.
admin@i-097995a6b9078f235:/$ vi /etc/systemd/system/gunicorn.socket
admin@i-097995a6b9078f235:/$ sudo vi /etc/systemd/system/gunicorn.socket
admin@i-097995a6b9078f235:/$ sudo systemctl enable --now gunicorn.socket
Created symlink /etc/systemd/system/sockets.target.wants/gunicorn.socket → /etc/systemd/system/gunicorn.socket.
admin@i-097995a6b9078f235:/$ ls /run/gunicorn.sock  -al
srw-rw-rw- 1 admin admin 0 Mar  2 16:28 /run/gunicorn.sock

Ça marche en théorie ! Il faut aussi que le dossier /run soit disponible en écriture pour d’autres utilisateurs que root :

1
2
3
4
5
6
7
8
9
10
11
12
13
admin@i-097995a6b9078f235:/$ sudo chmod o+w run/
admin@i-097995a6b9078f235:/$ /usr/local/bin/gunicorn --log-level DEBUG --bind unix:/run/gunicorn.sock wsgi
[2024-03-02 16:32:35 +0000] [10936] [DEBUG] Current configuration:
  config: ./gunicorn.conf.py
  wsgi_app: None
  bind: ['unix:/run/gunicorn.sock']
--- snip ---
[2024-03-02 16:36:55 +0000] [10958] [INFO] Starting gunicorn 20.1.0
[2024-03-02 16:36:55 +0000] [10958] [DEBUG] Arbiter booted
[2024-03-02 16:36:55 +0000] [10958] [INFO] Listening at: unix:/run/gunicorn.sock (10958)
[2024-03-02 16:36:55 +0000] [10958] [INFO] Using worker: sync
[2024-03-02 16:36:55 +0000] [10959] [INFO] Booting worker with pid: 10959
[2024-03-02 16:36:55 +0000] [10958] [DEBUG] 1 workers

Je peux maintenant lancer Nginx :

1
2
3
4
5
6
7
8
9
admin@i-097995a6b9078f235:~$ sudo systemctl start nginx
admin@i-097995a6b9078f235:~$ curl -s http://localhost
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.18.0</center>
</body>
</html>

Si j’avais fait plus attention j’aurais vu dans sa configuration qu’il utilise un nom de socket un peu différent :

1
2
admin@i-097995a6b9078f235:~$ sudo tail /var/log/nginx/error.log
2024/03/02 16:38:27 [crit] 10976#10976: *1 connect() to unix:/run/gunicorn.socket failed (2: No such file or directory) while connecting to upstream, client: 127.0.0.1, server: , request: "GET / HTTP/1.1", upstream: "http://unix:/run/gunicorn.socket:/", host: "localhost"

Je remplace gunicorn.socket par gunicorn.sock dans la configuration du Nginx et je relance :

1
2
3
4
5
6
7
admin@i-00529e3f60fcd6fa9:/$ curl -D- http://127.0.0.1/
HTTP/1.1 200 OK
Server: nginx/1.18.0
Date: Sat, 02 Mar 2024 16:49:11 GMT
Content-Type: text/html
Content-Length: 0
Connection: keep-alive

Pas de contenu. C’est parce que le script wsgi.py met Content-Length à 0.

Une fois l’entête supprimé ça fonctionne :

1
2
3
4
5
6
7
8
9
10
11
admin@i-00529e3f60fcd6fa9:/$ sudo systemctl restart gunicorn.service
admin@i-00529e3f60fcd6fa9:/$ sudo systemctl restart nginx.service
admin@i-00529e3f60fcd6fa9:/$ curl -D- http://127.0.0.1/
HTTP/1.1 200 OK
Server: nginx/1.18.0
Date: Sat, 02 Mar 2024 16:51:34 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive

Hello, world!
Cet article est sous licence CC BY 4.0 par l'auteur.