Laboratorio RHEL 8 Beta: Creiamo applicazioni web funzionanti

RHEL 8 Beta offre ai programmatori molte nuove opportunità, elencarle sarebbe lungo, ma studiare il nuovo è sempre meglio tramite la pratica. Di seguito, proponiamo di seguire un laboratorio reale per la creazione di un'infrastruttura applicativa basata su Red Hat Enterprise Linux 8 Beta.

Laboratorio RHEL 8 Beta: Creiamo applicazioni web funzionanti

Prenderemo come base Python, un linguaggio di programmazione molto popolare tra gli sviluppatori, insieme alla combinazione Django e PostgreSQL, un accoppiamento abbastanza comune per la creazione di applicazioni, e configureremo RHEL 8 Beta per lavorare con essi. Poi aggiungeremo qualche altro ingrediente (non segreto).

L'ambiente di test cambierà, dato che è interessante esplorare le possibilità di automazione, il lavoro con i container e provare ambienti con più server. Per iniziare un nuovo progetto, si può cominciare creando un semplice prototipo manualmente – in questo modo si può vedere cosa deve realmente accadere e come si svolge l'interazione, per poi passare all'automazione e alla creazione di configurazioni più complesse. Oggi parleremo della creazione di un tale prototipo.

Iniziamo distribuendo l'immagine della macchina virtuale RHEL 8 Beta VM. È possibile installare la macchina virtuale da zero, oppure utilizzare l'immagine guest KVM fornita con l'abbonamento al Beta. Nel caso in cui si utilizzi l'immagine guest, sarà necessario configurare un CD virtuale che conterrà i metadati e i dati utente per l'inizializzazione nel cloud (cloud-init). Non è necessario fare nulla di speciale con la struttura del disco o i pacchetti disponibili; andrà bene qualsiasi configurazione.

Esaminiamo il processo in dettaglio.

Installazione di Django

Con la versione più recente di Django, è necessario un ambiente virtuale (virtualenv) con Python 3.5 o superiore. Nelle note della Beta è indicato che è disponibile Python 3.6, verifichiamo se è davvero così:

[cloud-user@8beta1 ~]$ python
-bash: python: command not found
[cloud-user@8beta1 ~]$ python3
-bash: python3: command not found

Red Hat utilizza attivamente Python come strumento di sistema in RHEL, quindi perché ottenere questo risultato?

Il fatto è che molti sviluppatori che utilizzano Python stanno ancora considerando di passare da Python 2 a Python 3, mentre Python 3 è in fase di sviluppo attivo e continuano a emergere nuove versioni. Pertanto, per soddisfare la domanda di strumenti di sistema stabili e offrire allo stesso tempo agli utenti accesso a diverse nuove versioni di Python, il Python di sistema è stato trasferito in un nuovo pacchetto e sono state rese disponibili sia Python 2.7 che 3.6. Maggiori dettagli sui cambiamenti e sui motivi di tali scelte possono essere trovati nel post del blog di Langdon White (Langdon White).

Quindi, per ottenere un Python funzionante, è necessario installare solo due pacchetti, e python3-pip sarà installato come dipendenza.

sudo yum install python36 python3-virtualenv

Perché non utilizzare chiamate dirette al modulo, come suggerisce Langdon, e non installare pip3? Tenendo presente l'automazione imminente, si sa che Ansible richiede pip installato, poiché il modulo pip non supporta gli ambienti virtuali (virtualenvs) con un file eseguibile pip personalizzato.

Avendo a disposizione un interprete python3 funzionante, si può procedere con il processo di installazione di Django e ottenere un sistema operativo funzionante insieme agli altri componenti. Ci sono molte opzioni di implementazione disponibili online. Qui viene presentata una versione, ma gli utenti possono utilizzare i propri processi.

Installeremo le versioni di PostgreSQL e Nginx disponibili in RHEL 8 di default tramite Yum.

sudo yum install nginx postgresql-server

Per PostgreSQL sarà necessario psycopg2, ma deve essere disponibile solo nell'ambiente virtualenv, quindi lo installeremo con pip3 insieme a Django e Gunicorn. Ma prima dobbiamo configurare virtualenv.

Sulla scelta corretta della posizione per installare i progetti Django ci sono sempre molte discussioni, ma quando ci sono dei dubbi, si può sempre fare riferimento allo standard Linux Filesystem Hierarchy Standard. Nello specifico, l'FHS afferma che /srv è utilizzato per: «archiviare dati specifici di un nodo, come dati e script dei server web, dati memorizzati sui server FTP e repository dei sistemi di controllo versioni (introdotti nell'FHS-2.3 nel 2004)».

