Python socket bind in a rootless container/world

My original task was to create a Python script to send ICMP echo-request (aka. ping) messages to some hosts and then do some basic reporting. This is a simple task right? Well, not really, at least, if you care about security. To be able to ping a host, we need to open a socket, which cannot be done by a regular user, either on a Windows nor on a Linux machine. Now I will concentrate on Linux systems.

The internet is full of with fantastic ideas, but sometimes it is maybe a better idea to think a little before you copy something from stackoverflow. In a bunch of topics, they advice you to run your script as root, because what can go wrong, right? Or disable SELinux, what could go wrong? Well, say no to these! (This can apply to other topics too.. don’t always accept the first solution, think, think, think.)

So my idea is to create a simple script in Python. I wanted to use a full Python and async implementation, so I choose the aioping (https://github.com/M-o-a-T/aioping) library. They state that the library can only be used, if you run it as root. This is not really acceptable, so let’s see, what can be done to solve this issue..

Let’s see, what’s going on under the hood. In the core of the library, it tries to open a raw socket:

import socket
socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.getprotobyname("icmp"))
socket.bind(('', 0))

If you try to run just this, as a non-root user, you will get the following error message:

bash-4.2$ python3.6
Python 3.6.7 (default, Dec  5 2018, 15:02:05)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-36)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import socket
>>> socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.getprotobyname("icmp"))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib64/python3.6/socket.py", line 144, in __init__
    _socket.socket.__init__(self, family, type, proto, fileno)
PermissionError: [Errno 1] Operation not permitted

Well, nobody lied, it really need to be ran as ‘root’ (well.. yeah no, maybe). But that sucks, so what options do we have here? We can use the magic of setuid/setgid or from kernel version 2.2, we can use Linux kernel capabilities. Next we will check these.

SETUID/SETGID

setuid and setgid (short for “set user ID” and “set group ID”)[1] are Unix access rights flags that allow users to run an executable with the permissions of the executable’s owner or group respectively and to change behaviour in directories.

https://en.wikipedia.org/wiki/Setuid

So we can just simply set the owner of the script to be “root” and then enable the setuid bit and bamm we are done. What an awful idea! Security folks probably started to heavily breath now! But they are right, don’t use that.

Linux kernel capabilities

For the purpose of performing permission checks, traditional UNIX implementations distinguish two categories of processes: privileged processes (whose effective user ID is 0, referred to as superuser or root), and unprivileged processes (whose effective UID is nonzero). Privileged processes bypass all kernel permission checks, while unprivileged processes are subject to full permission checking based on the process’s credentials (usually: effective UID, effective GID, and supplementary group list).

Starting with kernel 2.2, Linux divides the privileges traditionally associated with superuser into distinct units, known as capabilities, which can be independently enabled and disabled. Capabilities are a per-thread attribute.

http://man7.org/linux/man-pages/man7/capabilities.7.htm

This sounds good, let’s see what capabilities are out there and which can be interesting for us! You can find all the capabilities and details in the manual, here I collected the most interesting for this topic:

CAP_NET_ADMIN

Perform various network-related operations:
– interface configuration;
– administration of IP firewall, masquerading, and accounting;
– modify routing tables;
– bind to any address for transparent proxying;
– set type-of-service (TOS)
– clear driver statistics;
– set promiscuous mode;
– enabling multicasting;
– use setsockopt(2) to set the following socket options:
– SO_DEBUG, SO_MARK, SO_PRIORITY (for a priority outside the range 0 to 6), SO_RCVBUFFORCE, and SO_SNDBUFFORCE.

http://man7.org/linux/man-pages/man7/capabilities.7.htm

CAP_NET_BIND_SERVICE

Bind a socket to Internet domain privileged ports (portnumbers less than 1024).

http://man7.org/linux/man-pages/man7/capabilities.7.htm

CAP_NET_RAW

Use RAW and PACKET sockets;
bind to any address for transparent proxying.

http://man7.org/linux/man-pages/man7/capabilities.7.htm

