📜 ⬆️ ⬇️

RHEL 8 Beta Workshop: We build working web applications

RHEL 8 Beta offers developers a lot of new features, which can take pages to list, however, it is always better to learn new things in practice, so we offer the following practical training to create real application infrastructure based on Red Hat Enterprise Linux 8 Beta.



Take Python, a combination of Django and PostgreSQL, a fairly common link for building applications, and configure RHEL 8 Beta to work with them. Then add a couple more (non-secret) ingredients.

The test environment will change, because it is curious to explore the possibilities of automation, working with containers and try environments with several servers. To start working with a new project, you can start by creating a small simple prototype manually - this way you can see what exactly should happen and how the interaction takes place, and then move on to automating and creating more complex configurations. Today, the story of the creation of such a prototype.
')
Begin by deploying the RHEL 8 Beta VM virtual machine image. You can install a virtual machine from scratch, or use a KVM guest image that is available with a Beta subscription. When using a guest image, you will need to configure a virtual CD, which will contain metadata and user data for cloud initialization (cloud-init). You do not need to do anything special with the disk structure or available packages, any configuration will do.

Let's look at the whole process in more detail.

Install django


The latest version of Django will require a virtual environment (virtualenv) with Python 3.5 or later. In the Beta notes you can see that Python 3.6 is available, let's check if this is really 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 is this result?

The fact is that many developers using Python are still thinking about switching from Python 2 to Python 2, while Python 3 itself is under active development, and more and more new versions are constantly appearing. Therefore, in order to satisfy the need for stable system tools, and at the same time offer users access to various new versions of Python, system Python was transferred to a new package and provided the ability to install both Python 2.7 and 3.6. More information about the changes and why this was done can be found in a blog post by Langdon White .

So, to get a working Python, you need to install only two packages, while the python3-pip will pull up in the form of a dependency.

 sudo yum install python36 python3-virtualenv 

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

With a working python3 interpreter at your disposal, you can continue the Django installation process and get a working system along with our other components. The network presents many options for implementation. One version is presented here, but users can use their own processes.

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

 sudo yum install nginx postgresql-server 

PostgreSQL requires psycopg2, but you only need it to be available in virtualenv environment, so we will install it using pip3 with Django and Gunicorn. But first we need to set up virtualenv.

There is always a lot of controversy about choosing the right place to install Django projects, but when there are any doubts, you can always refer to the Linux Filesystem Hierarchy Standard. In particular, the FHS states that / srv is used for: “storing site-specific data — data that the system issues, for example, web server data and scripts, data stored on FTP servers, and control system repositories versions (appeared in FHS-2.3 in 2004). ”

This is our case, so we add everything we need to / srv, which is 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 easy: create a database, create a user, set up permissions. There is one thing to keep in mind during the initial installation of PostgreSQL — the postgresql-setup script, which is installed along with the postgresql-server package. This script helps you perform basic tasks associated with administering a cluster of databases, such as initializing a cluster or the upgrade process. To set up a new PostgreSQL instance on the RHEL system, we need to run the command:

 sudo /usr/bin/postgresql-setup -initdb 

After that, you can run PostgreSQL using systemd, create a database and set up a project in Django. Remember to restart PostgreSQL after making changes to the client authentication configuration file (usually pg_hba.conf) to configure the storage of the password for the application user. If you encounter other difficulties, make sure to change the IPv4 and IPv6 settings in the pg_hba.conf file.

 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 /var/lib/pgsql/data/pg_hba.conf file:

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

In the /srv/djangoapp/settings.py file:

 # 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 make sure everything works. After starting the development server, it is nice to create the admin user to test the database connection.

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

WSGI? Wai?


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

The goal of the Web Server Gateway Interface is to redirect requests from the web server to the Python web framework. WSGI is such a legacy of a terrible past, when CGI mechanisms were in use, and today WSGI is actually a standard, regardless of the web server used or the Python framework. But despite its wide distribution, there are still many nuances when working with these frameworks, and many choices. In this case, we will try to establish interaction between Gunicorn and Nginx through a socket.

Since both of these components are installed on the same server, try using a UNIX socket instead of a network socket. Since for communications, in any case, we need a socket, we will try to take another step and configure the activation of a socket for Gunicorn via systemd.

The process of creating socket activated services is quite simple. First, a unit-file is created that contains a ListenStream directive indicating the point at which the UNIX socket will be created, then a unit-file for the service, in which the directive will point to the unit-file of the socket. Then, in the unit-file of the service, all that remains is to call Gunicorn from the virtual environment and create a WSGI binding for the UNIX socket and the Django application.

