Separate Dependencies From Implementation Using POSIX ACL Extended Attributes & Docker

The motivation

I am going to develop a responsive web-extension using angular. The web-extension is for a project that I am starting called nauci. I will talk about nauci more in future posts. In this post I intend to introduce the nauci base entry docker image and how I am using it to encapsulate the nauci project dependencies in a docker sub image, but not the source code. The plan is to bind my local nauci source directory to a docker container built with the nauci dependencies image, thereby separating dependency and tooling concerns from project implementation. If someone wants to contribute to nauci, they can simply grab the nauci dependencies image from docker hub once it is available, bind it to their clone of the nauci GitHub repository and contribute to the code base using using their preferred locally installed developer tools. I should mention that the nauci source is not on GitHub yet, but once it is ready to be pushed to the public repository I intend to license the project as free-software.

I personally like to install as few packages on my dev box as possible and the idea of helping others avoid the same is appealing. The nauci base entry image is publicly available on docker hub and everyone is welcome to use it as a base image to encapsulate their own project dependencies. From now on I will refer to the nauci base entry image simply as base image. This is my first significant post. I hope you enjoy it.

Prerequisites

If you would like to follow along then your host file system must support both POSIX Access Control Lists and Extended Attributes. It might be the case that other ACL models will also work thanks to ACL interoperability. However, ACL interoperability is not standardized and I have not tested its behavior. Extended attributes are necessary to attach ACL rules directly to files and directories making them available to docker via an attached volume. Not all file systems support ACLs, and the ones that do may not have the support enabled by default. This stack exchange explains several ways to test if your file system has POSIX ACL support enabled. Most Linux distributions have articles explaining how to install and configure POSIX ACL support for various file systems. i.e. An Access Control Lists article for Archlinux. I have not tried other ACL models or different access control systems such as Role Based Access Control (RBAC). If you know how to achieve the same with different prerequisites then please contact me with the details.

Preparing for collaboration

Before a team can work together on a project someone must decide how to facilitate collaboration. Docker can be used in two distinct ways to facilitate collaboration among members of a software development team. The first way requires developers to connect securely over the internet to a centrally hosted development environment encapsulated in a docker container. All of the project dependencies are installed in a single container and made accessible to each user. All of the chosen development tools, including the text editor, are also installed in the container. Typically, everyone on the team is expected to use exactly the same tools. One or more individuals must maintain the hosted environment, and of course, there is a cost associated with this. The advantage is that most team members do not need to take part in the maintenance and management of the development environment. They can simply sign in, do their work and sign out. Another advantage of the hosted approach is that the central development environment is available to collaborators no matter what computer they are on. As long as they have the right client software they can connect to the development environment from almost anywhere. While the advantages of a centrally hosted development environment are appealing, it is not the approach that I wish to take.

I am developing decentralized tools for the public good. I support decentralization and would like to collaborate with others in a decentralized way. I also feel strongly that developers should be granted as much choice as possible in the tools that they use to edit source code. I spend an awful lot of time with computers developing software (insert a ~sigh~ from my wife). Most of that time is spent in front of a text editor. Over the years, I have grown personally attached to a certain editor and would not want to impose restrictions that prevent other devs from using their own beloved coding editor. This brings us to the second approach to collaboration using docker. Instead of every developer connecting to a centrally hosted and maintained docker container, each developer will fetch the same image from Docker Hub and run their own container on their local development box.

The base image contains an entry point that accepts a number of optional parameters. The entry point executes a bash script called base_entry_init.sh which is now available on GitHub. I designed the base image making as few assumptions as possible. The entry point supports the creation of multiple users and so it is indeed possible to use it in a centralized hosted environment if that suits your needs. To simplify communication I am going to write about a single user environment, but realize that a multiple user environment is supported.

The central purpose of the base image is to allow a running container to bind project dependencies with a host user’s local source repository and custom development tools. In order for this all to work we must ensure that our host user is bind compatible with the guest user (inside the Docker container).

I am going to be talking about the user on the host machine and the user on the dev machine quite a lot throughout this article. From now on I am just going to refer to them as the host user and the guest user respectively.

Creating a new project

Suppose that you would like to create a beautiful snowflake of a project uniquely named my_project. You decide to make a source directory on your host machine.

mkdir -p ~/dev/my_project/src
cd ~/dev/my_project
ls -l
total 1
drwxr-xr-x 2 dustfinger dustfinger 2 Oct 11 10:41 src