Okay, so now we know, that in the Linux world, there’s no more only root or non-root, we can use these capabilities, so we can grant permissions more granular. But if we follow the “principle of least privilege”, we can ask: do we need to allow all these capabilities? Well no, not really.

If we add CAP_NET_ADMIN capability to a process, it will be able to do various things to our networking stack, like interface configuration, change of firewall settings and so on. We clearly don’t want this, and if we check the list of permissions, we don’t need them.

On the other hand, CAP_NET_BIND_SERVICE allows the process to bind on a privileges port (any port under 1024). We also don’t need this, so we can just exclude this capability as well.

The last thing is CAP_NET_RAW. This allows exactly what we need: allows the process to use RAW sockets and to bind to any IP address. Perfect, just what we need.

Linux capability sets

Now we know, that we need the CAP_NET_RAW capability. But how should we add this to our process? We can me these permitted, inheritable and effective (and many more, but these are the most important now).

Each thread has the following capability sets containing zero or more of the above capabilities:

Permitted
This is a limiting superset for the effective capabilities that the thread may assume. It is also a limiting superset for the capabilities that may be added to the inheritable set by a thread that does not have the CAP_SETPCAP capability in its effective set.

Inheritable
This is a set of capabilities preserved across an execve(2). Inheritable capabilities remain inheritable when executing any program, and inheritable capabilities are added to the permitted set when executing a program that has the corresponding bits set in the file inheritable set.

Effective
This is the set of capabilities used by the kernel to perform permission checks for the thread.

http://man7.org/linux/man-pages/man7/capabilities.7.html

Add Linux capabilities to a process

Adding Linux capabilities to a process is fairly easy with the use of the setcap command. As this is going to be a Python script, we need to add it to the Python interpreter. Be careful, if you add this to the global interpreter of your system, all scripts will have these capabilities! To be on the safe side, it’s worth to create a virtualenv, and only pimp that interpreter!

(+ means, that we add the capability, e=effective, p=permitted)

setcap cap_net_raw+ep /path/to/your/interpreter

We can check the capabilities with the getcap command (this is an example of the ping command):

getcap /usr/bin/ping
/usr/bin/ping = cap_net_admin,cap_net_raw+p

And that’s it. Now you should be able to run your script without running it as root. It has now more permissions, than a regular process, but still not running as root!

Running in a rootless Docker container

Okay, now we can run that thing without root. So now let’s put it in a container! If we run a Docker container, by default, it drops a bunch of capabilities, as we wont need most of them. In this case, we will only need the cap_net_raw capability, so we can drop all others.

To read more about capabilities: https://www.redhat.com/en/blog/secure-your-containers-one-weird-trick

Example command to run the container with only the net_raw capability:

docker run -it --cap-drop=all --cap-add=net_raw <image>

With the –cap=drop=all command, we can drop all the capabilities. With the –cap-add=<name>, we can add capabilities.

That’s it.

DNS based ad/tracking blocking setup with NextDNS (at home)

In the past I played with AdBlock, pi-hole, now with NextDNS. In this article, I will show you the major differences, then at the end I will show a step-by-step guide for installing and setting up dnsmasq+netxdns client (so you can skip the first part, if you just need the install guide 🙂 ).

Why DNS based ad and tracking blocking?

  • Using browser plugins might get pain in the ass, as sites can detect if you use it and make you disable it.
  • Sites can load slower, as your browser needs to deal with the not needed ads as well
  • Your internet usage is higher when you load additional ads
  • Companies can track, what sites you visit and show ads based on that (Facebook, Google, etc…)

What solutions can you use?

There are several options with which you can do DNS based ad-blocking:

  • Add lists to your hosts file (client or server)
  • Use an on-premis service like pi-hole
  • Use a cloud service like NextDNS
  • Use a hybrid solution: on-premis server + cloud provider

Add lists to your hosts file (client or server)

This is the most simple solution, there are bunch of lists on the internet, basically these are key-value pairs. If the name is in the list, then your host will resolve it to it’s local address, so your browser will not be able to reach that resource.