Here are some examples of unit files that can be taken as a base. First we set up a socket.

 [Unit] Description=Gunicorn WSGI socket [Socket] ListenStream=/run/gunicorn.sock [Install] WantedBy=sockets.target 

Now you 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, simply create proxy configuration files and set up a directory for storing static content, if you use it. Nginx configuration files in RHEL are /etc/nginx/conf.d. You can copy the following example there into the /etc/nginx/conf.d/default.conf file, and start the service. Be sure to specify server_name according to your hostname.

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

Run a Gunicorn socket and Nginx using systemd, and you can begin testing.

Bad gateway error?


If you enter the address in the browser, you will most likely receive an error 502 Bad Gateway. It may be caused by improperly configured permissions for a UNIX socket, or due to more complex problems associated with access control in SELinux.

In the nginx error log you can find a line like 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 we test Gunicorn directly, we get an empty answer.

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

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

 sudo setenforce 0 

All this can cause criticism and bloody tears, but this is just a debugging of the prototype. Turn off the check only to make sure that the problem is exactly this, after which we will return everything back to its place.

By refreshing the page in the browser or by restarting our curl command, you can see the Django test page.

So, after making sure that everything works, and there are no more permissions problems, we re-enable SELinux.

 sudo setenforce 1 

There will be no talk about audit2allow and creating policies based on alerts using sepolgen, because there is currently no real Django application, there is no complete map of what Gunicorn may want to access, and what this access should be denied. Therefore, it is necessary to save SELinux’s work to protect the system, and at the same time allow the application to start and leave messages in the audit log so that you can then create a real policy based on them.

Specifying Permitted Domains (permissive domains)


Not all heard about the allowed domains in SELinux, but there is nothing new in them. Many even worked with them, themselves without knowing it. When a policy is created based on audit messages, the policy created is a permitted domain. Let's try to create a simple permissive policy.

To create a specific allowed domain for Gunicorn, you need some kind of policy, and also need to mark the appropriate files. In addition, the necessary tools to collect new policies.

 sudo yum install selinux-policy-devel 

The mechanism of allowed domains is a great tool for identifying problems, especially when it comes to a custom application or applications that ship without already created policies. In this case, the allowed domain policy for Gunicorn will be as simple as possible - we will declare the main type (gunicorn_t), declare the type we will use to mark several executable files (gunicorn_exec_t), and then configure the transition for the system to correctly mark the running processes . The last line sets the policy as allowed by default at the time of its 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 something else besides what our unknown demon is referring to.

 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 does not allow Nginx to write data to the UNIX socket used by Gunicorn. Usually, in such cases, they begin to change policies, but other tasks still lie ahead. You can also change the settings of a domain, turning it from a domain of restrictions into a domain of permissions. Now we will transfer httpd_t to the permission domain. This will provide Nginx with the necessary access, and we will be able to continue further debugging work.

 sudo semanage permissive -a httpd_t 

So, when you managed to save SELinux protection (in fact, you should not leave the project with SELinux in restriction mode) and the permission domains are loaded, you need to find out what you need to mark as gunicorn_exec_t in order for everything to work again as expected. Let's try to access the website to see new messages about access restrictions.

 sudo ausearch -m AVC -c gunicorn 

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

But in addition, a message appears 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 look at the status of the gunicorn service or run the ps command, then no running processes will appear. It seems that gunicorn is trying to access the Python interpreter in our virtualenv environment, possibly to run workers (workers). Therefore, we now mark these two executable files and check whether we can open our test page Django.

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

You will need to restart the gunicorn service so that you can select a new tag. You can restart it immediately or stop the service and let the socket start when the site is opened in the browser. Make sure the processes get the correct labels using ps.

 ps -efZ | grep gunicorn 

Do not forget to create a normal SELinux policy!

If you look at AVC messages now, then the last message contains permissive = 1 for everything related to the application, and permissive = 0 for the rest of the system. If you understand what kind of access is needed for a real application, you can quickly find the best way to solve such problems. But until then, it’s best to have the system protected and to get a understandable and usable audit of the Django project.

 sudo ausearch -m AVC 

Happened!


A working Django project with Nginx and Gunicorn WSGI front-end appeared. We configured Python 3 and PostgreSQL 10 from the RHEL 8 Beta repositories. Now you can go ahead and create (or simply deploy) Django applications or explore other available tools in RHEL 8 Beta to automate the configuration process, improve performance, or even containerize this configuration.

Source: https://habr.com/ru/post/447540/


All Articles