I am showing you the long listing format of the project directory on purpose. I want to draw your attention to the default ownership and permissions of the src directory for reasons that will become evident in the next section titled Creating a bind compatible user.

A simplified look at permissions

Consider the permissions descriptor drwxr-xr-x. We can divide the permissions descriptor into four parts.

| type | User (u) | Group (g) | Other (o) |
|------+----------+-----------+-----------|
| d    | rwx      | r-x       | r-x       |

The type column contains a d for directory. Some other valid types are l for symbolic link, - for a regular file and c for a character file. The second column reflects the permissions for the owning user (u), the third for the owning group (g), and lastly for other (o) users not in the group. The values of the last three columns are called octets. Each of the three octets represent the permissions set for the (u), (g) or (o) respectively. The permissions are read (r), write (w) and execute (x).

When a process is executed it has associated with it real, effective, and saved user ids. Similarly, a process has real, effective, saved and supplementary group ids. The real user id (ruid) and the real group id (rguid) of a process are the same as the user id (uid) and primary group id (gid) of the user that executed the process. When a process requests to take an action on a resource, it is the effective user id (euid) and effective group id (egid) that the operating system uses to resolve the permissions granted to the process and determine if they meet permissions required to take the action.

Now consider the ownership descriptor dustfinger dustfinger. The first “dustfinger” is the name of the owning user and the second one is the name of the owning group. When a process tries to take an action on a resource, the operating system will lookup the process’s effective user. If the effective user has the same uid as the resources owning user then the (u) permissions will be granted to the process. If the effective user does not have the same uid as the owning user, then the operating system will look up the process’s effective group and attempt to match that against the resource’s owning group. Again, if the euid matches the owing group id then the resource’s (g) permissions are granted to the process. Finally, if the effective group does not match then the (o) permissions are granted to the process.

Creating a bind compatible user

I am going to exploit my creative right as a blogger to make up some terminology for the sake of simplifying communication. Or at least, I hope that is what this will achieve. When a host user and guest user have the same file and directory permissions to a bound volume, then we can say that the users are bind compatible with respect to the volume. Please feel free to correct me if there is already a term to describe this concept. The way I have chosen to achieve bind compatibility between the host and guest user is to create a group with the same name and group id (GID) on both the host and guest machines.

I would like to point out that I am aware of Linux name-spaces. In particular, the user namespace could theoretically be used to achieve bind compatibility. Unfortunately, docker’s support for container isolation with user namespaces is to limited to satisfy my requirements. According to the documentation only the first five sequential UIDS can be remapped.

UID 231072 is mapped within the name space (within the container, in this case) as UID 0 (root). UID 231073 is mapped as UID 1, and so forth.

I am also aware that docker supports multiple such mappings, but they would each be constrained by the above limitation.

With that out of the way, we are now going to create a group that both our host and guest user will be a member of for the sake of bind compatibility. I am going to call this group developer, but you are free to name it whatever you like as long as it is a valid unique group name. Under normal circumstances if you were creating a group you would let the system decide what the GID was going to be. Since we will be setting this GID ourselves we should first check what the range is for system allocated GIDs.

Look up the range for system allocated GID