ProsCons
You can set it up without a server in your networkHard to keep it updated, have to mess with it
Provider independentWill only do ad blocking on the local machine
Works on all platforms (Windows, Linux, Mac)

Set up an on-prem system (like pi-hole)

When you have multiple hosts in your home (and probably you have: PC, Laptop, Smartphones, TVs, Home automation tools, etc..), you might want to have one centralized server for your DNS filtering. Pi-hole is a great solution for this, it was originally created for Raspberry Pis, which are small boards, on which you can run Linux (like a small home server). You can easily install it on bare-metal or via Docker.

Once you installed it, you can load any lists, what you can find on the internet.

ProsCons
You have total control over it, as it’s running on your deviceYou have to know, how to manage Linux
It’s free to useYou have to buy the hardware
You can easily manage pi-hole via it’s admin interfaceYou have to keep it running, update it, manage it, etc..
Most security/privacy, as it’s running on your deviceYou can only reach it from outside of your home via a VPN

For a security expert, this would be a great solution, as you have total control in your hands. However it gets messy, when you want to make your phone/tablet to use it, when your are out from home, as you have to use a VPN to stay secure.

Using a cloud provider like NextDNS

Using a cloud provider can make your life much easier, you just need to set their DNS servers and that’s it. NextDNS even has some client tools (which.. are really not necessary, but convenient for the masses).

ProsCons
You don’t need any technical knowledgeYou loose privacy at one point, as this service is managed by “someone else”
You can easily manage it via their web-adminYou might need to pay for it
You can also set it up on your smartphone, tablet, pcYou can’t really set it up on your no-to-smart smart devices, like TVs, Home automation tools

Using a hybrid solution with an on-primes server and cloud provider

If you have a little Linux knowledge and you have a home server/Raspberry Pi or anything, which is capable running Linux, you can easily set up a local server, then you can advertise it’s local IP address via DHCP from your home router.

Now I will show the steps needed for setting up dnsmasq and netxdns cli on a Linux box.

  • dnsmasq is a dns server, we will use it for caching, as once we have the resolved DNS address, then we don’t want to go to the cloud provider anymore (so we maybe need to pay less 😉 )
  • nextdns cli is a simple cli app created by NextDNS, it can act as a DoH (DNS over HTTPs) proxy, so your DNS traffic will be encrypted towards the internet but will be regular non-encrypted on the local network.

Setting up NextDNS cli

NextDNS cli is an open-source project, it’s managed by NextDNS. The repository can be found on github: https://github.com/nextdns/nextdns

The installation is easy, this will add the NextDNS repo to your repo manager (yum, apt, etc), the list of supported systems is reachable here: https://github.com/nextdns/nextdns/wiki

sh -c 'sh -c "$(curl -sL https://nextdns.io/install)"' 

The installation is a next-next-finish script, it will ask for basic things like you NextDNS ID. It will create a basic configuration, so you can edit it later. It will also make the nextdns client to start on startup of the system.

After installing it, we need to configure it to listen on the local address of the server (localhost/127.0.0.1). To do this, the setup-router must be set to false! (It’s not for this use anyways..)

/etc/nextdns.conf

log-queries true
report-client-info true
detect-captive-portals false
hardened-privacy true
timeout 5s
setup-router false
auto-activate false
listen 127.0.0.1:53
config <REMOVED>
bogus-priv true
use-hosts false

After changing the settings, restart the service

service nextdns restart

And check the logs, if everything is fine

nextdns log

Ad this point, nextdns is listen on 127.0.0.1:53, so non of your clients should be able to reach it. But this is fine. To check, you can use netstat:

netstat -dnlp | grep nextdns
tcp 0 0 127.0.0.1:53 0.0.0.0:* LISTEN 3762/nextdns
udp 0 0 127.0.0.1:53 0.0.0.0:* 3762/nextdns

Setting up dnsmasq

Dnsmasq is part of most of the basic repositories, so just try to install it (if it’s not found, then you need to find and add the proper repo)

yum install dnsmasq

Add the following to the dnsmasq configuration (/etc/dnsmasq.conf)

