RHEL 8 Beta Practicum: Building Functional Web Applications

RHEL 8 Beta offers developers many new features, the listing of which could fill pages; however, it’s always better to explore new things practically. Below, we offer a hands-on approach to creating application infrastructure based on Red Hat Enterprise Linux 8 Beta.

RHEL 8 Beta Practicum: Building Functional Web Applications

We will base our work on Python, a programming language popular among developers, combined with Django and PostgreSQL, a well-known stack for building applications. We will configure RHEL 8 Beta to work with these technologies, and then we’ll add a couple of (non-secret) ingredients.

The testing environment will change, as it’s interesting to explore automation capabilities, work with containers, and try out multi-server environments. To start this new project, we can create a small simple prototype manually—this way, we can see exactly what should happen and how the interactions occur, and then move on to automation and creating more complex configurations. Today, we will discuss how to create such a prototype.

Let’s begin by deploying a RHEL 8 Beta VM image. You can set up a virtual machine from scratch or use the KVM guest image available with the Beta subscription. When using the guest image, you will need to configure a virtual CD that contains the metadata and user data for cloud initialization (cloud-init). There’s no need to do anything special with the disk structure or the available packages; any configuration will do.

Let's take a closer look at the entire process.

Installing Django

With the latest version of Django, you will need a virtual environment (virtualenv) with Python 3.5 or later. The Beta notes mention that Python 3.6 is available, so let’s check if that’s indeed the case:

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

Red Hat actively uses Python as a system tool in RHEL, so why are we seeing such a result?

The fact is that many developers using Python are still considering transitioning from Python 2 to Python 2, while Python 3 is currently in active development, with new versions constantly being released. Therefore, to meet the demand for stable system tools, and simultaneously provide users access to various new Python versions, the system Python has been moved to a new package, allowing for the installation of both Python 2.7 and 3.6. More detailed information about the changes and the reasons behind them can be found in the publication on Langdon White's blog (Langdon White).

So, to get a working Python, you only need to install two packages, with python3-pip pulled in as a dependency.

sudo yum install python36 python3-virtualenv

Why not use direct calls to the module as suggested by Langdon, and install pip3 instead? Keeping in mind the upcoming automation, it is known that Ansible will require pip to be installed, as the pip module does not support virtual environments (virtualenvs) with a custom pip executable.

With a working python3 interpreter at your disposal, you can continue the installation process for Django and set up a working system along with our other components. Numerous implementation options are available online. Here is one version, but users can utilize their own processes.

The versions of PostgreSQL and Nginx available in RHEL 8 by default will be installed using Yum.

sudo yum install nginx postgresql-server

For PostgreSQL, psycopg2 will be needed, but it should only be available in the virtualenv environment, so we will install it using pip3 alongside Django and Gunicorn. But first, we need to set up the virtualenv.

There is often much debate surrounding the proper choice of where to install Django projects, but when doubts arise, one can always refer to the Linux Filesystem Hierarchy Standard. In particular, the FHS states that /srv is used for: "storing data specific to a given node – data produced by the system, such as data and scripts for web servers, data stored on FTP servers, as well as repositories of version control systems (which appeared in FHS-2.3 in 2004)."

This is exactly our case, so we will place everything necessary in /srv, owned by our application user (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

Setting up PostgreSQL and Django is straightforward: we create a database, create a user, and configure permissions. One thing to keep in mind during the initial installation of PostgreSQL is the postgresql-setup script, which is installed with the postgresql-server package. This script helps perform basic tasks related to cluster database administration, such as initializing the cluster or upgrading processes. To configure a new instance of PostgreSQL on a RHEL system, we need to execute the command:

sudo /usr/bin/postgresql-setup --initdb

After that, you can start PostgreSQL using systemd, create a database, and set up the project in Django. Don't forget to restart PostgreSQL after making changes to the client authentication configuration file (usually pg_hba.conf) to configure the password storage for the application user. If you encounter other issues, ensure that the IPv4 and IPv6 settings in the pg_hba.conf file have been modified.

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

In the 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

In the 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 }}',
   }
}

After configuring the settings.py file in the project and setting up the database configuration, you can start the development server to ensure that everything works. After starting the development server, it's a good idea to create an admin user to test the connection to the database.

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

WSGI? What's that?

The development server is useful for testing, but to run the application, you need to configure the corresponding server and proxy for the Web Server Gateway Interface (WSGI). There are several common combinations, such as Apache HTTPD with uWSGI or Nginx with Gunicorn.

The Web Server Gateway Interface task is to redirect requests from web servers to the Python web framework. WSGI is a remnant of a terrible past when CGI mechanisms were prevalent, and today WSGI is essentially a standard, regardless of the web server or Python framework used. However, despite its widespread adoption, there are still many nuances when working with these frameworks and numerous options to choose from. In this case, we will attempt to establish interaction between Gunicorn and Nginx using a socket.

Since both of these components are installed on the same server, we will try to use a UNIX socket instead of a network socket. Since a socket is required for communication anyway, we will take another step and configure socket activation for Gunicorn via systemd.

The process of creating socket-activated services is quite simple. First, a unit file is created that contains the ListenStream directive, pointing to the location where the UNIX socket will be created; then a unit file for the service is created, where the Requires directive will reference the socket unit file. Finally, the service unit file will only need to invoke Gunicorn from the virtual environment and create the WSGI binding for the UNIX socket and the Django application.