As I mentioned in Literate Programming, sometimes I make use of regular expressions to filter the output for presentation purposes. In the next source block I am grepping /etc/logins.defs for the GID ranges. I could have just written grep -i gid /etc/login.defs, but I wanted to avoid pulling in comments that contain the word GID. By adding the optional E flag I was able to add the regular expression ^[^#].*gid to return lines that contain the word gid and do not begin with the comment symbol #.

grep -iE ^[^#].*gid /etc/login.defs
SYS_GID_MIN     101
SYS_GID_MAX     999
SUB_GID_MIN      100000
SUB_GID_MAX		600100000
SUB_GID_COUNT       65536

The system allocated GIDs range from 101-999. It is important that we do not create a new group within that range. Instead, we should assign our new group a GID within the range GID_MIN to GID_MAX. The constants starting with SUB are reserved for subordinate groups which we will not be using. Normally default user groups have IDs starting at 1000 and have the same value as their paired user ID (UID). In an attempt to avoid conflict with user’s default group IDs I decided to create my new group starting from GID 2000.

Ensure that your chosen group id has not already been assigned

First you should verify that you do not already have a group created with your chosen GID. The following command will return 0 if there are no assigned groups registered between 2000 and 2999.

getent group | cut -d: -f3 | grep -E ^2[[:digit:]]\{3\}$ | sort -n | wc -l
0

If the result is 0, as it is for me, then you can pick any GID in the 2000s range. If on the other hand you get a non-zero result, then you can simply remove the final pipe to wc and the list of already assigned group ids will be printed to the screen.

Create a new group named developer

Now that we know GID 2000 is available we can simply add a new group with the name developer and assign it the GID 2000. We will also want to add our host user to the new group and subsequently change the ownership of our project’s source directory so that the developer group has full ownership.

groupadd -g 2000 developer
usermod -aG developer dustfinger
chown :developer -R src

Make commands run in src directory as group owner

By default newly created files and directories inherit group ownership from the process that creates them. Also by default, a running process inherits its group ownership from the primary group of the user that executed it. When new files or directories are created inside of our source directory it is important that the group ownership is set to developer. Recall that in Create a new group named developer we changed the group ownership of the source directory to developer. Now we are going to set the setgid mode bit so that every process that is run under the src directory will inherit group ownership from the src directory rather than the user that executes it. Consequently, when a file or directory is created, the process that creates it will inherit group ownership from the src directory, and by transitivity the newly created file will inherit the same group ownership from the process. In the end what that means is that every file or directory that is created under our src directory, no matter what user initiates the process that creates it, will have its group ownership set to developer, which is what we want.

chmod -R 775 src
chmod 2775 src
ls -la
: total 10
: drwxr-xr-x  3 dustfinger dustfinger  3 Oct 11 10:41 .
: drwxr-xr-x 37 dustfinger dustfinger 43 Oct 11 10:41 ..
: drwxrwsr-x  2 dustfinger developer   2 Oct 11 10:41 src

You can see that there is an s in the execute bit position for group ownership. That is how you know setgid mode bit has actually been applied. You might be also interested in trying out the getfacl to view the effective access control list.

getfacl -e src/
: # file: src/
: # owner: dustfinger
: # group: developer
: # flags: -s-
: user::rwx
: group::rwx
: other::r-x
:

Set default permission for new content

When a directory has setgid applied by default new files or directories are created with read only permissions.

touch src/example
ls -la src/
: total 2
: drwxrwsr-x 2 dustfinger developer  3 Oct 11 11:38 .
: drwxr-xr-x 3 dustfinger dustfinger 3 Oct 11 10:41 ..
: -rw-r--r-- 1 dustfinger developer  0 Oct 11 11:38 example

You can see that group ownership was correctly set to developer, but group permissions [ - | rw- | r– | r– ] are read only. In order change the default behaviour so that group permissions are set to read | write | execute we must apply an appropriate access control list (ACL) to the src directory.

By the way, if your file system is zfs, as mine is, then you must ensure that xattr property is set to sa and the acltype is set to posixacl.

/sbin/zfs get aclinherit,acltype,xattr tank/root/home
NAME            PROPERTY    VALUE          SOURCE
tank/root/home  aclinherit  restricted     default
tank/root/home  acltype     posixacl       local
tank/root/home  xattr       sa             inherited from tank

Now all we need to do is use the setfacl command to set the default ACL for the developer group to apply rw permissions.

setfacl -Rdm g:developer:rw src
touch src/example2
ls -la src
: total 2
: drwxrwsr-x+ 2 dustfinger developer  4 Oct 11 11:40 .
: drwxr-xr-x  3 dustfinger dustfinger 3 Oct 11 10:41 ..
: -rw-r--r--  1 dustfinger developer  0 Oct 11 11:38 example
: -rw-rw-r--+ 1 dustfinger developer  0 Oct 11 11:40 example2

Let’s see how that has affected the effective rights of our access control list.

getfacl -e src/
# file: src/
# owner: dustfinger
# group: developer
# flags: -s-
user::rwx
group::rwx
other::r-x
default:user::rwx
default:group::rwx	#effective:rwx
default:group:developer:rw-	#effective:rw-
default:mask::rwx
default:other::r-x

Now both the host user and the guest user will have read and write access to all files in the source directory.

Create a project dependencies image

Suppose that our project requires us to develop a web application that is compatible with Firefox, but we don’t have Firefox on our host environment because we prefer to browse the web using Emacs Web Wowser (eww). Create a new Dockerfile in our src directory with the following contents

  FROM nauci/nauci_base_entry
  MAINTAINER dustfinger@nauci.org

  RUN apt-get -qqy install firefox-esr;
#+END_SRC docker

Don't forget to add docker ignore rules for anything that you do not want to be part of the image. Then build the Dockerfile in the usual way.

#+BEGIN_SRC sh :results output scalar :shebang "#!/bin/bash" :dir /home/dustfinger/dev/my_project
  docker build -t my_project_deps .

You should now have the following images in your local docker repository

docker images
: REPOSITORY               TAG       IMAGE ID            CREATED             SIZE
: my_project_deps          latest    fea6e0093b59        42 seconds ago      601MB
: nauci/nauci_base_entry   latest    82d57770d7cf        4 hours ago         215MB

Our last step is to run our dependency image interactively passing in our custom parameters to the base entry point. After running the image the shell will switch to an interactive shell inside the docker container. It is now time to set user passwords and any other administration tasks that your project may require.

# docker run -it -p 23:22 my_project_deps -s -n dustfinger -gusers,sudoer,video,plugdev
root@3c46128579fc:/# service ssh status
[[ ok  sshd is running.
root@3c46128579fc:/# passwd dustfinger
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully
root@3c46128579fc:/# ls -la ~dustfinger
total 24
drwxr-xr-x 4 dustfinger dustfinger    7 Oct 11 18:25 .
drwxr-xr-x 3 root       root          3 Oct 11 18:25 ..
-rw-r--r-- 1 dustfinger dustfinger  220 May 15  2017 .bash_logout
-rw-r--r-- 1 dustfinger dustfinger 3526 May 15  2017 .bashrc
-rw-r--r-- 1 dustfinger dustfinger  675 May 15  2017 .profile
drwxr-xr-x 2 dustfinger dustfinger    3 Oct 11 18:25 .ssh
drwxrwsr-x 2 dustfinger developer     2 Oct 11 18:25 dev
root@3c46128579fc:/# groups dustfinger
dustfinger : dustfinger video plugdev users developer usb
root@3c46128579fc:/# getent group developer
developer:x:2000:dustfinger
root@3c46128579fc:/# cat ~dustfinger/.ssh/config | grep -i forward
#   ForwardAgent no
ForwardX11 yes
ForwardX11Trusted yes
root@3c46128579fc:/# exit

Test your image

If you inspect the final output from the interactive session in Create a project dependencies image you will notice that dustfinger’s home directory has the same setgid applied to ~/dev/. If we commit this container and run the image again this time attaching my_project/src as a volume mapped to /home/dustfinger/src then both the host user and the guest user will have compatible rights to the shared source. That is exactly what I will demonstrate in my next post. For now, let’s test x-forwarding by seeing if we can launch Firefox and have its GUI forwarded from the container to the host.

First let’s see what our container is called.

docker ps -a
: CONTAINER ID  IMAGE               COMMAND                  CREATED             STATUS                      PORTS   NAMES
: 3c46128579fc  my_project_deps     "nauci_base_init.sh …"   17 minutes ago      Exited (0) 12 minutes ago           hungry_poitras

The status tells us that the container is not running, so let’s start it now.

docker start hungry_poitras
docker ps -a
: hungry_poitras
: CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                  PORTS                NAMES
: 3c46128579fc        my_project_deps     "nauci_base_init.sh …"   19 minutes ago      Up Less than a second   0.0.0.0:23->22/tcp   hungry_poitras

You can see that the local port 23 has been mapped to the container’s port 22. I am also running an ssh daemon from my dev machine so port 22 was not free on my host. You are free to map the ports to meet your own requirements. Since I have not done any network configuration in this blog post the container will have an ipv6 veth interface. Let’s take a look at that now.

ifconfig | grep -A2 -i veth
: vethe6655d7: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
:         inet6 fe80::2c7a:74ff:feaa:4961  prefixlen 64  scopeid 0x20<link>
:         ether 2e:7a:74:aa:49:61  txqueuelen 0  (Ethernet)

Now let’s ssh into our container with x-forwarding and launch Firefox. If you have any problems then turn on verbose output by adding -vv optional params.

ssh -Y -p 23 dustfinger@fe80::2c7a:74ff:feaa:4961%vethe6655d7 firefox

If it worked, then you should see a Firefox brows GUI popup in your host environment.

Conclusion

To be honest, when I started writing this post I new very little about docker. The experience I have gained has been my reward. I hope that by following along you have gained the same. If you notice something wrong please create an issue on the nauci base entry GitHub issues page. If you would like to share some tips for improvements, or just feel like commenting for any other reason, please email me directly, or post a comment on Show Hacker News. In my next post I will use these same techniques to rapidly develop and debug a web-extension, on both a mobile device and desktop, directly from the guest container. Then you will understand why the entry point creates a USB group.