domain=local.local
resolv-file=/etc/resolv.dnsmasq
min-port=4096
cache-size=10000
listen-address=<IP of your server>
bind-interfaces
port=53

In the resolv-file, we will tell dnsmasq, to use our nextdns client as the upstream DNS server. The listen-address must be the address of your server (like 192.168.X.X or 10.X.X.X etc)

Add the server to the resolve file (/etc/resolv.dnsmasq)

nameserver 127.0.0.1

This means, that dnsmasq will be the only, who can directly acccess the nextdns client.

Restart dnsmasq and make it start on system startup

service dnsmasq restart
systemctl enable dnsmasq

To verify, use netstat again:

netstat -dnlp | grep 53
tcp 0 0 127.0.0.1:53   0.0.0.0:*           LISTEN      3762/nextdns
tcp 0 0 <IP>:53        0.0.0.0:*           LISTEN      3589/dnsmasq
udp 0 0 127.0.0.1:53   0.0.0.0:*                       3762/nextdns
udp 0 0 <IP>:53        0.0.0.0:*                       3589/dnsmasq

Enable your new service on all your hosts in the network

Now your server is ready to take requests. You need to add the server’s IP address to the DHCP server settings of your home router, so it will tell the clients to use dnsmasq.

Setting up this can be different among different brands, so please contact your router’s manual if you need help.

Is it working?

To check, if dnsmasq is doing caching properly, you can execute the following on your server:

 pkill -USR1 dnsmasq

This will send a special signal to dnsmasq, so it will just put some statistics into the logs. If everything is fine, you should see something like this in /var/log/messages:

cat /var/log/messages | grep dnsmasq
[...]
dnsmasq[3589]: queries forwarded 9519, queries answered locally 1220
[...]

This shows, that around 10k queries were sent to nextdns client and around 1.3k queries were answered locally from the cache.

Conclusion

At this point, all your home systems are using NextDNS via your home server. Yes, even your coffee maker, so now more ads on that. But what’s with your Smartphone/Laptop/Tablet when you are not at home?

So now your queries are encrypted on the internet, and your ads are blocked. (You just gave up a little on privacy, as now NextDNS is the one, who knows everything 😉 )

Ansible: tags

Tags in Ansible are used for filtering tasks based on a list of strings – tags. This feature isn’t complicated until you just want to run or skip all of the tasks with a specific tag.

Can I run tasks based on multiple tags? What happens with my dependencies if I run my playbook with tags? Are imported/included tasks and roles affected?

For first-timers, tags and “when” conditions could look the same but they are not. While “when” keyword can contain any condition with any complexity than tags just “is the requested tag in the tags list or not” filter.

There’s one more difference when you check the output of a playbook run: the “when” keyword is evaluated at run time and based on the result, the task will be run or not. Tags evaluate before the first play starts. If a task’s tags and the provided tag list have no intersection then the task will be ignored from the current execution completely. If a task is missing due to tagging, then you won’t see it in the playbook’s output but in case of the “when” keyword you’ll see the task as “skipped“.

All of the coming sections will show, how to manipulate the execution of a playbook using the –tags and –skip-tags command-line options.

Note
If a task is ignored due to tagging, then variables used in the task don’t need to be initialized.

Reserved tags and keywords

There are 2 reserved tags and 3 keywords, which are special and predefined. The following section will describe these specialties.

Special keywords

We can use special keywords as command line parameters for –tags and –skip-tags in conjunction with custom or reserved tags to modify the list of executed tasks.

  • tagged: all of the tagged tasks
  • untagged: all tasks without any tags
  • all: don’t care, just run all of it. This is the default.

Reserved tags

Tasks, with “always” tag will be always executed except:

  • always” is explicitly skipped
  • the task has another tag which is explicitly skipped

Tasks, with “never” tag will never be executed except:

  • never explicitly in the “–tags” list
  • the task has another tag which is explicitly requested

When we are speaking about explicitly skipping or requesting these reserved tags, they work just like any other regular tag. Regular tag, I mean the ones that you create. Just don’t forget, if you specify only one of the reserved tags, you might get a very short task list.

