How to add a new internal user to a rock

You can declare run-user in rockcraft.yaml to specify which user you want to run a rock’s services with. If you don’t specify a user, the services run as root by default. The run-user key only accepts a limited number of users, which could be a constraint for certain container applications.

Mandatory packages or slices

To add a new internal user or group to a rock, two packages or their respective slices are needed:

  • base-files: to create the base folders such as /root and /home,

  • base-passwd: to produce /etc/passwd and /etc/group files.

Creating the user and/or group

Invoking the useradd and groupadd commands can take place in a part’s build step. This can be done by writing those commands in override-build field. However, the changes made by those commands will be only applied on the build instance, and will not be available in the resulting rock. Hence, $CRAFT_PART_INSTALL should be passed as the root directory to those two commands.

A full example

The following rockcraft.yaml illustrates how to use the useradd command in the override-build field inside a part.

For an example of how to access the user inside the rock, here’s a simple Python web service. The script will return an HTTP response containing the user that is running the web service:

#!/usr/bin/python3.12

import getpass

print(
    f"""\
Content-Type: text/html

Serving by {getpass.getuser()} on port 8080\
"""
)

Then, add a service to rockcraft.yaml that runs the web service:

services:
  web-service:
    override: replace
    command: python3.12 -m http.server --cgi 8080
    startup: enabled
    user: myuser
    working-dir: /

In the override-build section of the part, let’s create a new user and a new group. We will also copy the Python script to a cgi-bin folder, and give it the execute permission:

    override-build: |
      craftctl default
      groupadd --root ${CRAFT_PART_INSTALL} mygroup
      useradd -d /home/myuser -s /bin/bash --root ${CRAFT_PART_INSTALL} -g mygroup myuser
      mkdir ${CRAFT_PART_INSTALL}/cgi-bin/
      cp serve_user.py ${CRAFT_PART_INSTALL}/cgi-bin/
      chmod +x ${CRAFT_PART_INSTALL}/cgi-bin/serve_user.py

The final rockcraft.yaml file will look like this:

name: internal-user
base: bare
build-base: [email protected]
version: 'latest'
summary: Add an internal user inside a rock
description: Add an internal user inside a rock, and run a service with it.
platforms:
  amd64:

services:
  web-service:
    override: replace
    command: python3.12 -m http.server --cgi 8080
    startup: enabled
    user: myuser
    working-dir: /

parts:
  my-part:
    plugin: nil
    source-type: local
    source: .
    stage-packages:
      - base-passwd_data
      - base-files_base
      - python3.12_standard
    override-build: |
      craftctl default
      groupadd --root ${CRAFT_PART_INSTALL} mygroup
      useradd -d /home/myuser -s /bin/bash --root ${CRAFT_PART_INSTALL} -g mygroup myuser
      mkdir ${CRAFT_PART_INSTALL}/cgi-bin/
      cp serve_user.py ${CRAFT_PART_INSTALL}/cgi-bin/
      chmod +x ${CRAFT_PART_INSTALL}/cgi-bin/serve_user.py

With the part and web service in place, build the rock:

rockcraft pack

Next, we will convert the rock from an OCI archive to a Docker image using Skopeo:

sudo rockcraft.skopeo --insecure-policy copy oci-archive:internal-user_latest_amd64.rock docker-daemon:internal-user:latest

We can now check which internal user is running the service by running the image container:

id=$(docker run -d -p 8080:8080 internal-user:latest --verbose)
sleep 5
curl -s http://127.0.0.1:8080/cgi-bin/serve_user.py | grep "Serving by myuser"
docker rm -f "$id"

The response should contain the new user name:

Serving by myuser on port 8080