Questo è esattamente il nostro caso, quindi mettiamo tutto il necessario in /srv, di proprietà del nostro utente applicativo (cloud-user).

sudo mkdir /srv/djangoapp
sudo chown cloud-user:cloud-user /srv/djangoapp
cd /srv/djangoapp
virtualenv django
source django/bin/activate
pip3 install django gunicorn psycopg2
./django-admin startproject djangoapp /srv/djangoapp

La configurazione di PostgreSQL e Django è semplice: creiamo un database, un utente e impostiamo i permessi. C'è un aspetto da tenere a mente durante l'installazione di PostgreSQL: lo script postgresql-setup, che viene installato insieme al pacchetto postgresql-server. Questo script aiuta a svolgere attività di base di amministrazione del cluster di database, come inizializzare il cluster o gestire il processo di aggiornamento. Per configurare una nuova istanza di PostgreSQL su un sistema RHEL, dobbiamo eseguire il comando:

sudo /usr/bin/postgresql-setup -initdb

Dopo aver fatto ciò, possiamo avviare PostgreSQL tramite systemd, creare il database e configurare il progetto in Django. Non dimenticare di riavviare PostgreSQL dopo aver apportato modifiche al file di configurazione dell'autenticazione del client (di solito, pg_hba.conf) per impostare la memorizzazione della password per l'utente dell'applicazione. Se incontri altre difficoltà, assicurati che le impostazioni IPv4 e IPv6 siano corrette nel file pg_hba.conf.

systemctl enable --now postgresql

sudo -u postgres psql
postgres=# create database djangoapp;
postgres=# create user djangouser with password 'qwer4321';
postgres=# alter role djangouser set client_encoding to 'utf8';
postgres=# alter role djangouser set default_transaction_isolation to 'read committed';
postgres=# alter role djangouser set timezone to 'utc';
postgres=# grant all on DATABASE djangoapp to djangouser;
postgres=# q

Nel file /var/lib/pgsql/data/pg_hba.conf:

# IPv4 local connections:
host    all        all 0.0.0.0/0                md5
# IPv6 local connections:
host    all        all ::1/128                 md5

Nel file /srv/djangoapp/settings.py:

# Database
DATABASES = {
   'default': {
       'ENGINE': 'django.db.backends.postgresql_psycopg2',
       'NAME': '{{ db_name }}',
       'USER': '{{ db_user }}',
       'PASSWORD': '{{ db_password }}',
       'HOST': '{{ db_host }}',
   }
}

Dopo aver configurato il file settings.py del progetto e la configurazione del database, puoi avviare il server di sviluppo per assicurarti che tutto funzioni. Dopo aver avviato il server di sviluppo, è consigliabile creare un utente admin per testare la connessione al database.

./manage.py runserver 0.0.0.0:8000
./manage.py createsuperuser

WSGI? Che cos'è?

Il server di sviluppo è utile per il collaudo, ma per eseguire l'applicazione è necessario configurare adeguatamente un server e un proxy per il Web Server Gateway Interface (WSGI). Ci sono diverse combinazioni comuni, come Apache HTTPD con uWSGI o Nginx con Gunicorn.

Il compito del Web Server Gateway Interface è quello di reindirizzare le richieste da il server web. al framework web Python. WSGI è una sorta di eredità di un passato disastroso, quando i meccanismi CGI erano in uso; oggi WSGI è praticamente uno standard, indipendentemente dal server web o dal framework Python utilizzato. Ma nonostante la sua ampia diffusione, ci sono ancora molte sfide da affrontare quando si lavora con questi framework e molte opzioni tra cui scegliere. In questo caso, cercheremo di configurare l'interazione tra Gunicorn e Nginx tramite un socket.

Poiché entrambi questi componenti sono installati sullo stesso server, proveremo a utilizzare un socket UNIX invece di un socket di rete. Poiché è comunque necessario un socket per le comunicazioni, cerchiamo di fare un ulteriore passo avanti e configurare l'attivazione del socket per Gunicorn tramite systemd.

Il processo di creazione di servizi attivati da socket è abbastanza semplice. Prima si crea un file unit, che contiene la direttiva ListenStream, che indica il punto in cui sarà creato il socket UNIX; poi un file unit per il servizio, in cui la direttiva Requires punta al file unit del socket. Infine, nel file unit del servizio, rimarrà solo da eseguire Gunicorn dall'ambiente virtuale e creare un binding WSGI per il socket UNIX e l'applicazione Django.

Ecco alcuni esempi di file unit che puoi utilizzare come base. Prima di tutto, configuriamo il socket.