For running all tasks and tasks with tag “never“, it’s not enough to specify “–tags never” because you would only override the default tag (“all“), so in this case, you will run only “never” tagged tasks. Instead, you should use “–tags all,never” to run every task even the “never” tagged ones.

Demo playbook

You can download the playbook and its roles from our GitHub repository. From the tagging perspective, the functionality of the modules isn’t irrelevant thus all the tasks are just a “command” module executing echo. I used this instead of the “debug” module, as “command” always shows as changed so it triggers its handlers.

For demonstration purposes, I added the list of explicitly defined tags to the end of the name of the tasks, for example: “Include config tasks (configure)”.

All of the below commands are executed from the git root directory with the latest Ansible version (at the time of writing, it’s Ansible 2.8.).

Good2Know
If you define “#!/usr/bin/env ansible-playbook” or “#!/usr/bin/env ansible” commands as the interpreter of the playbook and make them executable, just as you would define for a bash script, then you can just run the playbook like “./playbook.yml”.

List all tags

This command can be handy if you want to know what tags are used across the whole playbook.

$ ./playbook.yml --list-tags

playbook: ./playbook.yml

  play #1 (localhost): Deploy Application 1	TAGS: [first_play]
      TASK TAGS: [always, application1, configure, firewall, first_play, install, logging, monitoring, repository, role_tag_from_play]

  play #2 (localhost): Deploy Application 2	TAGS: [second_play]
      TASK TAGS: [application2, configure, firewall, install, logging, monitoring, repository, second_play]

  play #3 (localhost): An untagged task	TAGS: []
      TASK TAGS: []

List all tasks

This command will show all the available tasks and their tags, however, the output might not be full if you use any of the “include” modules. (Details about the include modules are coming later.)

Scopes and inheritances

Although you can add tags to tasks, blocks, roles or plays, they will behave slightly differently. In the next section, I will show these differences. The irrelevant parts of the output will be omitted, so we can focus on the interesting parts.

Tasks

This is the most commonly used, all of the tasks with the specified tag will be ran, nothing special. Import and include tasks are exceptions and we’ll deal with them later.

Blocks

Blocks are used for grouping tasks together and to apply some parameters to all of them in one place. Just like in the main task file of the DEMO application roles, see below. We apply the same tag to all of the tasks, even the included and imported ones.

Ansible code snippet:

- block:
  - name: Include install tasks (install)
    include_tasks: install.yml
    tags:
      - install

  - name: Just for debugging (never, debug)
    command: echo
    tags:
      - never
      - debug

  - name: Include config tasks (configure)
    include_tasks: configure.yml
    tags:
      - configure

  tags:
    - application1

Output of the playbook:

$ ./playbook.yml

PLAY [Deploy Application 1] ******************************************************************************************

...SNIPPET...

repository : Configure repository (configure)] *****************************************************************
changed: [localhost] => (item=repo_app1)

TASK [application1 : Include install tasks (install)] ****************************************************************
included: /home/segal/projects/inframates/blog/2019/10-October/ansible_tags/roles/application1/tasks/install.yml for localhost

TASK [application1 : Install Application1 (install)] *****************************************************************
changed: [localhost]

TASK [application1 : Include config tasks (configure)] ***************************************************************
included: /home/segal/projects/inframates/blog/2019/10-October/ansible_tags/roles/application1/tasks/configure.yml for localhost

TASK [application1 : Configure Application1 (configure)] *************************************************************
changed: [localhost]

TASK [application1 : Configure Application1 without tag ()] **********************************************************
changed: [localhost]

TASK [firewall : Include config tasks ()] ****************************************************************************
included: /home/segal/projects/inframates/blog/2019/10-October/ansible_tags/roles/firewall/tasks/configure.yml for localhost

...SNIPPET...

TASK [repository : Configure repository (configure)] *****************************************************************
changed: [localhost] => (item=repo_app2)

TASK [application2 : Install Application2 (install)] *****************************************************************
changed: [localhost]

