In this second practice exercise chapter, we will run a set of exercises to check the knowledge you’ve acquired throughout this book. In contrast with this book’s chapters, not all the steps will be specified; it’s left up to your discretion to perform the steps required to accomplish the necessary goals. It is recommended that you avoid checking back on the chapters for guidance and instead try to use your memory or the tools available in the system. This experience will be a key factor when you take on the official exams.
It is strongly advised that you time yourself during this exercise so that you know how long it took for you to complete it.
All the practical exercises in this chapter require a virtual machine (VM) running Red Hat Enterprise Linux 9 (RHEL 9) to be installed with the base installation. Additionally, new virtual drives will be required for storage operations.
The exercises assume that you have the following:
This is a list of general recommendations for any test, most of which are common sense, but it’s always interesting to keep them in mind:
Important note
By design, in the following exercise, commands, packages, and so on will not be highlighted. Remember what you’ve learned so far to detect the keywords to see what needs to be done.
Don’t jump into the solution too early; try to think about it and remember what was covered.
In this section, we’ll copy each item from the list of goals, and explain them while using proper syntax highlighting.
Here’s how to do this:
wget https://raw.githubusercontent.com/PacktPublishing/Red-Hat-Enterprise-Linux-RHEL-9-Administration/main/chapter-19-exercise2/users.txt
First, let’s examine the users.txt file with the following code:
cat users.txt user;x;1000;1000;myuser1;/home/user1; /bin/false john ;x ;1001 ;1001; John; /home/john ;/bin/false doe ;x ;1002 ;1002; Doe; /home/doe ; /bin/sh athena ;x ;1011 ;1011; Athena Jones; /home/ajones ; /bin/sh pilgrim ;x ;2012 ;2012; Scott Pilgrim; /home/spilgrim ; /bin/sh laverne; x ; 2020;2020; LaVerne;/home/LaVerne;/bin/bash
As described in the request, the fields in that file are username, placeholder, uid, gid, name, home, and shell. The placeholder is not asked to create a user as it’s usually the password so that we can work with the other data while ignoring that.
As we can also see, each field is separated by at least a ; symbol, but some have extra spaces before or after them. Since we also have surnames, we can’t just remove all spaces; we need to do this before and after the actual text we want.
We need to use cut with the ; field separator, but first, we need to read the file line by line.
We can achieve this with bash’s built-in read function:
cat users.txt|while read -r line; do echo ${line};done
Using this as a base, we can start building up everything we’re going to need to create users. Let’s start by working on the individual steps and then build up the full command line.
We have lots of lines, so for each one, we need to define the fields and remove end/start spaces:
NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1) NEWUID=$(echo ${line}|cut -d ";" -f 3) NEWGID=$(echo ${line}|cut -d ";" -f 4) NEWNAME=$(echo ${line}|cut -d ";" -f 5) NEWSHELL=$(echo ${line}|cut -d ";" -f 6)
In the preceding examples, we’re echoing each line and cutting the field specified with -f using the ; field delimiter. This allows us to select exactly the field containing the data we’re looking for. To make this easier, we can store each in a variable so that we can reuse that snippet of code and still have a clear understanding of what each script will be doing.
The preceding code will work, but it will fail with the spaces, so we need to extend them to just capture the actual text without the spaces. Let’s use xargs for this:
NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1|xargs) NEWUID=$(echo ${line}|cut -d ";" -f 3|xargs) NEWGID=$(echo ${line}|cut -d ";" -f 4|xargs) NEWNAME=$(echo ${line}|cut -d ";" -f 5|xargs) NEWHOME=$(echo ${line}|cut -d ";" -f 6|xargs) NEWSHELL=$(echo ${line}|cut -d ";" -f 7|xargs)
Groups need to be created; for that, we are going to use NEWUSERNAME as the group name and NEWGID as the group ID (GID):
groupadd -g "${NEWGID}" "${NEWUSERNAME}"
The next step is to build the command line for adding a user:
useradd --d "${NEWHOME}" --m --s "${NEWSHELL}" --u "${NEWUID}" --g "${NEWGID}" --c "${NEWNAME}" "${NEWUSERNAME}"
Now that everything’s ready, let’s build the solution:
cat users.txt| while read -r line ; do
NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1|xargs)
NEWUID=$(echo ${line}|cut -d ";" -f 3|xargs)
NEWGID=$(echo ${line}|cut -d ";" -f 4|xargs)
NEWNAME=$(echo ${line}|cut -d ";" -f 5|xargs)
NEWHOME=$(echo ${line}|cut -d ";" -f 6|xargs)
NEWSHELL=$(echo ${line}|cut -d ";" -f 7|xargs)
groupadd -g "${NEWGID}" "${NEWUSERNAME}"
useradd -d "${NEWHOME}" -m -s "${NEWSHELL}" -u "${NEWUID}" -g "${NEWGID}" -c "${NEWNAME}" "${NEWUSERNAME}"
done
Note
Depending on the system that you are using to run this code it could happen that the user IDs (UIDs) or GIDs already exist and users or groups are not created. In that case, you can change the number in the second or third column of the affected user or group in the users.txt file or just ignore the error and keep doing the following exercises.
In this case, we need to create a group called users and then we will loop over the users once the new group has been created, and then modify the users group to get the myusers group and add their own as secondary groups:
groupadd myusers
cat users.txt| while read -r line ; do
NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1|xargs)
usermod -g myusers -G "${NEWUSERNAME}" "${NEWUSERNAME}"
done
Here’s how to do this:
cat users.txt| while read -r line ; do
NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1|xargs)
NEWHOME=$(echo ${line}|cut -d ";" -f 6|xargs)
chown -R ${NEWUSERNAME}:myusers ${NEWHOME}/
done
Here’s how to do this:
dnf -y install httpd
firewall-cmd --add-service=http
firewall-cmd –-runtime-to-permanent
setsebool httpd_enable_homedirs on
cat users.txt| while read -r line ; do
NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1|xargs)
NEWUID=$(echo ${line}|cut -d ";" -f 3|xargs)
NEWGID=$(echo ${line}|cut -d ";" -f 4|xargs)
NEWNAME=$(echo ${line}|cut -d ";" -f 5|xargs)
NEWHOME=$(echo ${line}|cut -d ";" -f 6|xargs)
NEWSHELL=$(echo ${line}|cut -d ";" -f 7|xargs)
mkdir -p "${NEWHOME}"/public_html/
chown "${NEWUSERNAME}:${NEWUSERNAME}" "${NEWHOME}"/public_html/
chmod 755 "${NEWHOME}"/public_html/
chmod 711 "${NEWHOME}"
echo "Hello, my name is ${NEWNAME} and I'm a user of this system" > ${NEWHOME}/public_html/index.html
done
Finally, we’ll need to enable homedirs by editing /etc/httpd/conf.d/userdir.conf and modifying UserDir so that it becomes Userdir public_html:
cd /etc/httpd/conf.d
sed -i 's/UserDir disabled/UserDir enabled/' userdir.conf
sed -i 's/#UserDir public_html/UserDir public_html/' userdir.conf
systemctl restart httpd
curl http://localhost/~john/
Hello, my name is John and I'm a user of this system
This can be done in several ways, but since all the users are in the users group, we can add that group, like so:
echo "%users ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
First, let’s create keys for each user and add the keys to root:
mkdir /root/.ssh
touch /root/.ssh/authorized_keys
cat users.txt| while read -r line ; do
NEWHOME=$(echo ${line}|cut -d ";" -f 6|xargs)
mkdir -p ${NEWHOME}/.ssh/
ssh-keygen -N '' -f ${NEWHOME}/.ssh/id_rsa
cat ${NEWHOME}/.ssh/id_dsa.pub >> /root/.ssh/authorized_keys
done
Now, let’s copy the authorized keys for each user:
cat users.txt| while read -r line ; do
NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1|xargs)
NEWHOME=$(echo ${line}|cut -d ";" -f 6|xargs)
cp /root/.ssh/authorized_keys ${NEWHOME}/.ssh/
chown -R ${NEWUSERNAME}:myusers ${NEWHOME}/.ssh/
done
Validate that users can SSH just like any other user:
USERS=$(cat users.txt|cut -d ";" -f1|xargs)
for user in ${USERS};
do
for userloop in ${USERS};
do
su -c "ssh ${user}@localhost" ${userloop}
done
done
The preceding command should work for all the users because we copied authorized_keys, right? This isn’t the case as some users have their shell disabled. You will need to exit the SSH by writing the exit command after every user is running SSH with success.
Edit /etc/ssh/sshd_config and replace any value of PasswordAuthentication with no.
Then, restart sshd, like so:
systemctl restart sshd
From /dev/random, we can get random data but it’s binary, so it’s probably not valid if we want to use it for logging in later. We can use a hash function over the data we’ve received and use that as the password, as follows:
MYPASS=$(dd if=/dev/urandom count=1024 2>&1|md5sum|awk '{print $1}')
This will be the password, without the need for it to be encrypted.
With usermod, we can define a password from its encrypted seed, so we will be combining both.
Additionally, we’re told to store the generated password in users.text, so we will need to edit the file.
But there’s a problem: editing a specific field in the .txt file might not be easy, but we can just rewrite it completely:
cat users.txt| while read -r line ; do
MYPASS=$(dd if=/dev/random count=12>&1|md5sum|awk '{print $1}')
NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1|xargs)
NEWUID=$(echo ${line}|cut -d ";" -f 3|xargs)
NEWGID=$(echo ${line}|cut -d ";" -f 4|xargs)
NEWNAME=$(echo ${line}|cut -d ";" -f 5|xargs)
NEWHOME=$(echo ${line}|cut -d ";" -f 6|xargs)
NEWSHELL=$(echo ${line}|cut -d ";" -f 7|xargs)
echo –e "${NEWUSERNAME};${MYPASS};${NEWUID};${NEWGID};${NEWNAME};${NEWHOME};${NEWSHELL}" >> newusers.txt
echo ${MYPASS} | passwd ${NEWUSERNAME} --stdin
done
cp users.txt users.txt.bkp
cp newusers.txt users.txt
In this way, we’ve rewritten the users.txt file to a new file by adding all the fields we had and have overwritten users.txt with our new copy.
The last command in the loop reads the password from the variable and feeds it to the passwd file, which will encrypt and store it while reading it from stdin.
Here’s how to do this:
cat users.txt| while read -r line ; do
NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1|xargs)
NEWHOME=$(echo ${line}|cut -d ";" -f 6|xargs)
LETTERSINNAME=$(( $(echo ${NEWUSERNAME}|wc -m) - 1 ))
if [ "$((${LETTERSINNAME} % 2 ))" == "0" ]; then
echo "My name is multiple of 2" >> ${NEWHOME}/public_html/index.htm
fi
done
In this example, we repeat the same field calculation, but we add the wc command to get the number of characters and remove one to adjust it to the number of letters.
In the comparison, we evaluate the remainder when dividing by 2 so that when there’s no remainder, this means that our number of letters is a multiple of 2.
When we read “Python package”, we should think about PIP. PIP is not recommended to be used on systems directly as it might alter the system-provided Python libraries, and it’s better to use a virtual environment for it. Alternatively, you can use a container that will keep it isolated.
As described in Chapter 17, Managing Containers with Podman, Buildah, and Skopeo, the easiest way to do this is by creating a file that defines the container creation steps.
For containers, it will be also required to install the podman package and the container-tools modules if you don’t have them in your system.
As this file is a Python package, we require a container that already has Python in it; for example, https://catalog.redhat.com/software/containers/ubi9/python-39/61a61032bfd4a5234d59629e.
So, let’s create ContainerFile with the following contents (available at https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-RHEL-9-Administration/blob/main/chapter-19-exercise2/ContainerFile):
FROM registry.access.redhat.com/ubi9/python-39 MAINTAINER RHEL9 Student <[email protected]> LABEL name="yq image" maintainer="student _AT_ redhat.com" vendor="Risu" version="1.0.0" release="1" summary="yq execution container" description="Runs yq" ENV USER_NAME=risu USER_UID=10001 LC_ALL=en_US.utf8 RUN pip3 install --upgrade pip --no-cache-dir && pip3 install --upgrade yq --no-cache-dir USER 10001 VOLUME /data ENTRYPOINT ["/opt/app-root/bin/yq"] CMD ["-h"]
When combined with podman build -t yq -f ContainerFile, it will pull the ubi9 image with Python so that we can just run the pip3 install command to install yq, which will be then assigned as our entry point.
For example, if we define an invalid entry point (because we might not know where the program is installed), we can use podman run -it --entrypoint /bin/bash <podmanid>. We can get the podman ID by running podman images and checking the generation date for each of the available pods in our system.
The created container can be tested with podman run –it <podmanid>, where it will output information about what the yq command does.
Note that yq, as expressed in its repository at https://github.com/kislyuk/yq, requires that we have installed the jq command, but we left it out on purpose to demonstrate how to create a container.
Here’s how to do this:
cat users.txt| while read -r line ; do
NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1|xargs)
NEWHOME=$(echo ${line}|cut -d ";" -f 6|xargs)
LETTERSINNAME=$(( $(echo ${NEWUSERNAME}|wc -m) - 1 ))
if [ "$((${LETTERSINNAME} % 2 ))" != "0" ]; then
chage -M 30 ${NEWUSERNAME}
fi
done
Here, we’ve reused the loop from question 10, but inverted the conditional. Since there are no requirements regarding the kind of password aging we can use, we just need to define the maximum number of days before a password change is required to be 30 days.
First, we need to make sure that logrotate is installed. We can do this by running the following command:
dnf -y install logrotate
Once installed, edit the /etc/logrotate.conf file so that it contains the following:
rotate 30 daily compress dateext
We need to ensure that no other period is defined (monthly, weekly, and so on).
This has a trick to it: some programs will log to the journal, while some of them will log to *.log files.
The date for today can be obtained with +%Y-%m-%d, as follows. This format, which uses the year-month-day format, is commonly used in program logs:
grep "$(date '+%Y-%m-%d')" -Ri /var/log/*.log|grep -i error > /root/errors.log
journalctl --since "$(date '+%Y-%m-%d')" >> /root/errors.log
By doing this, we combine both outputs. We could, of course, try to sort the entries by date so that they correlate, but bear in mind that the first grep command does a recursive search, so the filename is being prepended, making it harder to sort.
Usually, the system libraries contain the lib substring in them, so the update should be a matter of running the following command:
dnf upgrade *lib*
As it will ask for confirmation, review the listed packages to make sure that no errors occurred.
This is a tricky but useful knowledge check.
First, let’s make sure that the rpm package is available by running the following command:
dnf download rpm
Verify that the file exists with the following command:
ls –l rpm*.rpm
Check the file to make sure we have a way to go back in case we break it beyond repair:
rpm -qip rpm*.rpm
Now, let’s look at the destructive action that will help us validate we are solving the issue:
rm -fv /usr/bin/rpm
From here, it’s like look ma, no hands... no RPM is available to install the rpm*.rpm package, but we still need to install it to fix the issue.
rpm packages are compressed cpio archives, so what we can do is use the following command:
rpm2cpio rpm*.rpm |cpio –idv
This will extract the compressed rpm contents (without the need to run a script).
Move the uncompressed rpm file back into /usr/bin, like so:
mv usr/bin/rpm /usr/bin/rpm
Verify the installation and operation of rpm with the following command:
rpm –V rpm
It will complain, saying at least that the date has changed. However, it may have also updated the sizes and md5sum if the downloaded file was newer.
Move the system to a sane state by reinstalling the rpm package, like so:
rpm -i rpm*.rpm
This will make the system complain because the package was already installed (it will state that it will overwrite rpm, rpm2archive, rpm2cpio, rpmdb, rpmkeys, and more).
If the rpm version differs, we can just upgrade it with the following command:
rpm -Uvh rpm*.rpm
Then, we can verify this with the following command:
rpm –V rpm
Nothing should be reported as changed regarding what the database contains. If we get a timestamp modification or if we cannot upgrade, we can run the installation with the --force argument to tell rpm that it’s OK to continue and overwrite the files.
Alternatively, once the rpm binary has been restored with cpio, we can use the following command:
dnf –y reinstall rpm
Another approach for this could have been to scp the rpm binary from a similar system or to use rescue media.
We have no way of making this a default, but we can combine a cron job to do so.
Execute -e crontab as root to edit the root’s crontab and set up a job that runs every minute, like so:
*/1 * * * * pgrep -u doe |xargs renice +5 */1 * * * * pgrep -u john|xargs renice -5
This will use pgrep for all process IDs (PIDs) for john and doe and feed them via xargs to the renice process.
Alternatively, we could use something like the following:
renice +5 $(pgrep -u doe)
This can be used as an alternative to the xargs command.
tuned is a system daemon we can install to automatically apply some well-known parameters to our system, which will become the base for our specific optimizations later. Here’s how we can install it:
dnf -y install tuned
systemctl enable tuned
systemctl start tuned
tuned-adm profile throughput-performance
Using nmcli, check the current system IP’s address, like so:
nmcli con show
This should be the output:
Figure 19.1 – Output of nmcli con show
With this, we can find which system interface is being used and connected. Let’s say it’s enp1s0, which is connected on the connection named enp1s0.
Let’s use nmcli con show "enp1s0"|grep address to find the current addresses.
If our address is—for example—10.0.0.6, we can use the following code:
nmcli con mod "Wired Connection" ipv4.addresses 10.0.0.7
nmcli con mod "Wired Connection" ipv6.addresses 2001:db8:0:1::c000:207
Verify this with the following command:
nmcli con show "Wired Connection"|grep address
Edit the /etc/profile.d/mysystempath.sh file and place the following contents in it:
export PATH=${PATH}:/opt/mysystem/bin
To validate this, add the +x attribute to the file and create a folder with the following commands:
chmod +x /etc/profile.d/mysystempath.sh
mkdir -p /opt/mysystem/bin
Relogging with the user should show the new path when executing the following command:
echo ${PATH}
This is a tricky question. In this book, we’ve explained how to query zones and how to change the default one, and even shown screenshots of cockpit for managing the firewall, so now that you’re an experienced user, this shouldn’t be hard.
The first thing you need to do when you don’t know how to do something is check the manual page. Here’s the command to run in this instance:
man firewall-cmd
This doesn’t show a lot of interesting information. However, toward the end of the man pages, there’s a section called SEE ALSO, where we can find out about firewalld.zones(5). This means that we can check Section 5 of the manual for firewalld.zones.
We don’t usually specify the section as there might not be a lot of duplicates, so we can just run the following command:
man firewalld.zones
This instructs us to check the default ones in /usr/lib/firewalld/zones and /etc/firewalld/zones, so let’s do that:
cp /usr/lib/firewalld/zones/public.xml /etc/firewalld/zones/dazone.xml
Now, let’s edit the new copied file, called /etc/firewalld/zones/dazone.xml, and change its name from Public to dazone. Then, we need to reload the firewall:
firewall-cmd --reload
Let’s validate that the new zone is there with the following command:
firewall-cmd --get-zones
Let’s make it the default zone:
firewall-cmd --set-default-zone=dazone
Now, add the default interface (ens3):
firewall-cmd --add-interface=ens3 --zone=dazone
It will fail. This is expected since ens3 has already been assigned to a zone (public). So, let’s use the following commands:
firewall-cmd --remove-interface=ens3 --zone=public
firewall-cmd --add-interface=ens3 --zone=dazone
As you can see, even without prior knowledge about creating new zones, we’ve been able to use our system knowledge about finding information to accomplish this goal.
If we don’t remember the syntax for a repository, we can use one of the examples available on the system. To do this, go to /etc/yum.repos.d/, list the available files and pick one to create a myserver.repo file with the following contents:
[myserver] name=My server repository baseurl=https://myserver.com/repo/ enabled=1 gpgcheck=1 gpgkey=https://myserver.com/mygpg.key
How do we skip it if it’s unavailable? Let’s check the man page for yum. Again, not much information is provided here, but in the SEE ALSO section, man dnf.conf is specified. This lists a Boolean that might help us, so let’s add this to our repository file:
skip_if_unavailable=1
With that, we’ve completed our objectives.
3.145.164.47