[Unit]
Description=Gunicorn WSGI socket

[Socket]
ListenStream=/run/gunicorn.sock

[Install]
WantedBy=sockets.target

Ora è necessario configurare il daemon Gunicorn.

[Unit]
Description=Gunicorn daemon
Requires=gunicorn.socket
After=network.target

[Service]
User=cloud-user
Group=cloud-user
WorkingDirectory=/srv/djangoapp

ExecStart=/srv/djangoapp/django/bin/gunicorn 
         --access-logfile - 
         --workers 3 
         --bind unix:gunicorn.sock djangoapp.wsgi

[Install]
WantedBy=multi-user.target

Per Nginx, è sufficiente creare file di configurazione del proxy e configurare la directory per memorizzare il contenuto statico, se la utilizzi. In RHEL, i file di configurazione di Nginx si trovano in /etc/nginx/conf.d. Puoi copiare il seguente esempio nel file /etc/nginx/conf.d/default.conf e avviare il servizio. Assicurati di specificare server_name in base al nome del tuo host.

server {
   listen 80;
   server_name 8beta1.example.com;

   location = /favicon.ico { access_log off; log_not_found off; }
   location /static/ {
       root /srv/djangoapp;
   }

   location / {
       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;
       proxy_pass http://unix:/run/gunicorn.sock;
   }
}

Avvia il socket Gunicorn e Nginx tramite systemd, quindi puoi procedere con i test.

Errore Bad Gateway?

Se inserisci l'indirizzo nel browser, probabilmente riceverai un errore 502 Bad Gateway. Questo problema può essere causato da permessi configurati in modo errato per il socket UNIX, o da problemi di gestione dell'accesso più complessi legati a SELinux.

Nel registro degli errori di nginx, puoi trovare una riga del tipo:

2018/12/18 15:38:03 [crit] 12734#0: *3 connect() a unix:/run/gunicorn.sock fallito (13: Permesso negato) mentre si connetteva all'upstream, client: 192.168.122.1, server: 8beta1.example.com, request: "GET / HTTP/1.1", upstream: "http://unix:/run/gunicorn.sock:/", host: "8beta1.example.com"

Se testiamo Gunicorn direttamente, otterremo una risposta vuota.

curl —unix-socket /run/gunicorn.sock 8beta1.example.com

Vediamo perché questo accade. Aprendo il registro, vedremo probabilmente che il problema è legato a SELinux. Poiché stiamo eseguendo un demone per il quale non è stata creata una propria politica, viene contrassegnato come init_t. Verifichiamo questa teoria nella pratica.

sudo setenforce 0

Tutto questo può generare critiche e lacrime di sangue, ma è solo il debug di un prototipo. Disabilitiamo il controllo solo per assicurarci che il problema sia proprio questo, dopo di che riporteremo tutto al suo posto.

Aggiornando la pagina nel browser o riavviando il nostro comando curl, possiamo visualizzare la pagina di test di Django.

Quindi, assicurandoci che tutto funzioni e che non ci siano più problemi di permessi, riaccendiamo SELinux.

sudo setenforce 1

Qui non parleremo di audit2allow e della creazione di politiche basate su avvisi tramite sepolgen, poiché al momento non esiste un'applicazione Django reale, quindi non c'è una mappa completa di ciò a cui Gunicorn potrebbe voler accedere e a cosa questo accesso dovrebbe essere negato. Pertanto, è necessario mantenere il funzionamento di SELinux per proteggere il sistema, consentendo nel contempo all'applicazione di partire e registrare messaggi nel registro audit, in modo da poter successivamente creare una politica reale.

Indicazione dei domini permissivi

Non tutti hanno sentito parlare dei domini permissivi in SELinux, ma non c'è nulla di nuovo al riguardo. Molti hanno persino lavorato con essi, senza nemmeno rendersene conto. Quando viene creata una politica basata sui messaggi di audit, la politica creata rappresenta un dominio permissivo. Proviamo a creare una politica permissiva semplicissima.

Per creare un dominio permissivo specifico per Gunicorn, è necessaria una certa politica e sarà necessario contrassegnare i file pertinenti. Inoltre, sono necessari strumenti per assemblare nuove politiche.

sudo yum install selinux-policy-devel

Il meccanismo dei domini permissivi è uno strumento eccellente per identificare problemi, specialmente quando si tratta di applicazioni personalizzate o di applicazioni che vengono fornite senza politiche già create. In questo caso, la politica del dominio permissivo per Gunicorn sarà il più semplice possibile: dichiariamo il tipo principale (gunicorn_t), dichiariamo il tipo che useremo per contrassegnare diversi file eseguibili (gunicorn_exec_t) e poi impostiamo la transizione per il sistema, in modo da contrassegnare correttamente i processi in esecuzione. L'ultima riga imposta la politica come permissiva per impostazione predefinita al momento del caricamento.