Here are several examples of unit files that you can use as a foundation. First, we set up the socket.

[Unit]
Description=Gunicorn WSGI socket

[Socket]
ListenStream=/run/gunicorn.sock

[Install]
WantedBy=sockets.target

Now we need to configure the Gunicorn daemon.

[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

For Nginx, you simply need to create proxy configuration files and set up the directory for storing static content, if you are using it. In RHEL, Nginx configuration files are located in /etc/nginx/conf.d. You can copy the following example into the file /etc/nginx/conf.d/default.conf and start the service. Make sure you specify server_name according to your host name.

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;
   }
}

Start the Gunicorn socket and Nginx using systemd, and you can start testing.

Bad Gateway Error?

If you enter the address in your browser, you will most likely encounter a 502 Bad Gateway error. This can be caused by incorrectly configured permissions for the UNIX socket or more complex issues related to access management in SELinux.

You might find an entry in the nginx error log similar to this:

2018/12/18 15:38:03 [crit] 12734#0: *3 connect() to unix:/run/gunicorn.sock failed (13: Permission denied) while connecting to 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"

If you test Gunicorn directly, you will receive an empty response.

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

Let's explore why this is happening. If you open the log, you will likely see that the issue is related to SELinux. Since we have a daemon running for which no policy was created, it is marked as init_t. Let's check this theory in practice.

sudo setenforce 0

All of this may cause criticism and tears, but it is merely debugging a prototype. We will disable the check just to verify that this is indeed the problem, and then we will revert everything back to its original state.

Refreshing the page in the browser or restarting our curl command will show the Django test page.

So, having confirmed that everything works and there are no further permission issues, we will re-enable SELinux.

sudo setenforce 1

This will not cover audit2allow and creating policies based on alerts using sepolgen, as there is currently no real Django application, and thus no complete map of what Gunicorn might seek access to and what access should be denied. Therefore, it is necessary to keep SELinux active to protect the system while allowing the application to run and log audits, so that a real policy can be created based on them.

Defining permissive domains

Not everyone has heard of permissive domains in SELinux, but they are not new. Many have even worked with them without realizing it. When a policy is created based on audit messages, the resulting policy represents a permissive domain. Let's attempt to create a simple permissive policy.

To create a specific allowed domain for Gunicorn, you need a certain policy and also to label the corresponding files. Additionally, tools are required to compile new policies.

sudo yum install selinux-policy-devel

The allowed domains mechanism is a great tool for identifying issues, especially when it comes to custom applications or applications that come without pre-existing policies. In this case, the allowed domain policy for Gunicorn will be as simple as possible – we will declare a basic type (gunicorn_t), declare a type that we will use to label multiple executable files (gunicorn_exec_t), and then set up a transition for system to properly label running processes. The last line sets the policy as allowed by default upon loading.

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;

You can compile this policy file and add it to the system.

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

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

Let's check if SELinux is blocking anything else apart from what our unknown daemon is trying to access.

sudo ausearch -m AVC

type=AVC msg=audit(1545315977.237:1273): avc: denied { write } for 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 is preventing Nginx from writing data to the UNIX socket used by Gunicorn. Typically, in such cases, policies are modified, but there are other tasks to resolve as well. You can also change the domain settings, transforming it from a confinement domain to a permission domain. Now we will shift httpd_t to the permission domain. This will give Nginx the necessary access, allowing us to continue debugging.

sudo semanage permissive -a httpd_t

So, once we've managed to maintain SELinux protection (in reality, you shouldn't leave a project with SELinux in confinement mode) and permission domains are loading, it’s necessary to determine what exactly needs to be labeled as gunicorn_exec_t for everything to work as intended. Let's try accessing the website to see new access restriction messages.

sudo ausearch -m AVC -c gunicorn

You can see numerous messages containing 'comm="gunicorn"' that perform various actions on files in /srv/djangoapp, so it's clear this is one of the commands that should be marked.

However, in addition, a message appears that looks like this:

type=AVC msg=audit(1545320700.070:1542): avc: denied { execute } for 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

If you check the status of the gunicorn service or run the ps command, no running processes will appear. It seems that gunicorn is trying to access the Python interpreter in our virtualenv environment, possibly to run worker scripts. Therefore, let's mark these two executable files and check if we can open our test Django page.

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

You will need to restart the gunicorn service to select the new label. You can restart it immediately or stop the service and let the socket start it when the site is opened in the browser. Make sure the processes have the correct labels by using ps.

ps -efZ | grep gunicorn

Don't forget to create a proper SELinux policy later!

If you look at the AVC messages now, the last message contains permissive=1 for everything related to the application, and permissive=0 for the rest of the system. By understanding what exactly access is needed for the actual application, you can find the optimal way to solve similar issues more quickly. But until then, it’s better for the system to be secured, and to obtain a clear and useful audit for the Django project.

sudo ausearch -m AVC

Done!

A working Django project has appeared with a frontend on Nginx and Gunicorn WSGI. We set up Python 3 and PostgreSQL 10 from the RHEL 8 Beta repositories. Now we can move on and create (or just deploy) Django applications or explore other available tools in RHEL 8 Beta for automating the setup process, improving performance, or even containerizing this configuration.

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster