Fun with veth-devices, Linux bridges and VLANs in unnamed Linux network namespaces – I

Recently, I started writing some blog posts about my first experiences with LXC-containers and libvirt/virt-manager. Whilst gathering knowledge about LXC basics I stumbled across four hurdles for dummies as me, who would like to experiment with network namespaces, veth devices and bridges on the command line and/or in the context of LXC-containers built with virt-manager:

  • When you use virt-manager/libvirt to set up LXC-containers you are no longer able to use the native LXC commands to deal with these containers. virt-manager/virsh/libvirt directly use the kernel API for cgroups/namespaces and provide their own and specific user interfaces (graphical, virsh, XML configuration files) for the setup of LXC containers and their networks. Not very helpful for quick basic experiments on virtual networking in network namespaces ….
  • LXC-containers created via virt-manager/virsh/libvirt use unnamed namespaces which are identified by unique inode numbers, but not by explicit names. However, almost all articles on the Internet which try to provide a basic understanding of network namespaces and veth devices explicitly use “ip” command options for named namespaces. This raises the question: How to deal with unnamed network namespaces?
  • As a beginner you normally do not know how to get a shell for exploring an existing unnamed namespace. Books offer certain options of the “ip”-command – but these again refer to named network namespaces. You may need such a shell – not only for basic experiments, but also as the administrator of the container’s host: there are many situations in which you would like to enter the (network) namespace of a LXC container directly.
  • When you experiment with complex network structures you may quickly loose the overview over which of the many veth interfaces on your machine is assigned to which (network) namespace.

Objectives and requirements

Unfortunately, even books as “Containerization with LXC” of K. Ivanov did not provide me with the few hints and commands that would have been helpful. I want to close this gap with some blog posts. The simple commands and experiments shown below and in a subsequent article may help others to quickly setup basic network structures for different namespaces – without being dependent on named namespaces, which will not be provided by virt-manager/libvirt. I concentrate on network namespaces here, but some of the things may work for other types of namespace, too.

After a look at some basics, we will create a shell associated with a new unnamed network namespace which will be different from the network namespace of other system processes. Afterwards we will learn how to enter an existing unnamed namespaces by a new shell. A third objective is the attachment of virtual network devices to a network namespace.

In further articles we will use our gathered knowledge to attach veth interfaces of 2 different namespaces to virtual bridges/switches in yet a third namespace, then link the host to the bridge/switch and test communications as well as routing. We shall the extend our virtual networking scenario to isolated groups of namespaces (or containers, if you like) via VLANs. As a side aspect we shall learn how to use a Linux bridge for defining VLANs.

All our experiments will lead to temporary namespaces which can quickly be cretated by scripts and destroyed by killing the basic shell processes associated with them.

Requirements: The kernel should have been compiled with option “CONFIG_NET_NS=y”. We make use of userspace tools that are provided as parts of a RPM or DEB packet named “util-linux” on most Linux distributions.

Namespaces

Some basics first. There are 6 different types of “namespaces” for the isolation of processes or process groups on a Linux system. The different namespace types separate

  • PID-trees,
  • the networks,
  • User-UIDs,
  • mounts,
  • inter process communication,
  • host/domain-names (uts) of process groups

against each each other. Every process on a host is attached to certain namespace (of each type), which it may or may not have in common with another process. Note that the uts-namespace type provides an option to give a certain process an uts-namespace which may get a different hostname than the original host of the process!

“Separation” means: Limitation of the view on the process’ own environment and on the environment of other processes on the system. “Separation” also means a limitation of the control a process can get on processes/environments associated with other namespaces.

Therefore, to isolate LXC containers from other containers and from the host, the container’s processes will typically be assigned to distinct namespaces of most of the 6 types. In addition: The root filesystem of a LXC containers typically resides in a chroot jail.

Three side remarks:

  1. cgroups limit the ressource utilization of process groups on a host. We do not look at cgroups in this article.
  2. Without certain measures the UID namespace of a LXC container will be the same as the namespace of the host. This is e.g. the case for a standard container created with virt-manager. Then root in the container is root on the host. When a container’s basic processes are run with root-privileges of the host we talk of a “privileged container”. Privileged containers pose a potential danger to the host if the container’s environment could be left. There are means to escape chroot jails – and under certain circumstances there are means to cross the borders of a container … and then root is root on the host.
  3. You should be very clear about the fact that a secure isolation of processes and containers on a host depend on other more sophisticated isolation mechanisms beyond namespaces and chroot jails. Typically, SE Linux or Apparmor rules may be required to prevent crossing the line from a namespace attached process to the host environment.

In our network namespace experiments below we normally will not separate the UID namespaces. If you need to do it, you must map a non-privileged UID (> 1000) on UID 0 inside the namespace to be able to perform certain network operations. See the options in the man pages of the commands used below for this mapping.

Network namespaces

The relevant namespace type for the network environment (NICs, bridges etc.) to which a process has access to is the “network namespace”. Below I will sometimes use the abbreviation “net-ns” or simply “netns”.

When you think about it, you will find the above statements on network isolation a bit unclear:

In the real world network packets originate from electronic devices, are transported through cables and are then distributed and redirected by other devices and eventually terminate at yet other electronic devices. So, one may ask: Can a network packet created by a (virtual) network device within a certain namespace cross the namespace border (whatever this may be) at all? Yes, they can:

Network namespaces affect network devices (also virtual ones) and also routing rules coupled to device ports. However, network packets do NOT care about network namespaces on OSI level 2.

To be more precise: Network namespace separation affects network-devices (e.g. Ethernet devices, virtual Linux bridges/switches), IPv4/IPv6 protocol stacks, routing tables, ARP tables, firewalls, /proc/net, /sys/class/net/, QoS policies, ports, port numbers, sockets. But is does not stop an Ethernet packet to reach an Ethernet device in another namespace – as long as the packet can propagate through the virtual network environment at all.

So, now you may ask what virtual means we have available to represent something like cables and Ethernet transport between namespaces? This is one of the purposes veth devices have been invented for! So, we shall study how to bridge different namespaces by the using the 2 Ethernet interfaces of veth devices and by using ports of virtual Linux bridges/switches.

However, regarding container operation you would still want the following to be true for packet filtering:

A fundamental container process, its children and network devices should be confined to devices of a certain “network namespace” because they should not be able to have any direct influence on network devices of other containers or the host.
And: Even if packets move from one network namespace to another you probably want to be able to restrict this traffic in virtual networks as you do in real networks – e.g by packet filter rules (ebtables, iptables) or by VLAN definitions governing ports on virtual bridges/switches.

Many aspects of virtual bridges, filtering, VLANs can be tested already in a simple shell based namespace environment – i.e. without full-fletched containers. See the forthcoming posts for such experiments …

Listing network namespaces on a host

The first thing we need is an overview over active namespaces on a host. For listing namespaces we can use the command “lsns” on a modern Linux system. This command has several options which you may look up in the man pages. Below I show you an excerpt of the output of “lsns” for network namespaces (option “-t net”) on a system where a LXC container was previously started by virt-manager:

mytux:~ # lsns -t net -o NS,TYPE,PATH,NPROCS,PID,PPID,COMMAND,UID,USER 
        NS TYPE PATH              NPROCS   PID  PPID COMMAND                  UID USER  
4026531963 net  /proc/1/ns/net       389     1     0 /usr/lib/systemd/system    0 root   
4026540989 net  /proc/5284/ns/net     21  5284  5282 /sbin/init                 0 root  

Actually, I have omitted some more processes with separate namespaces, which are not relevant in our context. So, do not be surprised if you should find more processes with distinct network namespaces on your system.

The “NS” numbers given in the output are so called “namespace identification numbers”. Actually they are unique inode numbers. (For the reader it may be instructive to let “lsns” run for all namespaces of the host – and compare the outputs.)

Obviously, in our case there is some process with PID “5282”, which has provided a special net-ns for the process with PID “5284”:

mytux:~ # ps aux | grep 5282
root      5282  0.0  0.0 161964  8484 ?        Sl   09:58   0:00 /usr/lib64/libvirt/libvirt_lxc --name lxc1 --console 23 --security=apparmor --handshake 26 --veth vnet1    

This is the process which started the running LXC container from the virt-manager interface. The process with PID “5284” actually is the “init”-Process of this container – which is limited to the network namespace created for it.

Now let us filter or group namespace and process information in different ways:

Overview over all namespaces associated with a process

This is easy – just use the option “-p” :

mytux:~ # lsns -p 5284 -o NS,TYPE,PATH,NPROCS,PID,PPID,COMMAND,UID,USER 
        NS TYPE  PATH              NPROCS   PID  PPID COMMAND                                                            UID USER
4026531837 user  /proc/1/ns/user      416     1     0 /usr/lib/systemd/systemd --switched-root --system --deserialize 24   0 root
4026540984 mnt   /proc/5284/ns/mnt     20  5284  5282 /sbin/init                                                           0 root
4026540985 uts   /proc/5284/ns/uts     20  5284  5282 /sbin/init       
                                                    0 root
4026540986 ipc   /proc/5284/ns/ipc     20  5284  5282 /sbin/init                                                           0 root
4026540987 pid   /proc/5284/ns/pid     20  5284  5282 /sbin/init                                                           0 root
4026540989 net   /proc/5284/ns/net     21  5284  5282 /sbin/init                                                           0 root

Looking up namespaces for a process in the proc-directory

Another approach for looking up namespaces makes use of the “/proc” directory. E.g. on a different system “mylx“, where a process with PID 4634 is associated with a LXC-container:

mylx:/proc # ls -lai /proc/1/ns
total 0
344372 dr-x--x--x 2 root root 0 Oct  7 11:28 .
  1165 dr-xr-xr-x 9 root root 0 Oct  7 09:34 ..
341734 lrwxrwxrwx 1 root root 0 Oct  7 11:28 ipc -> ipc:[4026531839]
341737 lrwxrwxrwx 1 root root 0 Oct  7 11:28 mnt -> mnt:[4026531840]
344373 lrwxrwxrwx 1 root root 0 Oct  7 11:28 net -> net:[4026531963]
341735 lrwxrwxrwx 1 root root 0 Oct  7 11:28 pid -> pid:[4026531836]
341736 lrwxrwxrwx 1 root root 0 Oct  7 11:28 user -> user:[4026531837]
341733 lrwxrwxrwx 1 root root 0 Oct  7 11:28 uts -> uts:[4026531838]

mylx:/proc # ls -lai /proc/4634/ns
total 0
 38887 dr-x--x--x 2 root root 0 Oct  7 09:36 .
 40573 dr-xr-xr-x 9 root root 0 Oct  7 09:36 ..
341763 lrwxrwxrwx 1 root root 0 Oct  7 11:28 ipc -> ipc:[4026540980]
341765 lrwxrwxrwx 1 root root 0 Oct  7 11:28 mnt -> mnt:[4026540978]
345062 lrwxrwxrwx 1 root root 0 Oct  7 11:28 net -> net:[4026540983]
 38888 lrwxrwxrwx 1 root root 0 Oct  7 09:36 pid -> pid:[4026540981]
341764 lrwxrwxrwx 1 root root 0 Oct  7 11:28 user -> user:[4026531837]
341762 lrwxrwxrwx 1 root root 0 Oct  7 11:28 uts -> uts:[4026540979]

What does this output for 2 different processes tell us? Obviously, the host and the LXC container have different namespaces – with one remarkable exception: the “user namespace”! They are identical. Meaning: Root on the container is root on the host. A typical sign of a “privileged” LXC container and of potential security issues.

List all processes related to a given namespace?

“lsns” does not help us here. Note:

“lsns” only shows you the lowest PID associated with a certain (network) namespace.

So, you have to use the “ps” commands with appropriate filters. The following is from a system, where a LXC container is bound to the network namespace with identification number 4026540989:

mytux:~ # lsns -t net -o NS,TYPE,PATH,NPROCS,PID,PPID,COMMAND,UID,USER
        NS TYPE PATH              NPROCS   PID  PPID COMMAND                                               UID USER
4026531963 net  /proc/1/ns/net       401     1     0 /usr/lib/systemd/systemd --switched-root --system --d   0 root
4026540989 net  /proc/6866/ns/net     20  6866  6864 /sbin/init                                              0 root

mytux:~ #  ps -eo netns,pid,ppid,user,args --sort netns | grep 4026540989
4026531963 16077  4715 root     grep --color=auto 4026540989
4026540989  6866  6864 root     /sbin/init
4026540989  6899  6866 root     /usr/lib/systemd/systemd-journald
4026540989  6922  6866 root     /usr/sbin/ModemManager
4026540989  6925  6866 message+ /bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation   
4026540989  6927  6866 tftp     /usr/sbin/nscd
4026540989  6943  6866 root     /usr/lib/wicked/bin/wickedd-dhcp6 --systemd --foreground
4026540989  6945  6866 root     /usr/lib/wicked/bin/wickedd-dhcp4 --systemd --foreground
4026540989  6947  6866 systemd+ avahi-daemon: running [linux.local]
4026540989  6949  6866 root     /usr/lib/wicked/bin/wickedd-auto4 --systemd --foreground
4026540989  6951  6866 avahi-a+ /usr/lib/polkit-1/polkitd --no-debug
n4026540989  6954  6866 root     /usr/lib/systemd/systemd-logind
4026540989  6955  6866 root     login -- root
4026540989  6967  6866 root     /usr/sbin/wickedd --systemd --foreground
4026540989  6975  6866 root     /usr/sbin/wickedd-nanny --systemd --foreground
4026540989  7032  6866 root     /usr/lib/accounts-daemon
4026540989  7353  6866 root     /usr/sbin/cupsd -f
4026540989  7444  6866 root     /usr/lib/postfix/master -w
4026540989  7445  7444 postfix  pickup -l -t fifo -u
4026540989  7446  7444 postfix  qmgr -l -t fifo -u
4026540989  7463  6866 root     /usr/sbin/cron -n
4026540989  7507  6866 root     /usr/lib/systemd/systemd --user
4026540989  7511  7507 root     (sd-pam)
4026540989  7514  6955 root     -bash

If you work a lot with LXC containers it my be worth writing some clever bash or python-script for analyzing the “/proc”-directory with adjustable filters to achieve a customizable overview over processes attached to certain namespaces or containers.

Hint regarding the NS values in the following examples:
The following examples have been performed on different systems or after different start situations of one and the same system. So it makes no sense to compare all NS values between different examples – but only within an example.

Create a shell inside a new network namespace with the “unshare” command …

For some simple experiments it would be helpful if we could create a process (as a shell) with its own network-namespace. For this purpose Linux provides us with the command “unshare” (again with a lot of options, which you should look up).

For starting a new bash with a separate net-ns we use the option “-n“:

mytux:~ # unshare -n /bin/bash 
mytux:~ # lsns -t net
        NS TYPE NPROCS   PID USER  COMMAND
4026531963 net     398     1 root  /usr/lib/systemd/systemd --switched-root --system --deserialize 24   
4026540989 net      21  5284 root  /sbin/init
4026541186 net       2 27970 root  /bin/bash

mytux:~ # ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

mytux:~ # exit
exit

mytux:~ # ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1   
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether d7:58:88:ab:cd:ef brd ff:ff:ff:ff:ff:ff
....
....

Obviously, it is not possible to see from the prompt that we have entered a different (network) namespace with the creation of the new shell. We shall take care of this in a moment. For the time being, it may be a good idea to issue commands like

lsns -t net -p 1; lsns -t net -p $$

in the shell opened with “unshare”. However, also our look at the network interfaces proved that the started “bash” was directly associated with a different net-ns than the “parent” bash. In the “unshared” bash only a “lo”-device was provided. When we left the newly created “bash” we at once saw more network devices (namely the devices of the host).

Note: A namespace (of any type) is always associated with at least one process. Whenever we want to create a new namespace for an experiment we have to combine it with a (new) process. During the experiments in this post series we will create new network namespaces together with related simple bash-processes.

And: A namespace lives as long as the associated process (or processes). To keep a specific new network namespace alive for later experiments we put the associated basic bash-process into the background of the host-system.

In real world scenarios the processes related to namespaces are of course more complex than a shell. Examples are containers, browser-processes, etc. This leads us to the question whether we can “enter” an existing namespace somehow (e.g. with a shell) to gather information about it or to manipulate it. We will answer this question in a minute.

Information about host processes from a shell inside a specific network namespace?

You can get information about all processes on a host from any process with a specific network namespace – as long as the PID namespace for this process is not separated from the PID namespace of the host. And as long as we have not separated the UID namespaces: root in a network namespace then is root on the host with all the rights there!

Can a normal unprivileged user use “unshare”, too?

Yes, but his/her UID must be mapped to root inside the new network namespace. For this purpose we can use the option “-r” of the unshare command; see the man pages. Otherwise: Not without certain measures – e.g. on the sudo side. (And think about security when using sudo directives. The links at the end of the article may give you some ideas about some risks.)

You may try the following commands (here executed on a freshly started system):

myself@mytux:~> unshare -n -r /bin/bash 
mytux:~ # lsns -t net -t user
        NS TYPE  NPROCS   PID USER COMMAND
4026540842 user       2  6574 root /bin/bash
4026540846 net        2  6574 root /bin/bash
mytux:~ # 

Note the change of the prompt as the shell starts inside the new network namespace! And “lsns” does not give us any information on the NS numbers for net and user namespaces of normal host processes!

However, on another host terminal the “real” root of the host gets:

mytux:~ # lsns -t net -t user 
        NS TYPE  NPROCS   PID USER   COMMAND
4026531837 user     382     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 24   
4026531963 net      380     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 24   
4026540842 user       1  6574 myself /bin/bash
4026540846 net        1  6574 myself /bin/bash

There, we see that the user namespaces of the unshared shell and other host processes really are different.

Open a shell for a new named network namespace

The “unshare” command does not care about “named” network namespaces. So, for the sake of completeness: If you like to or must experiment with named network namespaces you may want to use the “ip” command with appropriate options, e.g.:

mytux:~ # ip netns add mynetns1 
mytux:~ # ip netns exec mynetns1 bash   
mytux:~ # lsns -o NS -t net -p $$
        NS
4026541079
mytux:~ # exit 
mytux:~ # lsns -o NS -t net -p $$
        NS
4026531963
mytux:~ # 

“mynetns1” in the example is the name that I gave to my newly created named network namespace.

How to open a shell for an already existing network namespace? Use “nsenter”

Regarding processes with their specific namespaces or LXC containers: How can we open a shell that is assigned to the same network namespace as a specific process? This is what the command “nsenter” is good for. For our purposes the options “-t” and “-n” are relevant (see the man pages). In the following example we first create a bash shell (PID 15150) with a new network namespace and move its process in the background. Then we open a new bash in the foreground (PID 15180) and attach this bash shell to the namespace of the process with PID 15150:

mylx:~ # unshare -n /bin/bash &
[1] 15150
mylx:~ # lsns -t net 
        NS TYPE NPROCS   PID USER  COMMAND
4026531963 net     379     1 root  /usr/lib/systemd/systemd --switched-root --system --deserialize 24   
4026540983 net      23  4634 root  /sbin/init
4026541170 net       1 15150 root  /bin/bash

[1]+  Stopped                 unshare -n /bin/bash
mylx:~ # nsenter -t 15150 -n /bin/bash
mylx:~ # ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1   
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
mylx:~ # echo $$
15180
mylx:~ # lsns -t net -p $$
        NS TYPE NPROCS   PID USER COMMAND
4026541170 net       3 15150 root /bin/bash
mylx:~ # 

Note, again, that “lsns” only gives you the lowest process number that opened a namespace. Actually, we are in a different bash with PID “15180”. If you want to see all process using the same network namespace you may use :

mylx:~ # echo $$
15180
mylx:~ # ps -eo pid,user,netns,args --sort user | grep 4026541170  
15150 root     4026541170 /bin/bash
15180 root     4026541170 /bin/bash
16284 root     4026541170 ps -eo pid,user,netns,
args --sort user
16285 root     4026541170 grep --color=auto 4026541170

Note that the shell created by nsenter is different from the shell-process we created (with unshare) as the bearing process of our namespace.

In the same way you can create a shell with nsenter to explore the network namespace of a running LXC container. Let us try this for an existing LXC container on system “mylx” with PID 4634 (see above: 4026540983 net 23 4634 root /sbin/init).

mylx:~ # nsenter -t 4634 -n /bin/bash
mylx:~ # ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1   
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
13: eth0@if14: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000   
    link/ether 00:16:3e:a3:22:b8 brd ff:ff:ff:ff:ff:ff link-netnsid 0
mylx:~ # exit
exit

Obviously, an ethernet device eth0 exists in this container. Actually, it is an interface of a veth device with a peer interface “if14”; see below.

Change the hostname part of a shell’s prompt in a separate network namespace

We saw that the prompt of a shell in a separate network namespace normally does not indicate anything about the namespace environment. How can we change this? We need 2 steps to achieve this:

  • We open a shell in the background not only for a separate network namespace but also for a different uts namespace. Then any changes to the hostname inside the uts namespace for the running background process will have no impact on the host.
  • The “nsenter” command does not only work for shells but for any reasonable command. Therefore, we can also apply it for the command “hostname”.

Now, before we enter the separate namespaces of the process with yet another shell we can first change the hostname in the newly created uts namespace:

mytux:~ # unshare --net --uts /bin/bash &
[1] 25512
mytux:~ # nsenter -t 25512 -u hostname netns1

[1]+  Stopped                 unshare --net --uts /bin/bash   
mytux:~ # echo $$
20334
mytux:~ # nsenter -t 25512 -u -n /bin/bash 
netns1:~ #
netns1:~ # lsns -t net -t uts -p $$
        NS TYPE NPROCS   PID USER COMMAND
4026540975 uts       3 25512 root /bin/bash
4026540977 net       3 25512 root /bin/bash
netns1:~ # exit
mytux:~ # hostname
mytux

Note the “-u” in the command line where we set the hostname! Note further the change of the hostname in the prompt! In more complex scenarios, this little trick may help you to keep an overview over which namespace we are currently working in.

veth-devices

For container technology “veth” devices are of special importance. A veth device has two associated Ethernet interfaces – so called “peer” interfaces. One can imagine these interfaces like linked by a cable on OSI level 2 – a packet arriving at one interface gets available at the other interface, too. Even if one of the interfaces has no IP address assigned.

This feature is handy when we e.g. need to connect a host or a virtualized guest to an IP-less bridge. Or we can use veth-devices to uplink several bridges to one another. See a former blog post
Fun with veth devices, Linux virtual bridges, KVM, VMware – attach the host and connect bridges via veth
about these possibilities.

As a first trial we will assign the veth device and both its interfaces to one and the same network namespace. Most articles and books show you how to achieve this by the use of the “ip” command with an option for a “named” namespace. In most cases the “ip” command would have been used to create a named net-ns by something like

ip netns add NAME

where NAME is the name we explicitly give to the added network namespace. When such a named net-ns exists we can assign an Ethernet interface named “ethx” to the net-ns by:

ip link set ethx netns NAME

However, in all our previous statements no NAME for a network namespace has been used so far. So, how to achieve something similar for unnamed network namespaces? A look into the man pages helps: The “ip” command allows the introduction of a PID together with the option parameter “netns” at least for the variant “ip link set”. Does this work for veth devices and the command “ip link add”, too? And does it work for both Ethernet interfaces?

In the example discussed above we had a namespace 4026541170 of process with PID 15180. We open a bash shell on our host mylx, where PID 15150 still runs in the background, and :

mylx:~ # echo $$
27977
mylx:~ # lsns -t net
        NS TYPE NPROCS   PID USER  COMMAND   
4026531963 net     393     1 root  /usr/lib/systemd/systemd --switched-root --system --deserialize 24   
4026540983 net      23  4634 root  /sbin/init
4026541170 net       1 15150 root  /bin/bash
mylx:~ # ip link add veth1 netns 15150 type veth peer name veth2 netns 15150
mylx:~ # nsenter -t 15150 -n /bin/bash
mylx:~ # echo $$
28350
mylx:~ # ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: veth2@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000   
    link/ether 8e:a0:79:28:ae:12 brd ff:ff:ff:ff:ff:ff
3: veth1@veth2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000   
    link/ether fa:1e:2c:e3:00:8f brd ff:ff:ff:ff:ff:ff
mylx:~ # 

Success! Obviously, we have managed to create a veth device with both its 2 interfaces inside the network namespace associated with our background process of PID 15150.

The Ethernet interfaces are DOWN – but this was to be expected. So far, so good. Of course it would be more interesting to position the first veth interface in one network namespace and the second interface in another network namespace. This would allow network packets from a container to cross the border of the container’s namespace into an external one. Topics for the next articles …

Summary and outlook on further posts

Enough for today. We have seen how we can list (network) namespaces and associated processes. We are able to create shells together with and inside in a new network namespace. And we can open a shell that can be attached to an already existing network namespace. All without using a “NAME” of the network namespace! We have also shown how a veth device can be added to a specific network namespace. We have a set of tools now, which we can use in more complicated virtual network experiments.

In the next post

Fun with veth-devices, Linux bridges and VLANs in unnamed Linux network namespaces – II

I shall present a virtual network environment for several interesting experiments with network namespaces – or containers, if you like. Further articles will discuss such experiments step by setp.

Addendum, 25.03.2024: I have started a new series about virtual networking experiments concerning veths with VLAN-interfaces, namespaces, routes, ARP, ICMP and security aspects. If you are interested in these topics a look at the posts in the new series may give you some more information on specific topics.

Links

Introduction into network namespaces
http://www.linux-magazin.de/ Ausgaben/ 2016/06/ Network-Namespaces

Using unshare without root-privileges
https://unix.stackexchange.com/ questions/ 252714/ is-it-possible-to-run-unshare-n-program-as-an-unprivileged-user
https://bbs.archlinux.org/viewtopic.php?id=205240
https://blog.mister-muffin.de/ 2015/10/25/ unshare-without-superuser-privileges/

LXC-Container – Fehlermeldungen mit Bezug zum File memory.memsw.usage_in_bytes

Wenn man mit LXC-Containern experimentiert, kann es sein, dass man über Fehlermeldungen im System Log File stolpert, die etwa wie folgt aussehen:

2017-10-22T16:25:08.090990+02:00 mytux libvirtd[2795]: Failed to open file '/sys/fs/cgroup/memory/machine.slice/machine-lxc\x2d25976\x2dlxcx.scope/memory.memsw.usage_in_bytes': No such file or directory
2017-10-22T16:25:08.091288+02:00 mytux libvirtd[2795]: Unable to read from '/sys/fs/cgroup/memory/machine.slice/machine-lxc\x2d25976\x2dlxcx.scope/memory.memsw.usage_in_bytes': No such file or directory

In meinem Fall (Opensuse Leap 42.2) wurde eine solche Meldung alle 3 Sekunden für jeden laufenden LXC-Container generiert. Nicht sehr erfreulich …

Die Ursache dieser Meldungen liegt darin, dass die “memsw”-Dateien Information über die Nutzung des Swaps durch Prozesse sammeln, die über cgroup-Festlegungen limitiert sein mögen. Wenn aber der Kernel des Hosts ohne die Option CONFIG_MEMCG_SWAP_ENABLED kompiliert wurde, werden die obigen Fehler ausgegeben. Dies ist etwa für den Default-Kernel von Opensuse Leap 42.2 der “swapaccount=1” mitgibt. Das muss man halt in die Grub2-Konfiguration eintragen. Unter Opensuse mit YaST …

LXC-Betriebssystem-Container mittels virt-manager unter Opensuse Leap 42.2 – I

Das Thema “Container” ist wegen der guten Ressourcenauslastung auch für KMUs von Interesse. Will man sich dem Thema “Container” unter Linux annähern, ohne sich gleich mit Docker zu befassen, kann man alternativ zu LXC greifen. Dafür sprechen sogar ein paar gute Gründe:

  • LXC ist erprobt; viele Distributionen bringen LXC mit.
  • Ubuntu setzt mit der LXD-Umgebung explizit auf dieses Container-Format.
  • LXC (und LXD) setzen mehr auf Container für vollständige Betriebssysteme, Docker dagegen auf modulare Container für Einzelapplikationen. Wären da nicht diverse Sicherheitsthemen, so könnten performante, ressourcenschonende LXC-Container virtuelle (Linux-) Maschinen unter KVM ersetzen. Die potentielle Packungsdichte von LXC-Containern auf einem Host ist deutlich höher als die von KVM/qemu-VMs.
  • LXC erlaubt einen unpriviligierten Container-Betrieb – der Gewinn an Sicherheit erfordert allerdings Mühe im Container-Setup. Aber immerhin …
  • Auch in kleinen Umgebungen will man Container neben virtuellen KVM-Maschinen gerne grafisch verwalten und überwachen. Als handliche und vor allem erschwingliche Werkzeuge bleiben da eigentlich nur “virtlib/virt-manager” (rudimentäre Minimalausstattung) und “Proxmox” für den professionellen Einsatz übrig. Beide Tools unterstützen LXC-Container und erlauben zudem das Anlegen mehr oder weniger komplexer Storage-Konfigurationen.
  • Läuft der gewünschte Container erst mal, darf man sich über eine sehr gute Performance freuen (zumindest im kleinskaligen Betriebsumfeld).

Am letzten Wochenende wollte ich einen LXC-Container unter Opensuse Leap einfach mal ein wenig austesten. Für das Erstellen wollte ich das Gespann “libvirt/virt-manager” heranziehen. Die Anlage eines sog. “Betriebssystem-Containers” erwies sich damit unter Leap 42.2 aber als kleineres Abenteuer – selbst bei einer rein lokalen Storage-Konfiguration. Dieser und der nachfolgende Artikel sollen Einsteigern helfen, die ersten Hürden zu überwinden.

Begrifflichkeiten und eine Warnung

Ich gehe nachfolgend aus Platzgründen nicht weiter auf die grundlegenden Unterschiede zwischen Containern und voll- oder paravirtualisierten “Virtuellen Maschinen” [VMs] ein. Wer sich Container-Technologie im Schnelldurchgang reinziehen will, sei auf den Foliensatz https://de.slideshare.net/jpetazzo/anatomy-of-a-container-namespaces-cgroups-some-filesystem-magic-linuxcon verwiesen.

Für den vorliegenden Blog-Beitrag ist die Vorstellung hilfreich, ein Container sei so etwas wie eine aufgeblasene chroot-Umgebung, die Prozesse in einer vom restlichen Betriebssystem “isolierten” Umgebung bündelt.

Auch die Suse-Dokumentationen zur Anlage von Containern
https://www.suse.com/documentation/sles-12/singlehtml/book_virt/book_virt.html#sec.lxc.setup.container.app
https://doc.opensuse.org/documentation/leap/virtualization/book.virt_color_en.pdf
merken an:

Conceptually, containers can be seen as an improved chroot technique. The difference is that a chroot environment separates only the file system, whereas containers go further and provide resource management and control via cgroups.”

Die vom Container-Nutzer ausgelösten Prozesse laufen direkt unter dem Kernel des Hosts – und nicht wie in einer VM unter einem eigenem Kernel für die emulierte HW). Für die Container-
Isolation sorgen unter Linux cgroups und vor allem strong>namespaces (seit Kernelversion 3.8):

  • “cgroups” bündeln Prozesse in Bezug auf die limitierte Nutzung von System-Ressourcen auf dem Host.
  • “namespaces” sorgen hingegen dafür, dass Prozesse und ihre Kinder eine separate Sicht und einen abgegrenzten Zugriff auf ihre jeweilige Betriebssystem-Umgebung erhalten.

Vor allem “namespaces” sorgen also für eine (hoffentlich !) hinreichende Isolation der Container gegeneinander und gegenüber dem Host. Aber: Container lassen sich ohne zusätzliche Hilfsmittel bei weitem nicht so gut vom Host isolieren wie etwa virtuelle Maschinen unter KVM/qemu; resultierende Sicherheitsthemen werde ich in kommenden Blog-Beiträgen immer mal wieder aufgreifen. Ich zitiere einen der LXC-Vorkämpfer aus einem frühen Artikel (https://www.berrange.com/posts/2011/09/27/getting-started-with-lxc-using-libvirt/):

“Repeat after me “LXC is not yet secure. If I want real security I will use KVM”.

Das gilt mit Einschränkungen wohl auch heute noch; wer den professionellen Einsatz von LXC-Containern erwägt, muss sich mit SE-Linux- oder Apparmor-Profilen sowie ggf. Kernel-Capabilities auseinandersetzen. Ferner empfehle ich, einen Einsatz von LXC-Containern unter KVM-VMs in Betracht zu ziehen, wobei letztere dafür mit virtio-Treibern beschleunigt werden müssen.

Unterschiede zwischen nativen lxc-Tools und libvirt-lxc

Will man mit virt-manager und LXC-Containern arbeiten, begibt man sich auf ein nicht unumstrittenes Gleis. Es gibt nämlich zwei nicht kompatible Toolsets zum Aufsetzen und Verwalten von LXC-Containern: lxc und libvirt-xlc. “lxc” ist das native Toolset und besteht aus einer Reihe sehr flexibel einsetzbaren CLI-Kommandos. “lxc” liegt in den Version 1.1 bzw. 2.0 unter vielen aktuellen Linux-Distributionen vor (Leap 42.2 =>LXC V1.1, Debian Stretch => LXC V2.0). Container werden unter “lxc” über (vorgefertigte) “Configuration Files” definiert.

Libvirt greift dagegen auf XML-Files zur Definition von Containern zurück – also genau wie bei KVM/qemu-VMs. Libvirt nutzt für das LXC-Setup ferner direkte Schnittstellen zum Kernel und rankt darum eigene, unabhängige Tools, die mit den nativen LXC-Tools wenig gemein haben. Man kann native lxc-Kommandos meines Wissens deshalb auch nicht zur Verwaltung der Container verwenden, die mit libvirt-lxc erzeugt wurden. Dafür kann man nach der Einrichtung von libvirt-LXC-Containern aber auf das grafische “virt-manager”-Interface zum Starten, Stoppen und Manipulieren dieser Container zurückgreifen. Auf der Kommandozeile kann alternativ das “virsh”-Kommando mit lxc-bezogenen Optionen eingesetzt werden.

Beide Toolsets nehmen es einem ab, sich um die Konfiguration von cgroups (Ressourcenmanagement) und namespaces (Abschottung) im Detail und händisch kümmern zu müssen. Sie tun dies aber auf unterschiedliche Weise – und stellen zugehörige Informationen keineswegs in kompatibler Weise bereit. In diesem Artikel nutze ich die “libvirt-lxc”-Tools. Nicht zuletzt, weil ich selbst von KVM her komme. Aber – ich sage es vorweg:

Die libvirt-lxc-Tools lassen sich aus meiner Sicht nicht wirklich bruchfrei nutzen.

Der eine oder andere Nutzer wird daher mit wachsender Erfahrung zu den nativen Tools wechseln. Im Internet sind zudem sehr viel mehr Informationen zur Verwaltung von LXC-Containern mit den nativen Tools verfügbar als für die “libvirt-lxc”-Tools. Interessant ist ein LXC-Container-Setup mit libvirt/virt-manager aber allemal.

Ziel: LXC-Betriebssystem-Container

Unter “virt-manager” werden 2 LXC-Container-Typen unterschieden:

  • LXC Distribution
    Container für eine Umgebung mit einer Root-Filesystem-Struktur für eine Distribution. Solche Container eigenen sich m.E. als Ersatz für KVM/qemu-VMs.
  • LXC Application Container für einzelne Anwendungen. Ein solcher Container bietet nur eine temporäre, isolierte Umgebung für einzelnen Anwendungen – und wird mit dem Schließen der Anwendung auch beendet. Solche Container sind eher mit Docker-Containern vergleichbar.

App-Containern fehlen bei ohne eigene Zusatzmaßnahmen wesentliche Elemente eines für die jeweilige Applikation funktionierenden Filesystems. Das ist ohne vorgefertigte Schablonen Bastelarbeit. Man muss sich je nach Applikation u.a. um Anteile von /etc, /sys, /proc/, /dev, /dev/shm und relevante Bibliotheks-Bereiche wie des Filesystems kümmern. Siehe die Links unten.

Das verführt dazu, am Anfang seiner Lernkurve einen “Betriebssystem-Container” zu erstellen. Für das Filesystem kann man dann nämlich eine Basisimplementation der jeweiligen Distribution heranziehen. Für Debian ist hier “debootstrap” zu nennen.

Bzgl. der praktischen Anlage notwendiger Verzeichnisinhalte erleichtern einem unter dem RPM-basierten Opensuse hingegen sog. Paket-Patterns das Leben. Für ein minimalistisch aufgesetztes Filesystem-Setup greift man hier auf das sog. “base”-Pattern zurück.

Opensuses Script für den Root-Filesystem-Unterbau

Bzgl. der Installation des “base”-Patterns für Container hat die OpenSuSE-Mannschaft es sich in letzter Zeit allerdings etwas zu einfach gemacht: Die aktuelle Doku verweist nämlich auf ein Skript “/usr/bin/virt-create-rootfs”.

Diese Script läuft ohne Zusatzwissen und ohne Modifikationen unter Leap 42.2/42.3 aber ins Leere. In Blick in in den Code zeigt: Standardmäßig wird nur Opensuse 13.1 unterstützt; die zugehörigen Repositories, die bemüht werden, sind aber längst obsolet. Ich zeige nachfolgend relevante Ausschnitte aus dem Script:

#!/bin/sh
set -e
....
....
function print_help
{
cat << EOF
virt-create-rootfs --root /path/to/rootfs [ARGS]

Create a new root file system to use for distribution containers.

ARGUMENTS

    -h, --help          print this help and exit
    -r, --root          path where to create the root FS
    -d, --distro        distribution to install
    -a, --arch          target architecture
    -u, --url           URL of the registration server
    -c, --regcode       registration code for the product
    -p, --root-pass     the root password to set in the root FS
    --dry-run           don't actually run it
EOF
}

ARCH=$(uname -i)
ROOT=
DISTRO=
URL=
REG_CODE=
ROOT_PASS=
DRY_RUN=
.....
.....
function call_zypper
{
    $RUN zypper --root "$ROOT" $*
}

function install_sle
{
    PRODUCT="$1"
    VERSION="$2"

    case "$VERSION" in
        12.0)
            # Transform into zypper internal version scheme
            VERSION="12"
            ;;
        *)
            fail "Unhandled SLE version: $VERSION"
            ;;
    esac
....
....

}

case "$DISTRO" in
    SLED-*)
        install_sle "SLED" "${DISTRO:5}"
        ;;
    SLED-* | SLES-*)
        install_sle "SLES" "${DISTRO:5}"
        ;;

    openSUSE-*)
        VERSION=${DISTRO:9}
        case "$VERSION" in
            13.1)
                REPO="http://download.opensuse.org/distribution/13.1/repo/oss/"
                UPDATE_REPO="http://download.opensuse.org/update/13.1/"
                ;;
            *)
                fail "Unhandled openSUSE version: $VERSION"
                ;;
        esac
        call_zypper ar "$REPO" "openSUSE"
        call_zypper ar "$UPDATE_REPO" "openSUSE udpate"
        call_
zypper in --no-recommends -t pattern base
        ;;
esac

if test "$DRY_RUN" != "yes"; then
    echo "pts/0" >> "$ROOT/etc/securetty"
    chroot "$ROOT" /usr/bin/passwd
fi

 
Man muss das betreffende Skript also anpassen oder die notwendigen Schritte manuell durchführen.

Modifikation des Scripts

Lässt man mal den Zugriff auf SLES oder SLED-Repositories außer Acht, so leistet das Script im Grunde nur Folgendes:

  • Ein Verzeichnis des Hosts als künftiges root-Verzeichnis des Containers aus einem Übergabeparameter auslesen.
  • Das grundlegende “oss”-Repository sowie das zugeh. Update-Repository für eine Distribution festlegen.
  • “zypper” in der Form “zypper –root PATH_TO_CONTAINER_ROOT in –no-recommends -t pattern” aufrufen.
  • Das root-Password für den chroot-Bereich ändern.

Entscheidend ist im Script-Ablauf die “–root” Option von zypper; die sorgt dafür, dass die Verzeichnisse/Files unterhalb des definierten chroot-Verzeichnisses (PATH_TO_CONTAINER_ROOT) gemäß des definierten Paket-Patterns gefüllt werden. Wir ergänzen das Script deshalb versuchsweise in der Abfrage für die Distribution und ändern die Statements in den Zeilen 205 – 207 ab:

....
.....
case "$DISTRO" in
    SLED-*)
        install_sle "SLED" "${DISTRO:5}"
        ;;
    SLED-* | SLES-*)
        install_sle "SLES" "${DISTRO:5}"
        ;;

    openSUSE-*)
        VERSION=${DISTRO:9}
        case "$VERSION" in
            13.1)
                REPO="http://download.opensuse.org/distribution/13.1/repo/oss/"
                UPDATE_REPO="http://download.opensuse.org/update/13.1/"
                ;;
            42.2)
                REPO="http://download.opensuse.org/distribution/leap/42.2/repo/oss/"
                UPDATE_REPO="http://download.opensuse.org/update/leap/42.2/"
                ;;
            *)
                fail "Unhandled openSUSE version: $VERSION"
                ;;
        esac
        echo "HERE0"
        call_zypper ar "$REPO" "openSUSE"
        echo "HERE1"
        call_zypper ar "$UPDATE_REPO" "openSUSE-udpate"
        echo "HERE2"
        call_zypper in --no-recommends -t pattern enhanced_base
        call_zypper in --no-recommends -t pattern  file_server
        #call_zypper in --no-recommends -t pattern base
        #call_zypper in --no-recommends -t pattern "patterns-openSUSE-enhanced_base"
        ;;
esac

 
Um wenigstens ein paar “Convenience”-Pakte zusätzlich zu erhalten, habe ich das “enhanced_base”-Pattern gewählt.

Anlegen des Filesystems

Für meinen Test nutze ich ein LVM-Volume auf einem SSD-Array; das Volume wird dabei auf das Verzeichnis “/kvm/” gemountet. Das Filesystem meines Containers soll unter “/kvm/lxc2/ zu liegen kommen. Also Verzeichnis angelegt und dann unser umgeschriebenes Script starten:

        
mytux:~ # virt-create-rootfs -r /kvm/lxc2/ -d openSUSE-42.2 
Adding repository 'Leap-42.2-Oss' .......................................................[done]
Repository 'Leap-42.2-Oss' successfully added

URI         : http://download.opensuse.org/distribution/leap/42.2/repo/oss/
Enabled     : Yes                                                          
GPG Check   : Yes                                                          
Autorefresh : No                                                           
Priority    : 99 (default priority)                                        

Repository priorities are without effect. All enabled repositories share the same priority.
r
Adding repository 'Leap-42.2-Update' ....................................................[done]
Repository 'Leap-42.2-Update' successfully added

URI         : http://download.opensuse.org/update/leap/42.2/oss/
Enabled     : Yes                                               
GPG Check   : Yes                                               
Autorefresh : No                                                
Priority    : 99 (default priority)                             

Repository priorities are without effect. All enabled repositories share the same priority.

New repository or package signing key received:

  Repository:       Leap-42.2-Oss                                       
  Key Name:         openSUSE Project Signing Key <opensuse@opensuse.org>
  Key Fingerprint:  22C07BA5 34178CD0 2EFE22AA B88B2FD4 3DBDC284        
  Key Created:      Mon May  5 10:37:40 2014                            
  Key Expires:      Thu May  2 10:37:40 2024                            
  Rpm Name:         gpg-pubkey-3dbdc284-53674dd4                        


Do you want to reject the key, trust temporarily, or trust always? [r/t/a/? shows all options] (r): a
Building repository 'Leap-42.2-Oss' cache ...............................................[done]
Building repository 'Leap-42.2-Update' cache ............................................[done]
Loading repository data...
Reading installed packages...
Resolving package dependencies...

The following 163 NEW packages are going to be installed:
  aaa_base bash binutils coreutils cpio cracklib cracklib-dict-full dbus-1 dbus-1-x11
  device-mapper diffutils dirmngr dracut elfutils expat file file-magic filesystem fillup
  findutils fipscheck gawk gio-branding-openSUSE glib2-tools glibc gpg2 grep gzip hardlink
  hwinfo info insserv-compat kbd kbd-legacy klogd kmod kmod-compat krb5 libX11-6 libX11-data
  libXau6 libacl1 libadns1 libaio1 libapparmor1 libasm1 libassuan0 libattr1 libaudit1 libblkid1
  libbz2-1 libcap-ng0 libcap2 libcom_err2 libcrack2 libcryptsetup4 libcurl4 libdbus-1-3 libdw1
  libedit0 libelf0 libelf1 libexpat1 libfdisk1 libffi4 libfipscheck1 libgcc_s1 libgcrypt20
  libgio-2_0-0 libglib-2_0-0 libgmodule-2_0-0 libgmp10 libgobject-2_0-0 libgpg-error0 libidn11
  libkeyutils1 libkmod2 libksba8 libldap-2_4-2 liblua5_1 liblzma5 libmagic1 libmount1
  libmozjs-17_0 libncurses5 libncurses6 libnl-config libnl3-200 libopenssl1_0_0 libpcre1
  libpolkit0 libpopt0 libprocps3 libpth20 libqrencode3 libreadline6 libsasl2-3 libseccomp2
  libselinux1 libsemanage1 libsepol1 libsmartcols1 libssh2-1 libstdc++6 libsystemd0
  libtirpc-netconfig libtirpc3 libudev1 libusb-0_1-4 libusb-1_0-0 libustr-1_0-1 libutempter0
  libuuid1 libverto1 libwicked-0-6 libwrap0 libx86emu1 libxcb1 libxml2-2 libz1 libzio1
  mozilla-nspr ncurses-utils netcfg openSUSE-build-key openSUSE-release openSUSE-release-ftp
  openssh openssl pam pam-config patterns-openSUSE-base patterns-openSUSE-enhanced_base
  perl-base permissions pigz pinentry pkg-config polkit polkit-default-privs procps rpcbind rpm
  sed shadow shared-mime-info suse-module-tools sysconfig sysconfig-netconfig systemd
  systemd-presets-branding-openSUSE systemd-sysvinit sysvinit-tools terminfo-base time udev
  update-alternatives util-linux util-linux-systemd which wicked wicked-service xz

The following 2 NEW patterns are going to be installed:
  base enhanced_base

The following NEW product is going to be installed:
  openSUSE

163 new packages to install.
Overall download size: 48.1 MiB. Already cached: 0 B. After the operation, additional 178.0 MiB
will be used.
Continue? [y/n/...? shows all options] (y): y
....
....
New password: 
Retype new password: 
passwd: password updated successfully

 
Die Installation nimmt kaum Zeit in Anspruch. Danach liegt ein ziemlich umfassendes chroot-Filesystem vor, für das wir auch ein root-
Passwort definiert haben:

mytux:~ # la /kvm/lxc2
total 84
drwxr-xr-x 21 root root 4096 Sep 25 18:27 .
drwxr-xr-x  6 root root 4096 Sep 25 18:17 ..
drwxr-xr-x  2 root root 4096 Sep 25 18:28 bin
drwxr-xr-x  2 root root 4096 Oct  7  2016 boot
drwxr-xr-x  2 root root 4096 Sep 25 18:28 dev
drwxr-xr-x 56 root root 4096 Sep 25 18:29 etc
drwxr-xr-x  2 root root 4096 Oct  7  2016 home
drwxr-xr-x  8 root root 4096 Sep 25 18:28 lib
drwxr-xr-x  5 root root 4096 Sep 25 18:28 lib64
drwxr-xr-x  2 root root 4096 Oct  7  2016 mnt
drwxr-xr-x  2 root root 4096 Oct  7  2016 opt
dr-xr-xr-x  2 root root 4096 Oct  7  2016 proc
drwx------  4 root root 4096 Sep 25 18:28 root
drwxr-xr-x  7 root root 4096 Sep 25 18:28 run
drwxr-xr-x  2 root root 4096 Sep 25 18:28 sbin
drwxr-xr-x  2 root root 4096 Oct  7  2016 selinux
drwxr-xr-x  4 root root 4096 Sep 25 18:27 srv
dr-xr-xr-x  2 root root 4096 Oct  7  2016 sys
drwxrwxrwt  7 root root 4096 Sep 25 18:28 tmp
drwxr-xr-x 13 root root 4096 Sep 25 18:27 usr
drwxr-xr-x 12 root root 4096 Sep 25 18:27 var
mytux:~ # 

 

Definition des LXC-Containers mit virt-manager

Wir starten nun virt-manager. Zunächst muss mal LXC auf unserem Host laufen – und wir brauchen eine Verbindung zum lokalen Socket. Dazu bemüht man den Menüpunkt “File >> Add Connection”:

Wir drücken dann auf den Button “Connect”. Nun legen wir unseren Container an über “File >> New Virtual Machine”.

Der Button “Forward” führt zu einem Dialog-Window, in dem wir direkt unser Verzeichnis für das Root-FS eintippen (wir befassen uns nicht mit Storage-Verwaltung).

Wieder “Forward” klicken; wir werden nun aufgefordert, RAM-Größe und die Anzahl der zu verwendenden CPUs festzulegen:

Schließlich geben wir dem Container noch einen Namen:

Die Netzwerk-Konfiguration unterlassen wir im Moment. Der Grund hierfür ist, dass wir ohne Installation weiterer Pakete im Container mit einer Netzwerkverbindung eh’ nicht viel anfangen können. Ich komme darauf aber im nächsten Blog-Beitrag zurück. Wir starten die Container-Installation – und uns wird fast umgehend eine Shell angeboten :

Zum Einloggen geben wir das für den chroot-Bereich definierte Passwort ein. So weit, so gut. Ein wenig Herumspielen zeigt sehr schnell: Die Installation ist wirklich recht minimal.

Ausblick

Im nächsten Blog-Beitrag

LXC-Betriebssystem-Container mittels virt-manager unter Opensuse Leap 42.2 – II

ergänze ich zunächst einige nützliche Pakete aus den Opensuse-Repositories. Dann zeige ich, wie man zu einer funktionierenden Netzwerk-Karte in Form eines veth-Devices kommt.

Links

Unterschiede LXC, LXD, Docker
https://unix.stackexchange.com/questions/254956/what-is-the-difference-between-docker-lxd-and-lxc
https://bobcares.com/blog/lxc-vs-docker/
http://www.zdnet.com/article/ubuntu-lxd-not-a-docker-replacement-a-docker-enhancement/

Zeichensatzvorgaben für MySQL und PHP – SET NAMES – und leere Strings durch htmlentities()

Gestern ist mir ein dummer Fehler passiert, dessen Analyse mich Zeit gekostet hat. Dabei erwies sich die Sache am Ende als trivial. Es ging letztlich um konsistente Zeichensatzeinstellungen für PHP und MySQL – allerdings mit einem mir bislang unbekannten Nebeneffekt.

Zeichensätze im Kontext von PHP und MySQL sind ein Thema vieler Foren- und Q&A-Artikel im Internet – und nicht immer trifft man auf zufriedenstellende Antworten. Ich hoffe, dieser Artikel trägt anhand eines Beispiels zu etwas mehr Klarheit bei.

Voraussetzungen und aufgetretener Fehler

Unsere hausinternen Apache- und Datenbank-Server laufen normalerweise vollständig unter UTF-8. Inklusive der eigenen Datenbank- und Tabellen-Kollationen unter MySQL- oder MariaDB-Systemen. Aber für Tests müssen wir immer mal wieder gezielt eine Kompatibilität zu den festgelegten Kollationen für MySQL-Banken/Tabellen auf Kundenservern herstellen. Meist kommt dann Latin1 (iso-8859-1) ins Spiel.

Gestern mussten wir für einen solchen Test Datensätze eines von uns entwickelten, php-basierten CMS von einem gehosteten MySQL-Kundenserver in unsere lokale Test-Datenbank übernehmen. Diese Datensätze beinhalteten viele Text-Strings mit deutschen Umlauten. Da es sich nur um wenige Records handelte, haben wir die Daten in diesem Fall mit Copy/Paste und mit Hilfe von Eingabefeldern unserer CMS-Verwaltungsoberfläche übernommen. Das CMS lief dabei unter einer lokalen UTF8-Standarddomäne. Unsere aktuellen CMS-Programme zeigten die fraglichen Textstrings denn auch korrekt in der Web-Oberfläche des CMS an.

Es gab aber andere zu untersuchende Punkte im Layout. Um die Unstimmigkeiten zu testen, haben wir zusätzlich eine separate Testdomäne angelegt und diverse PHP-Klassen vom Kundenserver (auf dem dortigen Versionsstand) in bestimmte Verzeichnisse des zugehörigen Web-Spaces auf unserem lokalen Server geladen. Ergebnis: 2 Domainen mit z.T. unterschiedlichen Versionsständen von PHP-Klassen.

Und dann passierte es:

Bei einem Aufruf der Webseiten in der Testdomäne verschwanden plötzlich fast alle Text-Strings aus der Web-Oberfläche.

Bilder und grundsätzlicher Aufbau der Webseiten bleiben jedoch erhalten. Ich war zunächst völlig verblüfft über diesen Effekt. Zumal der Einsatz unserer lokalen Versionen der gleichen PHP-Klassen kein Verschwinden der Textstrings zeitigte.

Die Analyse war nicht ohne, da ich zunächst nicht wusste, wonach ich zu suchen hatte. Man denkt da zunächst natürlich an Unterschiede im Programmcode selbst. Am Schluss entpuppten sich aber eine im wesentlichen unmodifizierte Klasse, die die Datenbankverbindung steuert, sowie eine Klasse, die die Strings aus Sicherheitsgründen nach unerlaubten Sequenzen filtert, als Kerne des Übels. Ausschlaggebend waren allerdings nicht Programmunterschiede sondern bestimmte Parameter-Setzungen sowie Server- und Zeichensatzeinstellungen. Aber der Reihe nach.

Zeichensatzeinstellungen in der Kommunikationskette zwischen einem PHP-Server und einer MySQL-Datenbank

In der Datenaustausch-Kette zwischen einer MySQL-Datenbank und PHP-Modulen auf einem Web-Server (und natürlich auch bei der Übersendung eines HTML-/XML- oder JSON-Outputs an Web-Clients) spielen verschiedene Zeichensatzeinstellungen eine Rolle. Einige davon können vom Entwickler beeinflusst werden. Andere wiederum nicht immer.

Für unseren Fall waren vor allem die nachfolgenden Punkte relevant; sie betreffen den Web-Server (mit PHP-Modul) und die MySQL-Datenbank:

  1. Zeichensätze (“Kollationen”) der MySQL-Datenbank und zugehöriger Datenbanktabellen.
  2. Zeichensatz-Vorgaben zur MySQL-Verbindung und zum Datentransfer von und zu (PHP-)Programmen, die mittels der SQL-Direktive “SET NAMES” vorgenommen wurden.
  3. Einstellungen für den Default-Character-Set des PHP-Apache-Moduls.
  4. Zeichensatzeinstellungen für die PHP-Funktion “htmlentities()”.

Zeichensätze in der Datenbank und die Direktive “SET NAMES”

Für die Kollation der relevanten MySQL-Datenbank und ihrer Tabellen war auf dem Kundenserver “latin1_german2_ci” gewählt worden. Unsere Einstellungen im lokalen Testsystem waren dazu auf Tabellenebene kompatibel. Die Kollation einer Datenbanktabelle bestimmt letztlich aber nur die interne Ablage der Daten in der Tabelle und nicht den Zeichensatz, unter dem z.B. per SQL ermittelte Resultsets an weiterverarbeitende Programme übermittelt werden.

Für letzteres sind andere Parameter verantwortlich, die man als Entwickler für eine spezifische Verbindung zur MySQL-Datenbank einstellen kann. Unter MySQL und der MariaDB nutzt man dafür etwa die SQL-Direktive “SET NAMES” (oder aber die Funktion mysqli_set_charset(); s.u.).

“SET NAMES” führt zur gleichzeitigen Festlegung dreier RDBMS-Parameter für die Behandlung einer spezifischen Datenbankverbindung. Diese Parameter sind: character_set_client, character_set_connection, character_set_results.

Siehe hierzu etwa
https://dev.mysql.com/doc/refman/5.7/en/set-names.html
und
https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_character_set_results.

Die Datenbankverbindung wird unter dem PHP/mysqli-Interface dabei über das Connection-Objekt

$dbi = mysqli_connect(….)

identifiziert. Im Wesentlichen legen die genannten Parameter Folgendes fest:

  • character_set_client: Zeichensatz, unter dem Daten, die vom Datenbank-Client zum RDBMS-Server transferiert werden, interpretiert werden.
  • character_set_connection: Handhabung bestimmterErsetzungen und Konversionen, u.a. von Numbers zu Strings.
  • character_set_results: Zeichensatz, unter dem Daten, die vom RDBMS zu Client-Programmen übermittelt werden interpretiert werden.

Der “Client” ist in unserem Fall natürlich ein PHP-Programm auf einem Apache-Server. Die Anwendung des “SET NAMES”-Statements durch ein PHP-Programm, z.B.

$sql_unames = “SET NAMES ‘utf8′”;
$this->dbi->query($sql_unames);
//dbi->Datenbank-Connection Object
//$this->dbi = mysqli_connect(….)

ist somit von zentraler Bedeutung für die Kommunikation zwischen einem PHP-Programm und einer MySQL/MariaDB! Solange eine hinreichende Konvertierung verwendeter Zeichen gewährleistet ist, muss der Zeichensatz, unter dem Resultsets zum PHP-Programm übermittelt werden, nicht zwingend mit dem der Datenbanktabellen selbst identisch sein.

Es sei darauf hingewiesen, dass es zu “SET NAMES” auch Alternativen gibt, die man im Rahmen des mysqli-Interfaces einsetzen kann und sollte (s.u.). Dass wir in einigen Programmen noch SET NAMES verwenden, hat lediglich historische Gründe. Die hier beschriebene Problematik gilt aber unabhängig vom genauen Werkzeug zur Einstellung der Zeichensatzparameter.

Konsistenz zu Zeichensatzvorgaben für PHP – Einstellungen in der php.ini

Das PHP-Programm muss in jedem Fall mit dem gewählten Zeichensatz für Resultsets adäquat umgehen können; s.u.. Der rechte Bereich der folgenden Skizze, die ich aus einem anderen Artikel dieses Blogs entliehen habe, verdeutlicht das für den Fall “SET NAMES ‘latin1′” :

Nun könnte man in seinen PHP-Programmen natürlich spezifische Umwandlungsfunktionen (u.a. iconv(), mb_convert_encoding(), utf8-encode(), utf8-decode) für die Zeichensatzkonvertierung aus- und eingehender Strings bemühen. Die linke Seite der Skizze liefert hierfür Beispiele (s. den dazu gehörigen Abschnitt weiter unten).

Wenn möglich, kann man aber auch einen Standardzeichensatz für die PHP-Verarbeitung von Strings vorgeben und sich darauf bzgl. der Datenbankinteraktion verlassen.

Entsprechende Einstellungen nimmt man in der Konfigurationsdatei
/etc/php7/apache2/php.ini
vor. Die relevanten Parameter sind dort:

; PHP's default character set is set to UTF-8.
; http://php.net/default-charset
default_charset = "UTF-8"

; PHP internal character encoding is set to empty.
; If empty, default_charset is used.
; http://php.net/internal-encoding
;internal_encoding =

; PHP input character encoding is set to empty.
; If empty, default_charset is used.
; http://php.net/input-encoding
;input_encoding =

; PHP output character encoding is set to empty.
; If empty, default_charset is used.
; mbstring or iconv output handler is used.
; See also output_buffer.
; http://php.net/output-encoding
;output_encoding =

Die hierfür gesetzten Werten sollte natürlich mit den Einstellungen für die Datenbankverbindung zusammenpassen.

Kundenvorgaben

Wir parametrieren in Kundenprojekten die PHP-Methoden für den Verbindungsaufbau zum Datenbankserver meist gemäß expliziter Kundenvorgaben. In unserem Fall hatten wir “SET NAMES ‘latin1′” gewählt. (Hinweis: Die Zeichensatz-Namen auf einem MySQL-Server enthalten grundsätzlich keine Trennzeichen; daher ist statt iso8859-1 “latin1” zu verwenden).

Der Grund dafür war, dass das Apache-PHP-Modul des (gehosteten) Kunden-Web-Servers auf “iso-8859-1” eingestellt war. Das bestätigte eine Überprüfung mit phpinfo(). Diese Einstellungen sollten wir gem. Kundenvorgabe nicht ändern, da auf dem Web-Server auch andere PHP-Programme als unsere eigenen laufen müssen.

Auf dem Kundenserver waren die Zeichensatzeinstellungen also konsistent.

Ursache unseres Problems und die Rolle von htmlentities()

Man ahnt es bereits: Durch das Kopieren der PHP-Klasse für die Steuerung von Datenbankverbindungen vom Kundenserver auf die Testdomäne unseres lokalen Web-Servers entstand dort u.a. eine Inkonsistenz zwischen der Zeichensatzbehandlung unter PHP (utf8!) und dem Zeichensatz für den Transfer von Resultsets aus der MySQL-Datenbank (latin1).

Diese Inkonsistenz kam jedoch noch nicht zum Tragen, als wir die Daten per Copy/Paste über lokale Web-Interfaces einer UTF8-Standard-Domäne in die Bank einbrachten.

Für unsere Testdomäne dagegen muss man jedoch u.a. eine fehlerhafte Konvertierung von deutschen Umlauten erwarten! Warum aber verschwanden die Text-Strings in Gänze aus den Web-Oberflächen?

Die Antwort lieferte schließlich eine von mir bislang zu wenig beachtete Eigenschaft der PHP-Funktion “htmlentities()”.

Unsere Web-Generatoren jagen Strings vor einer Web-Darstellung durch eine Reihe von Prüfroutinen, Transformatoren für erlaubte Zeichenfolgen und durch Filter (u.a. HTMLPurifier, aber auch eigene Filter). Dabei wird in einem Zwischenschritt (nach einer vorhergehenden Konvertierung erlaubter HTML-Zeichenfolgen) auch “htmlentities()” eingesetzt.

htmlentities() erlaubt selbst eine Vorgabe des “Character Sets” über einen Parameter. Ein Check zeigte: Dieser Parameter stand in der lokalen Testdomäne explizit auf “UTF-8”. Diese Einstellung betrifft jedoch eine Konvertierung in den gewünschten Ziel-Zeichensatz für den HTML-Output. Hier hatten wir kein Problem, da die
HTML-Header der Webseiten bzw. die vom Apache-Server generierten HTTP-Header tatsächlich auf UTF-8 ausgerichtet waren.

Allerdings sorgten schon die vorhergehenden Widersprüche zwischen dem Zeichensatz der Datenbank-Resultsets und dem Zeichensatz für die anschließende Behandlung von Strings durch PHP. Das hatte gravierende Folgen. Unter http://php.net/manual/de/function.htmlentities.php findet man nämlich folgenden Hinweis:

Rückgabewerte
Gibt die kodierte Zeichenkette zurück. Enthält der string eine in dem übergebenen encoding ungültige Code Unit Sequenz, wird eine leere Zeichenkette zurückgegeben, sofern weder das ENT_IGNORE noch das ENT_SUBSITUTE Flag gesetzt sind.

(Hervorhebung durch mich).

Das war des Rätsels Lösung:

Eine Inkonsistenz in der Zeichensatzbehandlung im Datenaustausch zwischen unserer MySQL-Datenbank und PHP führte zu nicht behandelbaren Zeichen bei der Anwendung von htmlentities() auf Strings – und diese Funktion produzierte dann gemäß ihrer Default-Einstellungen leere Strings.

Trivial – man muss es halt nur wissen! Ein Test mit

“SET NAMES ‘utf8′”

ließ denn alle verschwundenen Strings auch in unserer Testdomäne prompt wieder erscheinen!

Notwendige Checks vor der Anwendung von SET NAMES (oder von mysqli_set_charset())

Hat man die Kette der Zeichensatzsetzungen bzw. Zeichensatzbehandlung im Austausch zwischen PHP und einer MySQL-Datenbank erst einmal verstanden, so ist auch klar, wo man mit vorbeugenden Maßnahmen ansetzen kann. Solche Vorkehrungen sind – wie das Beispiel zeigt – vor allem dann notwendig, wenn man die Zeichensatz-Einstellungen für PHP nicht auf allen involvierten Web-Servern beeinflussen kann oder darf.

Bevor man “SET NAMES” (oder mysqli_set_charset(); s.u.) in einem PHP-Programm tatsächlich anwendet, sollte man die Setzung des “Default Character Sets” in der php.ini für den aktuellen Server explizit abfragen – mittels

ini_get(‘default_charset’);

– und dann mit der ebenfalls abfragbaren Kollation der Datenbanktabellen vergleichen. Das Ergebnis dieses Vergleichs kann man dann nach bestimmten Regeln behandeln:

Z.B. Warnhinweise bei (ernsthafter) Inkompatibilität ausgeben. Oder wenn man wirklich sicher ist, dass nur deutsche Umlaute zu potentiellen Problemen führen können: Wahl des zu den PHP-Einstellungen konsistenten Zeichensatzes für den Transfer von Resultsets aus der Datenbank. In unserem Fall also “utf8”.

Alternative: Ändern der php.ini-Vorgaben für den Zeichensatz durch und für das laufende PHP-Programm

Auf festgestellte Inkonsistenzen in den Zeichensatzeinstellungen kann man u.U. – nämlich wenn man die dafür nötigen Rechte besitzt – auch mit einer Modifikation der php.ini-Vorgaben für das laufende Programm reagieren. Dazu muss man natürlich die Funktion ini_set() bemühen; Bsp.:

ini_set( string ‘default_charset’, ‘ISO-8859-1’)

bemühen.

Empfohlener Weg zum Setzen des “Character Sets” für eine MySQL-Verbindung

Ich möchte explizit darauf hinweisen, dass es andere Möglichkeiten als die SQL-Direktive “SET NAMES” gibt, Character Sets für die Datenbankverbindung zu setzen. Das PHP-Manual empfiehlt explizit, die Funktion

mysqli_set_charset(mysqli $link , string $charset)

anstelle von SQL und “Set Names” zu verwenden. Siehe: http://php.net/manual/de/mysqli.set-charset.php

Zeichensätze und die Web-Client-Seite

Obwohl nicht Kernthema dieses Artikels werfen wir noch einen ergänzenden Blick auf den
Datenaustausch des PHP/Web-Servers mit Web-Clients (z.B. einem Browser). Die Zeichensatzthematik setzt sich natürlich auch auf dieser Kommunikationsstrecke fort; der linke Teil der obigen Skizze verdeutlicht das am Beispiel von Ajax/Ajaj-Programmen. Diese erfordern i.d.R. UTF-8 für einen ordnungsgemäßen Datenaustausch mit dem Web-Server.

Ist der Parameter “default_charset” in der php.ini-Datei aber auf “iso-8859-1” gesetzt, so muss man ein- und ausgehende Daten entsprechend konvertieren. Dafür eignen sich die Funktionen utf8_decode() für einlaufende POST-/GET-Daten aus Ajax-Programmen und utf8_encode() bei der Erzeugung des Ajax/Ajaj-Outputs in Richtung Web-Client.

Für reinen HTML-Output gilt analoges; dabei sind aber auch HTTP- und HTML-Header-Anweisungen für Character Sets zu setzen. Siehe hierzu:
http://www.html-info.eu/php/php-als-script-sprache/item/zeichensatz-latin1-oder-unicode-utf-8.html

Fazit

In unbeabsichtigter Weise kann htmlentities() plötzlich zu einer Testfunktion für die Konsistenz zwischen

  • der Zeichensatz-Einstellungen durch “SET NAMES” bzw. mysqli_set_charset() für die MySQL/MariaDB-Datenbankanbindung an ein PHP-Programm
  • und den Zeichensatzeinstellungen für das PHP-Modul selbst auf dem Webserver

werden – und in letzter Konsequenz zu leeren Strings auf Webseiten führen.

Es lohnt sich vor einem Einsatz von “SET NAMES” – oder besser mysqli_set_charset() – eigentlich immer, die Einstellungen in der “php.ini” bzgl. des “default_charset” und verwandter Parameter abzufragen und daraus angemessene Konsequenzen für den Aufbau der Datenbankverbindung zu ziehen.

Dies gilt vor allem dann, wenn man im Rahmen von Tests und Produktivierungen auf verschiedenen Servern arbeiten will oder muss – und dabei nicht alle Konfigurationsparameter der Server selbst beeinflussen kann und darf.

Neben dem Setzen von Server- und Verbindungsparametern kann man auf festgestellte oder vorgegebene Zeichensatzanforderungen oder Zeichensatzdiskrepanzen aber auch gezielt mit verschiedenen Funktionen reagieren, die PHP für eine Zeichensatz-Detektion und eine explizite Zeichensatzkonvertierung von Strings anbietet.

Veracrypt, SSHFS und sichere Datenhaltung auf gehosteten Servern – II – die Client-Seite …

Vor einigen Tagen habe ich abends ein paar begeisterte Linux-Anhänger getroffen; einige Gesprächspartner haben mir gegenüber die Sicherheit von Linux gegenüber MS Windows hervorhoben. Bei allem Respekt: IT-Sicherheit ist selten nur eine Frage des Betriebssystems. Wer meint, dass die Installation von Linux “Sicherheit” quasi garantieren würde, irrt gewaltig. Der Einsatz von Linux entbindet den Nutzer keineswegs

  • von einer detaillierten Risiko-Analyse/-Bewertung für potentiell bedrohte Daten oder Systeme
  • und von der Festlegung geeigneter Gegenmaßnahmen.

Dieser Artikel liefert hierfür ein relativ einfaches Beispiel:

Wir betrachten die Sicherheit der Inhalte von Dateien, die wir in verschlüsselten Datei-Containern verwahren. Wir öffnen solche Dateien bei Bedarf beispielsweise auf einem Laptop. Der Kontext wurde durch den vorhergehenden Artikel dieser Miniserie

Veracrypt, SSHFS und kryptierte Datenhaltung auf gehosteten Servern – I

vorgegeben: Dort hatte ich beschrieben, wie man verschlüsselte Veracrypt-Container auf gehosteten Servern zur Lagerung geheimzuhaltender Informationen einsetzen kann. Ich hatte bereits im letzten Artikel betont, dass die Dateien auf dem Server selbst nie in entschlüsselter Form verwendet werden sollten. Um diesen Punkt herum rankten sich dann zwei durchzuführende Mount-Prozesse, um die Container-Inhalte remote auf einem Client – z.B. auf einem mobilen Laptop – bequem nutzen zu können.

Der Client ist also das System, auf dem wir unsere geheimen Daten entschlüsseln und konkret nutzen. Ich gehe nachfolgend auf einige ausgewählte und prinzipielle Sicherheitsaspekte ein, die man dort im Auge behalten sollte. Die Liste der sicherheitsrelevanten Punkte ist damit keineswegs abgedeckt; aber unsere Diskussion wird zeigen, dass Sicherheit immer nur durch eine Kombination verschiedener Maßnahmen entsteht – unter Linux ebenso wie unter anderen Betriebssystemen. Linux erleichtert dem User dabei im Idealfall die Umsetzung der notwendigen Maßnahmen; der Einsatz von Linux allein ist für die Erfüllung von Geheimhaltungspflichten in der Regel aber unzureichend.

Ausgangssituation und Nutzerverhalten

Die zu schützenden Güter (im ISO 27001-Jargon: Assets) sind in unserem Beispiel geheimzuhaltende Informationen, welche in Dateiform vorliegen. Nehmen wir als extremes Beispiel etwa eine “odt”-Datei, in der der User etliche schwer zu merkende Passwörter hinterlegt hinterlegt hat. Ein ebenso brisantes Beispiel wäre etwa die Hinterlegung von Patientendaten, auf die eine Ärztin im mobilen Einsatz zugreifen möchte, in einem verschlüsselten Dateicontainer. Der Container kann remote vorliegen – etwa als Veracrypt-Archiv auf einem über das Internet erreichbaren Server. Verschlüsselte Container können aber auch lokal auf dem Laptop selbst angelegt sein.

Als Anwender/Anwenderin vertrauen wir dabei völlig auf die Verschlüsselung der Dateien. Unser Linux-Laptop für den mobilen Einsatz sei zeitgemäß ausgestattet und beinhalte u.a eine SSD. Als Anwender greifen wir auf unsere Dateien, wie im letzten Artikel beschrieben, bei Bedarf mit SSHFS und in jedem Fall mittels lokalet VC-Mounts auf dem Client zu.

Wenn erforderlich öffnen wir die Datei auf dem Laptop mit Hilfe einer passenden Anwendung (z.B. LibreOffice). Ggf. legen wir dort sogar eine Kopie der Datei an. Die Kopie lagern wir auf dem Laptop in einem der dortigen verschlüsselten VC-Containern. Geöffnete, entschlüsselte Dateien schließen wir nach Beendigung unserer Arbeit sofort wieder.

Als potentielles Bedrohungsszenario betrachten wir im Rahmen dieses Artikels primär den Verlust oder Diebstahl des Laptops. In diesem Fall ist ein physikalischer Zugriff
Unbefugter oder Krimineller auf das Gerät möglich. Frage:

Sind unsere Dateien, die dort in einem Veracrypt-Container lagern, sicher?

Schlüsselfragen

Zunächst ist festzuhalten, dass die letzte Frage im Kern falsch gestellt. Es geht ja letztlich nicht um die Dateien, sondern um deren Inhalte. Die richtige Frage wäre: Sind die Informationen, die (u.a.) in den Dateien hinterlegt wurden, im vorgegebenen Bedrohungsszenario vor dem Zugriff Unbefugter sicher?

Ich nehme die Antwort vorweg: Nein, sie sind es nicht – zumindest nicht ohne eine Reihe von Zusatzmaßnahmen. Das liegt an einer Vielzahl von potentiellen “Schwachstellen”, die sich bereits im Zuge der Entschlüsselung der Dateien auf unserem Laptop ergeben. Bereits für den Server hatten wir ja ansatzweise diskutiert, dass einmal entschlüsselte Datei-Inhalte trotz des späteren Schließens der Datei und trotz des Löschens von evtl. angelegten Kopien im Filesystem erhalten bleiben könnten. Schlüsselfragen sind also:

  • Was machen die Anwendungen, die wir zum Öffnen, Auslesen oder Bearbeiten der Dateien benutzt habe, mit den Datei-Inhalten? Wo verbleiben ggf. Reste entschlüsselter Daten nach Beendigung der Anwendung?
  • Was machen das Betriebssystem, das Desktop-System (KDE, Gnome, …) und die eingesetzten Filesysteme mit den Dateien? Wo verbleiben ggf. Reste entschlüsselter Information?
  • Was macht unsere Hardware mit unseren Dateien? Wo verbleiben ggf. Reste entschlüsselter Information?

Wir können das nachfolgend nur schlaglichtartig beleuchten. Das ist aber schon hinreichend, um unsere Ausgangsthese zu belegen.

Anwendungen als Schwachstelle

Zur Ansicht der (durch Veracrypt) entschlüsselten Datei verwenden wir geeignete Linux-Programme (Editoren, Office-Programme, IDEs etc.). Im Beispiel etwa LibreOffice [LO]. Nun legen die meisten solcher Programme temporär oder gar dauerhaft Kopien der Datei im Originalzustand oder nach vorgenommenen Änderungen an. Das Problem ist, dass diese Kopien i.d.R. nicht im Bereich des gemounteten VC-Datei-Containers landen, sondern in anderen vordefinierten Verzeichnissen, die auch ohne gemounteten Container erreichbar sind. Dort wird die Information aber ohne weitere Maßnahmen unverschlüsselt hinterlegt!