TASK [application2 : Configure Application2 ()] **********************************************************************
changed: [localhost]

TASK [firewall : Include config tasks ()] ****************************************************************************
included: /home/segal/projects/inframates/blog/2019/10-October/ansible_tags/roles/firewall/tasks/configure.yml for localhost

...SNIPPET...

With blocks, you can easily tag multiple tasks in one place as you can see above. If you have to add some new tasks later, those also will be tagged. It’s an easy way to make sure that all of your tasks are tagged and not forgotten.

Imports vs Includes

It doesn’t matter what you import or include, it can be a task, role or even a playbook, however, there are key differences between import and include.

Imports

Imports are similar to blocks. If you import something, then it will receive all the properties of the import task. The import itself won’t show up in the output of the playbooks. For example, if you tag an import task, then you also tag all the imported entities:

  • Tag an import_tasks task: you’ll tag all of the tasks from the imported file.
  • Tag an import_role task: you’ll tag every task in the imported role.
  • Tag an import_playbook: you’ll tag every role in the imported playbook.

Don’t forget: it’s recursive!

Includes

Tagging includes works just like tagging any regular task. Properties will not propagate to the included entities. Includes will show up in the output of the playbooks, just like other tasks.

  • Tag an include_tasks: you won’t tag any of the tasks from the included file.
  • Tag an include_role: you won’t tag any of the tasks from the included role.
  • Tag an include_playbook: you won’t tag any of the roles from the included playbook.

Be aware, if you don’t tag an include just the included tasks, then the included tasks won’t be executed, because they won’t be included.

Includes vs imports example

Let’s see the output of the next run:

$ ./playbook.yml --tags configure

PLAY [Deploy Application 1]

...SNIPPET...

TASK [repository : Configure repository (configure)] *****************************************************************
changed: [localhost] => (item=repo_app1)

TASK [application1 : Include config tasks (configure)] ***************************************************************
included: /home/segal/projects/inframates/blog/2019/10-October/ansible_tags/roles/application1/tasks/configure.yml for localhost

TASK [application1 : Configure Application1 (configure)] *************************************************************
changed: [localhost]

TASK [monitoring : Install monitoring packages (install)] ************************************************************
changed: [localhost]


...SNIPPET...

PLAY [Deploy Application 2]

...SNIPPET...

TASK [repository : Configure repository (configure)] *****************************************************************
changed: [localhost] => (item=repo_app2)

TASK [application2 : Configure Application2 ()] **********************************************************************
changed: [localhost]

TASK [monitoring : Configure monitoring (configure)] *****************************************************************
changed: [localhost]

...SNIPPET...

Task “Configure Application2” in “application2” role has no tags, but as you can see, it also executed, because of the tag provided to the import but on the other side none of the firewall tasks ran. “Drop firewall config” task has the “configure” tag but its include task was filtered due to the missing tag thus this task has never run.

typeprocessingDo you see it as a task in
the output of the playbook?
tag children
includedynamicruntimeyesno
importstaticpre-processednoyes

Roles

Roles listed under a play’s “roles” keyword works like “import_role” tasks.

One thing can be confusing though, if you add tags to a role listed in a play just like the “role_tag_from_play” tag, then it’s not a filter. You just tag all of the tasks in the role, NOT filtering those.

$ ./playbook.yml --tags role_tag_from_play

PLAY [Deploy Application 1] ******************************************************************************************

TASK [logging : Install logging packages (install)] ******************************************************************
changed: [localhost]

TASK [logging : Configure logging (configure)] ***********************************************************************
changed: [localhost]

TASK [monitoring : Install monitoring packages (install)] ************************************************************
changed: [localhost]

TASK [monitoring : Configure monitoring (configure)] *****************************************************************
changed: [localhost]

PLAY [Deploy Application 2] ******************************************************************************************

PLAY [An untagged task] **********************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************
ok: [localhost]

PLAY RECAP ***********************************************************************************************************
localhost                  : ok=5    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  

We tagged only the logging role in the first play. As you see all of the logging tasks executed plus the monitoring ones because those are tagged with “always“.

Plays