gunicorn.te:

policy_module(gunicorn, 1.0)

type gunicorn_t;
type gunicorn_exec_t;
init_daemon_domain(gunicorn_t, gunicorn_exec_t)
permissive gunicorn_t;

Possiamo compilare questo file di politica e aggiungerlo al sistema.

make -f /usr/share/selinux/devel/Makefile
sudo semodule -i gunicorn.pp

sudo semanage permissive -a gunicorn_t
sudo semodule -l | grep permissive

Controlliamo se SELinux sta bloccando qualcos'altro oltre a ciò a cui accede il nostro demone sconosciuto.

sudo ausearch -m AVC

type=AVC msg=audit(1545315977.237:1273): avc: denied { write } per pid=19400 comm="nginx" name="gunicorn.sock" dev="tmpfs" ino=52977 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:var_run_t:s0 tclass=sock_file permissive=0

SELinux non consente a Nginx di scrivere dati nel socket UNIX utilizzato da Gunicorn. Di solito, in questi casi, si inizia a modificare le politiche, ma ci sono ancora altre questioni da risolvere. Possiamo anche modificare le impostazioni del dominio, trasformandolo da dominio di restrizioni a dominio di permessi. Ora trasferiamo httpd_t nel dominio permissivo. Questo garantirà a Nginx l'accesso necessario, e possiamo continuare con il lavoro di debug.

sudo semanage permissive -a httpd_t

Quindi, una volta riusciti a mantenere la protezione di SELinux (in realtà non si dovrebbe lasciare un progetto con SELinux in modalità restrittiva) e i domini permissivi vengono caricati, è necessario capire cosa esattamente deve essere contrassegnato come gunicorn_exec_t affinché tutto funzioni correttamente di nuovo. Proviamo a contattare il sito web per vedere se ci sono nuovi messaggi sui limiti di accesso.

sudo ausearch -m AVC -c gunicorn

Possiamo vedere numerosi messaggi che contengono ‘comm="gunicorn"’, che eseguono varie operazioni sui file in /srv/djangoapp, quindi, ovviamente, questa è una delle comandi che vale la pena contrassegnare.

Ma inoltre, appare un messaggio di questo tipo:

type=AVC msg=audit(1545320700.070:1542): avc: denied { execute } per pid=20704 comm="(gunicorn)" name="python3.6" dev="vda3" ino=8515706 scontext=system_u:system_r:init_t:s0 tcontext=unconfined_u:object_r:var_t:s0 tclass=file permissive=0

Se si controlla lo stato del servizio gunicorn o si esegue il comando ps, non appariranno processi in esecuzione. Sembra che gunicorn stia cercando di accedere all'interprete Python nel nostro ambiente virtualenv, forse per lanciare i processi di lavoro (workers). Quindi ora contrassegneremo questi due file eseguibili e verificheremo se riusciamo ad aprire la nostra pagina di prova Django.

chcon -t gunicorn_exec_t /srv/djangoapp/django/bin/gunicorn /srv/djangoapp/django/bin/python3.6

Sarà necessario riavviare il servizio gunicorn per poter scegliere il nuovo tag. Può essere riavviato immediatamente o si può fermare il servizio e lasciare che il socket lo avvii quando si apre il sito nel browser. Assicurati che i processi abbiano ricevuto i tag corretti, utilizzando ps.

ps -efZ | grep gunicorn

Non dimenticare poi di creare una politica SELinux adeguata!

Se ora si controllano i messaggi AVC, l'ultimo messaggio contiene permissive=1 per tutto ciò che riguarda l'applicazione e permissive=0 per il resto del sistema. Comprendendo quale accesso è realmente necessario per l'applicazione, si può trovare più rapidamente un modo ottimale per risolvere tali problemi. Ma fino ad allora è meglio che il sistema sia protetto e che si ottenga un audit comprensibile e utilizzabile per il progetto Django.

sudo ausearch -m AVC

Fatto!

È emerso un progetto Django funzionante con frontend su Nginx e Gunicorn WSGI. Abbiamo configurato Python 3 e PostgreSQL 10 dai repository RHEL 8 Beta. Ora possiamo andare avanti e creare (o semplicemente distribuire) applicazioni Django o esplorare altri strumenti disponibili in RHEL 8 Beta per automatizzare il processo di configurazione, migliorare le prestazioni o persino containerizzare questa configurazione.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS 🔥 Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS | ProHoster