Für LO können dies einerseits Dateien mit Recovery-Informationen (in der Regel die vollständige Datei unter “/tmp” in Ordnern/Dateien mit dem Namen “luxxxxx..” sein. Diese temporären Dateien werden nach dem regulären Schließen der Dateien zwar wieder gelöscht; im Fall eines Crashes verbleiben sie aber. Es gibt zudem auch cache-artig hinterlegte Bilder, die langfristig im /tmp-Verzeichnis verbleiben. Hat man in LO gar die Option zum automatischen Anlegen von Backup-Dateien aktiviert, so finden sich letztere dauerhaft – also auch nach dem Schließen der jeweiligen Dokumente – im vorgesehenen Ordner wieder. Bei mir etwa unter dem Standardverzeichnis “~/.config/libreoffice/4-suse/user/backup/”. Natürlich unverschlüsselt!

Aber auch gezieltes Löschen und ein anschließendes Leeren des Papierkorbs nutzen nichts: Wer einmal versucht hat, gelöschte Dateien in einer Partition zu restaurieren, weiß, dass ein normales Löschen von Dateien überhaupt keine Sicherheit darstellt. Die Inhalte verbleiben lange auf den HDDs oder SSDs, bis sie wirklich überschrieben werden. Je mehr Platz (bei ext4: Inodes) ein Linux-Filesystem hat, desto größer die Wahrscheinlichkeit für ein Verbleiben der Original-Information in Festplatten-Blöcken. Die Chance, gelöschte Dateien oder Fragmente davon teilweise oder gar vollständig wiederherzustellen, ist somit relativ groß.

Aufpassen muss man natürlich auch auf Anwendungen, die mit internen oder extern Versionsverwaltungssystemen kooperieren. Solche Systeme können bei schreibenden Zugriffen u.U. eine automatische Versionierung vornehmen und eine
Serie von unterschiedlichen Dateizuständen im System hinterlegen.

Identifizierte Schwachstellen:
Anwendungen erzeugen typischerweise temporäre und dauerhafte Dateikopien – in unserem Fall unverschlüsselt in Festplatten-Bereichen außerhalb des kryptierten Datei-Containers. Auch nach einem gezielten Löschen solcher Informationen lassen sich die enthaltenen Informationen von Leuten mit physikalischem Zugriff auf die Laptop-Festplatten (HDD/SSD) rekonstruieren. Zu Gegenmaßnahmen s. weiter unten.

Betriebs- und Desktop-Systeme als Schwachstelle

Bei zu geringem RAM beginnt ein Betriebssystem u.U. zu pagen bzw. zu swappen. Auch in diesem Fall landet dekryptierte Information auf der Festplatte. Ein weiteres Übel können in unserem Kontext Desktop-Suchmaschinen (z.B. unter KDE) darstellen, die ggf. automatisch Verzeichnisse, Dateien und deren Inhalte sowie geöffnete Dateien nach Schlagworten scannen. Auch dann landet unverschlüsselte Information dauerhaft auf Festplatten – nämlich in Index-Verzeichnissen oder Datenbanken der Suchmaschinen.

Identifizierte Schwachstellen:
SWAP-Bereiche auf der Platte und Datenbanken von Desktop-Suchmaschinen sind Orte für das potentielle Auffinden entschlüsselter Information.

Eraser-Programme als Gegenmaßnahme?

Wenn man seine Anwendungen sehr gut kennt und weiß, wo überall Datei-Inhalte abgelegt werden, könnte man sich gegen ein Rekonstruieren gezielt gelöschter Information ggf. dadurch absichern, dass man zu Programmen greift, die einen Löschvorgang nicht nur als Freigabe entsprechender Knoten im Filesystem verstehen sondern entsprechende Blöcke tatsächlich mit Nullen oder zufälligen Zeichen überschreiben. Dieses Vorgehen setzt, wie gesagt, voraus, dass man vollständig überblicken muss, welche Anwendungen wo genau Informationen hinterlegen. Und man muss die notwendigen Löschvorgänge auch regelmäßig durchführen.

Nun gibt es zudem Programme, die alle freigegebenen Blöcke einer Festplatte identifizieren und überschreiben können. Auch solche Programme muss man aber regelmäßig laufen lassen – u.a. vor einem Shutdown des Systems. Selbst dann besteht aber immer noch die Gefahr, dass ein Laptop in einem StandBy- oder Hybernation-Zustand verloren geht oder gestohlen wird.

Die Vorstellung, ein gezieltes Löschen und Überschreiben von freigegebenen Speicherblöcken regelmäßig und in typischen Arbeitssituationen rechtzeitig sowie ohne Lücken durchführen zu können, halte ich insgesamt für illusorisch.

Verschlüsselte Partitionen als Lösung?

Das eigentliche Fazit aus den bisherigen Überlegungen ist: Im RAM entschlüsselte und geheimzuhaltende Informationen müssen auf dem Laptop immer in verschlüsselter Form auf die Festplatten gelangen. D.h.:

Alle Partitionen auf den Speichersystemen des Clients sollten vollständig verschlüsselt betrieben werden.

Das muss unabhängig vom Einsatz von verschlüsselten Datei-Containern realisiert werden! Egal, ob letztere lokal oder remote vorliegen. Entscheidend ist, dass das Entschlüsseln und Öffnen von Container-Dateien auf dem Client in unserem Szenario immer zu einer verschlüsselten Ablage von Information auf den Datenträgern des Clients führen muss – egal was die Anwendungen oder das Betriebssystem da genau ablegen wollen.

Systeme mit verschlüsselten Partitionen kann man unter Linux z.B. mit Hilfe LUKS oder eben auch Veracrypt einrichten. Verbunden ist damit typischerweise die Eingabe eines speziellen Codes beim Hochfahren des Systems oder beim Übergang aus einem Hybernation-Zustand in den Normalbetrieb. Erst dann wird die Entschlüsselung der Partitionsinhalte vorgenommen.

Was immer eine Anwendung danach tut: Die Informationen erreichen den Plattencontroller dann nur in verschlüsselter Form (hoffentlich). Wir haben das Prinzip der verschlüsselten VC-
Container, das wir bereits im letzten Artikel betrachtet hatten, inzwischen also auf die Festplatten und dortigen Partitionen des Clients ausgedehnt.

Die richtige Konsequenz aus unseren Sicherheitsanforderungen scheint also zu sein, auf den Clients nicht VC-Container für Datei-Kopien anzulegen, sondern gleich alle Platten bzw. deren Partitionen zu verschlüsseln. Nachdem wir dafür entwickelten Verfahren und Verschlüsselungsverfahren unter Linux vertrauen – sind unsere Informationen nun endlich sicher?

SSDs als Schwachstelle

Tja. Die Antwort auf die letzte Frage könnte eigentlich Ja lauten, wären da nicht sehr unangenehme Eigenschaften von modernen SSDs. Ich will hier gar nicht auf alle Details eingehen (davon verstehe ich zu wenig; s. jedoch die unten angegebenen Links). Aber der entscheidende Punkt scheint mir das Wear-Leveling zu sein, das praktisch alle modernen SSDs aufweisen. Da einzelne Speicherbausteine der SSD jederzeit ausfallen können, ist der tatsächliche Speicherplatz auf SSDs deutlich größer ausgelegt als der nutzbare Netto-Platz. Information, die auf reguläre Speicherblöcke geschrieben wird, wird redundant vervielfacht, um gegen einzelne Ausfälle von Speicherzellen gewappnet zu sein. Die Verwaltung übernimmt dabei der herstellerspezifische, in die SSD verbaute SSD-Controller in autonomer Weise. Der bemüht sich wiederum um einen gleichmäßige Auslastung aller vorhandenen Speicherzellen.

An die intern redundant hinterlegten Informationen kommt man mit Mitteln eines normalen Betriebssystems leider nicht heran. Wohl aber mit Spezialtools für das Auslesen von SSDs. Fazit:
Hast du oder hat dein System irgendwann mal unverschlüsselte Information auf die Platte geschrieben, so weißt du nicht, im welchen und wie vielen Speicherzellen diese Information auf der SSD noch vorhanden ist. Das wiederum bedeutet:

Eine SSD, die man unvorsichtigerweise irgendwann mal unverschlüsselt genutzt hat, enthält womöglich noch zu schützende Informationen in Bereichen, an die man ohne spezielle Mittel gar nicht herankommt. Wohl aber Experten, die physikalische Zugriff auf die SSD erhalten!

Nun gibt es jedoch auch Werkzeuge, die angeblich in der Lage sind, alle(!) Speicherzellen einer SSD zu überschreiben. Genau ein solches Tool müsste man bei einer SSD, die man schon benutzt hat und nun verschlüsseln will, vorab zum Einsatz bringen. Diese Tools sind aber mit Risiken verbunden, nach Auskunft mancher Tester im Internet auch nicht zuverlässig und stressen die SSD in jedem Fall.

Schlussfolgerungen

In unserem Szenario führt die bisherige Diskussion zu folgenden Punkten:

  1. Ein halbwegs sicherer Client erfordert gem. der obigen Anforderungen und Diskussion vollständig verschlüsselte Partitionen aller Festplatten und SSDs.
  2. Am besten verzichtet man auf Clients mit höchsten Sicherheitsanforderungen ganz auf den Einsatz von SSDs. Das ist übrigens die Empfehlung von Experten des BSI wie auch der Veracrypt-Entwickler.
  3. Wenn man SSDs dennoch benutzen will oder muss: An den Controller der SSD dürfen von Anfang an nur verschlüsselte Daten übermittelt werden. Die SSD ist also bereits im jungfräulichen Zustand (Fabrikzustand) ausschließlich mit verschlüsselten Partitionen zu versehen – und auch später dürfen nur verschlüsselte Partitionen hinzukommen.
  4. Zudem gibt es die Empfehlung, dass die (hinreichende lange) Passphrase oder notwendige Keyfiles für den Zugang zu den verschlüsselten Partitionen nicht geändert werden sollten. Der Grund dafür ist, dass ein sicheres Überschreiben der alten Header nicht gewährleistet werden kann. Wird also ein Password/Keyfile kompromittiert, ist es am besten eine neue SSD aufzusetzen, als auf der ursprünglichen SSD alte Passwörter zu ändern.

Ferner gilt: Wird ein Laptop
im nicht heruntergefahrenen Zustand verloren/entwendet, ist diese Situation ohne weitere Maßnahmen kaum besser als bei einem unverschlüsselten System: Es ist lediglich die Sperre des Bildschirmschoners zu überwinden.

Verschlüsselte virtuelle Maschinen als Maßnahme?

Es gibt noch einen weiteren Ansatz, sich zu schützen: Nämlich virtuelle Maschinen auf verschlüsselten Partitionen oder LVM-Volumes anzulegen – und die Datei-Container vom Server nur dort zu öffnen und zu bearbeiten. Dieser Ansatz, bei dem nicht alle Partitionen des Laptops verschlüsselt werden müssten, erscheint mir halbwegs sicher; ich bin aber auch dabei ein wenig skeptisch:

  • Zum einen muss die Abschottung von Host perfekt sein. Wer überblickt das aber schon?
  • Zum anderen darf man Verzeichnisse des Gastes/Hostes nicht im jeweils anderen System nutzen oder Dateien vom Gast zum Host kopieren oder Gastdateien mit Programmen des Hosts auslesen oder öffnen. Ein Mounten von Verzeichnissen des Gastes im Host verbietet sich daher. Aber auch umgekehrt muss man aufpassen.

Die geheimzuhaltenden Dateien dürfen nur und ausschließlich im Gastsystem und mit Programmen des virtuellen Gastsystems geöffnet werden. Sie dürfen nie unter dem Host-System selbst – also hier unter dem Laptop-Betriebssystem – geöffnet werden. Also ist von Samba und NFS zum gegenseitigen Mounten von Verzeichnissen zwischen virtualisiertem Gast und Host abzusehen! Zudem dürfen sich auf den Platten des (Virtualisierungs-) Hosts keine Informationen befinden, die Hinweise auf Passwörter und die Entschlüsselung der Partitionen für den Gast geben.

Setzt man trotzdem auf den Ansatz mit virtualisierten Gastsystemen, weil man auf dem Client nicht alles verschlüsseln will, so ist es wohl am einfachsten, für das Gastsystem ein verschlüsseltes LVM-Volume des Laptops heranzuziehen. Siehe die Links unten.

Fazit

Eine Sicherheit allein durch den Einsatz verschlüsselter Datei-Container gibt es in unserem Szenario nicht. Auch unter Linux nicht! Egal, ob die Container remote auf gehosteten Servern oder lokal vorhanden sind. Vielmehr ist für eine vollständige Verschlüsselung aller Partitionen des Client-Systems zu sorgen, wobei auf den Einsatz von SSDs im Idealfall verzichtet werden sollte.

Wir haben hier nur einen kleinen Teil von Sicherheitsaspekten betrachtet. Das, was wir diskutiert haben, hätte man in ähnlicher Weise aber auch für andere Betriebssysteme aufschreiben können. Sicherheit ist ein weit komplexeres Thema als nur eine Frage des Betriebssystems. Das gute an Linux ist jedoch, dass man hier alle notwendigen Tools zur Verbesserung der Sicherung geheimzuhaltender Information als Opensource Programme und kostenfrei vorfindet.

Also: Verschlüsselt eure geheimzuhaltenden Informationen. Nicht zuletzt, wenn ihr im Auftrag von Unternehmen unterwegs seid! Verlangt euren (Linux-) Admins entsprechende Lösungen ab. Aber hört nicht auf, Sicherheit zu hinterfragen. Sie ist immer relativ! Auch unter Linux!

Links – weitere Infos

Grundlegende Probleme der Verschlüsselung reiner Datei-Container-Verschlüsselung
https://www.computerwissen.de/ it-sicherheit/ web-security/ artikel/ verschluesselte-container-bieten-ihnen-keinen-zuverlaessigen-schutz.html
https://www.cs.washington.edu/ research/ security/ truecrypt.pdf

SSDs mit Wear-Leveling und Sicherheitsprobleme
http://www.zdnet.com/ article/ ssd-security-the-worst-of-all-worlds/
https://www.pcwelt.de/ ratgeber/ Datensicherheit-6581465.html
https://www.computerwoche.de/ a/ die-geheimen-schwaechen-der-ssd,2501912,4
https://www.computerbase.de/ forum/ showthread.php?t=1395284
https://forum.truecrypt.ch/ t/ ssds-not-appropriate-for-encryption/486
https://www.veracrypt.fr/ en/ Wear-Leveling.html
https://www.ghacks.net/ 2011/02/23/ solid-state-drives-and-encryption-a-no-go/
https://debianforum.de/ forum/ viewtopic.php?t=163274
http://news.softpedia.com/ news/ systemd-232-supports-veracrypt-encrypted-partitions-adds-60-improvements-509945.shtml

Performance SSDs – Encryption – TRIM
https://security.stackexchange.com/ questions/ 61089/ can-truecrypt-encrypt-ssds-without-performance-problems
https://www.heise.de/ ct/ hotline/ Linux-Verschluesselte-SSD-trimmen-2405875.html
http://mgessat.com/ verschluesselungssoftware-veracrypt-unabhaengige-ueberpruefung-abgeschlossen/
http://media-addicted.de/ ssd-and-truecrypt-durability-and-performance-issues/744/
http://asalor.blogspot.de/ 2011/08/ trim-dm-crypt-problems.html

Sicheres Löschen von Files auf konventionellen HDDs mit Wipe
[funktioniert nicht mit Flash-Speichern/SSDs]
https://superuser.com/ questions/ 19326/ how-to-wipe-free-disk-space-in-linux
https://tails.boum.org/ doc/ encryption_and_privacy/ secure_deletion/ index.en.html
https://wiki.ubuntuusers.de/ Daten_sicher_l%C3%B6schen/
https://www.cyberciti.biz/ tips/ linux-how-to-delete-file-securely.html

Vollständiges Löschen von SSDs?
https://wiki.ubuntuusers.de/ SSD/ Secure-Erase/
http://www.pc-magazin.de/ ratgeber/ festplatte-restlos-loeschen-sicher-saeubern-ssd-secure-erase-anleitung-hdd-3196204.html#
https://wiki.ubuntuusers.de/ SSD/ Secure-Erase/

Bereinigen des RAMs, Caches, SWAP spaces
https://www.tecmint.com/ clear-ram-memory-cache-buffer-and-swap-space-on-linux/

LVM und verschlüsselte Volumes
https://debianforum.de/ forum/ viewtopic.php? f=12&t=127012

KVM Gastsysteme auf verschlüsselten Volumes
https://www.ibm.com/ support/ knowledgecenter/ en/ linuxonibm/liaat/liaatkvmsecencrypt.htm
https://www.ibm.com/ support/ knowledgecenter/ linuxonibm/liaat/ liaatsecurity_pdf.pdf
https://security.stackexchange.com/ questions/ 20334/ memory-dumping-cause-for-concern-in-virtualization
https://forums.whonix.org /t/ how-useful-is-in-guest-encryption/1253
https://security.stackexchange.com/ questions/ 29535/ full-disk-encryption-within-a-vm-how-secure-is-it/29538#29538

Ein paar Aspekte von LUKS
https://wiki.ubuntuusers.de/ LUKS/ Containerdatei/
https://www.pcwelt.de/ ratgeber/ Ausgesperrt_ _Luks_mit_ Ersatzschluessel_ oeffnen-Linux-8664088.html
http://www.bocekm.com/ enlarge-luks-encrypted-logical-volume/

Backups mit LUKS und SSHFS
https://ruderich.org/ simon/ notes/ encrypted-remote-backups