Tagging a play will tag all the roles and tasks in it. If you scroll up to the output to the task list section, you can see, that a play tag was added to all of the tasks within the play.

You can try to run the following scenario if you want to check the result:

$ ./playbook.yml --tags first_play

Dependencies

Under meta, you can define some roles required to run before your role. For more details please check the official docs.

Tagging a dependency role works like tagging an “import_role” task. If you tag one, the tag applies to all of the tasks in the role. It can lead to some issues if you run your role with some tags and the dependencies are not running because of the missing tag(s) from the dependency declaration.

$ ./playbook.yml --tags install

PLAY [Deploy Application 1] ******************************************************************************************

TASK [logging : Install logging packages (install)] ******************************************************************
changed: [localhost]

TASK [repository : Configure repository (configure)] *****************************************************************
changed: [localhost] => (item=repo_app1)

TASK [application1 : Include install tasks (install)] ****************************************************************
included: /home/segal/projects/inframates/blog/2019/10-October/ansible_tags/roles/application1/tasks/install.yml for localhost

TASK [application1 : Install Application1 (install)] *****************************************************************
changed: [localhost]

TASK [monitoring : Install monitoring packages (install)] ************************************************************
changed: [localhost]

TASK [monitoring : Configure monitoring (configure)] *****************************************************************
changed: [localhost]

PLAY [Deploy Application 2] ******************************************************************************************

TASK [logging : Install logging packages (install)] ******************************************************************
changed: [localhost]

TASK [application2 : Install Application2 (install)] *****************************************************************
changed: [localhost]

TASK [monitoring : Install monitoring packages (install)] ************************************************************
changed: [localhost]

PLAY [An untagged task] **********************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************
ok: [localhost]

PLAY RECAP ***********************************************************************************************************
localhost                  : ok=10   changed=8    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Repository only has a task with “configure” tag so if we don’t tag the role in the dependency list then as you can see in “Deploy Application 2” play, then the role won’t execute.

Handlers

Tagging has no effect on handlers. You can tag handlers, it won’t cause any syntax error, it just makes no sense.

$ ./playbook.yml --tags first_play --skip-tags invalid_tag

PLAY [Deploy Application 1] ******************************************************************************************

...SNIPPET...

TASK [monitoring : Configure monitoring (configure)] *****************************************************************
changed: [localhost]

RUNNING HANDLER [application1 : Restart Application1 (invalid_tag)] **************************************************
changed: [localhost]

RUNNING HANDLER [firewall : Reload firewall] *************************************************************************
changed: [localhost]

PLAY [Deploy Application 2] ******************************************************************************************

PLAY [An untagged task] **********************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************
ok: [localhost]

PLAY RECAP ***********************************************************************************************************
localhost                  : ok=17   changed=12   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

The “–skip-tags” option will be described in detail in the next section. Now it’s enough to know that with this option, you can ignore tasks. We ignored the handler’s tag, however, it was executed.

Skip tags

Now we know, how tasks got their tags, let’s see what happens if we want to skip some of them.

If you use “–skip-tags” alone, without “–tags” then it pretty the same as –tags except those tasks will be ignored witch match one of the provided tags.

–tags and –skip-tags at the same time

In this case, Ansible searches for those tasks, which has tags provided in the “–tags” option but they don’t have tags provided in the “–skip-tags”.

Use the same tag in tags and skip-tags option

If “–tags” is a subset of “–skip-tags”, then no task will be ran, except the ones which tagged as “always” and have no tags from the “skip-tags” list.

$ ./playbook.yml --tags configure --skip-tags configure

PLAY [Deploy Application 1] ******************************************************************************************

TASK [monitoring : Install monitoring packages (install)] ************************************************************
changed: [localhost]

PLAY [Deploy Application 2] ******************************************************************************************

PLAY [An untagged task] **********************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************
ok: [localhost]

PLAY RECAP ***********************************************************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

“Install monitoring packages” task in the “monitoring” role is tagged as “install” explicitly and “always” in the first play at play level. This is the only task that is not tagged with “configure”, but tagged as “always” thus it executed. “Configure monitoring” is also tagged with “always“, but it’s also tagged with “configure” so it’s left out from the task execution list.

Combine –tags and –skip-tags

What if you want to run all of the tasks, which have multiple tags at the same time. For example, we want to reconfigure only application1. In this case, we need to run tasks that have “application1” and “configure” tags.

Unfortunately, Ansible doesn’t have this feature. Tags are in an OR relation and there is no way right now to change this. But we have “–skip-tags” and with an ugly little hack, we can achieve this behavior.

What if we provided all of the unwanted tags in “–skip-tags” and add the required ones into “–tags”? Well, we got a long, ugly Ansible command which runs only those tasks that we need. I hope you still remember the “–list-tags” option that shows us all of the tags. Taking advantage of the tag inheritance, we can save lots of typing.

Reconfigure full stack of application1 but not application2

$ ./playbook.yml --tags configure --skip-tags second_play

PLAY [Deploy Application 1] ******************************************************************************************

TASK [logging : Configure logging (configure)] ***********************************************************************
changed: [localhost]

TASK [repository : Configure repository (configure)] *****************************************************************
changed: [localhost] => (item=repo_app1)

TASK [application1 : Include config tasks (configure)] ***************************************************************
included: /home/otp00233272/projects/inframates/blog/2019/10-October/ansible_tags/roles/application1/tasks/configure.yml for localhost

TASK [application1 : Configure Application1 (configure)] *************************************************************
changed: [localhost]

TASK [monitoring : Install monitoring packages (install)] ************************************************************
changed: [localhost]

TASK [monitoring : Configure monitoring (configure)] *****************************************************************
changed: [localhost]

RUNNING HANDLER [application1 : Restart Application1 (invalid_tag)] **************************************************
changed: [localhost]

PLAY [Deploy Application 2] ******************************************************************************************

PLAY [An untagged task] **********************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************
ok: [localhost]

PLAY RECAP ***********************************************************************************************************
localhost                  : ok=8    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Everything as excepted, all of the tasks with “configure” tag from the first play and none of the tasks from the second play were executed.

Reconfigure only applocation1

$ ./playbook.yml --tags configure --skip-tags application2,firewall,install,logging,monitoring,repository

 PLAY [Deploy Application 1] 
 TASK [application1 : Include config tasks (configure)] *
 included: /home/segal/projects/inframates/blog/2019/10 - October/ansible_tags/roles/application1/tasks/configure.yml for localhost
 TASK [application1 : Configure Application1 (configure)] *
 changed: [localhost]
 RUNNING HANDLER [application1 : Restart Application1 (invalid_tag)] 
 changed: [localhost]
 PLAY [Deploy Application 2] 
 PLAY RECAP *
 localhost                  : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

On the downside, we had to explicitly tell Ansible which tags should be skipped but at least we reconfigure only our application.

Summary

Summarize the above results in tables.

Inheritances

Will your sub-entities inherit the parent’s tags or not? In other words, what is the scope of a tag?

Tagging…Children’s tasks also got the tags
include(.*) taskno
import(.*) taskyes
role in playyes
play in a playbookyes
block of tasksyes
role in dependencyyes
handler taskno effect on handlers

Tag matrix

Add requirements with “–tags”

Will the tasks be executed, if they are tagged with <your tag> and –tags option filled with <option>

tag / optionalluntaggedtaggedalwaysneveryour tag
untaggedrunrunskippedskippedskippedskipped
alwaysrunrunrunrunskippedrun
neverskippedskippedskippedskippedrunskipped
your tagrunskippedrunskippedskippedrun

Skipping with “–skip-tasks”

Will the tasks executed if it’s tagged as <your tag> and –skip-tags option filled with <option>

tag / optionalluntaggedtaggedalwaysneveryour tag
untaggedskippedskippedrunrunrunrun
alwaysrunrunskippedskippedrunrun
neverskippedskippedskippedrunskippedskipped
your tagskippedrunskippedrunrunskipped

I hope in this blog post I clarified how tags Ansible tags work.