Skip to content

Add conditional root check to datanode entrypoint#306

Open
monrax wants to merge 1 commit into
7.2from
chore/conditional-root-check-entrypoint
Open

Add conditional root check to datanode entrypoint#306
monrax wants to merge 1 commit into
7.2from
chore/conditional-root-check-entrypoint

Conversation

@monrax

@monrax monrax commented Jul 9, 2026

Copy link
Copy Markdown

Summary

Add support for running the datanode container as non-root while maintaining backward compatibility with root execution.

This change allows the datanode to work in restricted environments that don't allow root containers, e.g. security-hardened Kubernetes clusters (Pod Security Standards, policies, OpenShift, etc.)

Changes

Add branching based on the user running the entrypoint:

  • When running as root: Uses install -d with proper ownership, performs chown, and drops privileges via setpriv (traditional behavior)
  • When running as non-root: Creates directories with mkdir, sets chmod 0700 (user can chmod its own directories), and executes datanode directly

Why is this necessary?

In order to make the official Graylog Helm chart ready for security-hardened production environments, DataNode must be able to run as non root. This is possible in Kubernetes as setting up the right securityContext makes the chown and setpriv operations here unnecessary. A flag must be passed explicitly when running in this mode (as an env var: GDN_RUN_AS_NONROOT=true), so it doesn't occur by accident.

Impact

  • Enables deployment in restricted environments without requiring a root init container
  • Maintains same security posture (0700 permissions) in both paths
  • Works seamlessly with Kubernetes fsGroup for volume ownership
  • Complements graylog-helm security hardening efforts

Related

Graylog2/graylog-helm#100

Testing

  • Tested with root execution (7.1 branch)
  • Tested with non-root execution (uid 1100)
  • Verified directory permissions are correct (0700)
  • Verified datanode starts successfully in both modes

Notes for Reviewers

  • The commit history must be preserved - please use the rebase-merge or standard merge option instead of squash-merge
  • Sync up with the author before merging

@bernd bernd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason to keep the possibility to run the container as root when the entrypoint switches to the unprivileged user anyways? Our server containers also use USER in Dockerfile to run as unprivileged user.

Also: please open PRs against the latest branch (7.2) and then backport once merged.

@monrax monrax changed the base branch from 7.1 to 7.2 July 10, 2026 09:36
This allows the container to start as non-root (e.g., with fsGroup) while
maintaining backward compatibility with root execution.

When running as root:
- Uses install -d with proper ownership and 0700 permissions
- Performs chown on the data directory
- Uses setpriv to drop privileges to the configured user

When running as non-root:
- Creates directories with mkdir (owned by current user)
- Sets 0700 permissions via chmod (user can chmod directories it owns)
- Executes datanode directly with no privilege operations
- Assumes fsGroup handles volume ownership in Kubernetes

This enables deployment in restricted environments (Pod Security Standards,
policies, etc.) that don't allow root containers, while maintaining security
posture and backward compatibility.

Aligns with graylog-helm PR #100 security hardening approach.
@monrax monrax force-pushed the chore/conditional-root-check-entrypoint branch from ba593e9 to dcc3463 Compare July 10, 2026 10:13
@monrax

monrax commented Jul 10, 2026

Copy link
Copy Markdown
Author

thanks for the quick reply @bernd !

What's the reason to keep the possibility to run the container as root when the entrypoint switches to the unprivileged user anyways? Our server containers also use USER in Dockerfile to run as unprivileged user.

Well, the container is already running as root in its current form. It is the datanode process that runs with an unprivileged user via exec setpriv. You can check this with a simple

docker exec -it datanode -- id

which returns

uid=0(root) gid=0(root) groups=0(root)

for any graylog-datanode container that is running as of today. Using USER to flip the datanode to non-root the way the server containers do it won't work here, because today's datanode entrypoint requires root to chown the data dir and setpriv, as well as running install with -o, -g, and -m options on the opensearch dirs:

install -d -o "$GDN_USER" -g "$GDN_GROUP" -m 0700 \

chown -R "$GDN_USER":"$GDN_GROUP" "$GRAYLOG_DATANODE_DATA_DIR"

exec setpriv --reuid="$GDN_USER" --regid="$GDN_GROUP" --init-groups \

All non-optional operations that require root, all in the current entrypoint. Any attempts to run a graylog-datanode container as non-root today makes it crash immediately.

Unless I'm missing something, there's no setpriv in the server docker-entypoint.sh script.

What I'm introducing with this PR is actually a way to run the container itself as non-root, in environments that let you set the user/group/permissions/capabilites beforehand, like on Kubernetes.

if [ "$(id -u)" = "0" ]; then

# run exactly in the same way as it is running today. I.e. root is required for chown and setpriv

else

# NEW branch: run as a non root user; no setpriv required
# and chmod works because the same user running datanode owns the opensearch dirs

fi

Also: please open PRs against the latest branch (7.2) and then backport once merged.

done! Apologies, since 7.1 is still the default branch in this repo I thought of it as the main/master branch. I have rebased on 7.2

@bernd

bernd commented Jul 10, 2026

Copy link
Copy Markdown
Member

for any graylog-datanode container that is running as of today. Using USER to flip the datanode to non-root the way the server containers do it won't work here, because today's datanode entrypoint requires root to chown the data dir and setpriv, as well as running install with -o, -g, and -m options on the opensearch dirs:

But if we chown the directories in the Dockerfile (like we do in server), we don't need to run it in the entrypoint. And existing instances already have the correct permissions, no?

@monrax

monrax commented Jul 10, 2026

Copy link
Copy Markdown
Author

I believe we need to consider a couple things before using the the server pattern of chown at build time + setting a USER layer (+ not doing any privilege manipulation in the entrypoint script):

  1. Whenever a volume is mounted on ${GRAYLOG_HOME}/data, the mount shadows whatever permissions were baked in the Dockerfile at build time. The server can run non-root on a new mounted volume not because of the Dockerfile chown + USER, but because the entrypoint script fixes ownership at runtime conditionally:

if [[ "$(stat --format='%U:%G' $dir)" != 'graylog:graylog' ]] && [[ -w "$dir" ]]; then
chown -R graylog:graylog "$dir" || echo "Warning can not change owner to graylog:graylog"
fi

If the mounted volume doesn't have write permissions it skips the chown entirely, but if the mounted volume does have write permissions, then it depends on who the runtime user is: if it's root (or another account with CAP_CHOWN) it chowns fine without any warnings, but if it's not thenchown fails, a warning is printed, and the main process executes anyway. In docker, because in the Dockerfile the COPY --chown step runs before the VOLUME layer, a fresh volume is initialized with the expected graylog:graylog ownership set in the image, even at runtime.

COPY --chown=${GRAYLOG_UID}:${GRAYLOG_GID} --from=graylog-downloader ${GRAYLOG_HOME} ${GRAYLOG_HOME}

USER ${GRAYLOG_USER}
VOLUME ${GRAYLOG_HOME}/data
ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"]
CMD ["server"]

In Kubernetes, write permissions are added to the dir using fsGroup (so the runAs user running the entrypoint can write to the volume, as is the expectation), which makes it attempt the chown and fail with the Warning: ... but carry on. Because the dir is writeable by the same group the runtime user belongs to, then it carries on without a problem.

For datanode the story is different as it does an unconditional, hard-failing chown -R + setpriv:

# Make sure the data node can write to the data dir
chown -R "$GDN_USER":"$GDN_GROUP" "$GRAYLOG_DATANODE_DATA_DIR"
# Starting the data node with dropped privileges
exec setpriv --reuid="$GDN_USER" --regid="$GDN_GROUP" --init-groups \

which is why running it non-root today crashes immediately with:

chown: changing ownership of '/var/lib/graylog-datanode': Operation not permitted

So, we need a conditional chown just like in the server, which is what this PR introduces while maintaining the current behavior identical. Otherwise, we'd be making the non-root path I'm introducing here as the only path and just add a chown that can fail with a warning guarded by a [ -w "$GRAYLOG_DATANODE_DATA_DIR" ] check before it + the USER layer in the Dockerfile (just like with server).

  1. The datanode currently uses 0700 on the Opensearch dirs. So, even if we went with the server approach of chown if writable, otherwise warn and carry on, it wouldn't be enough: the mounted volume is left group-accessible, and it seems the config/data/logs dirs need to end up 0700 and owned by the runtime user (the install -d -o "$GDN_USER" -g "$GDN_GROUP" -m 0700 does this in the existing root path). The non-root path introduced in this PR makes sure to mkdir those subdirs and chmod 0700 them (which works because the non-root user owns the dirs it just created). Not sure if this is a hard requirement by Opensearch, but it is what's currently working though.

And existing instances already have the correct permissions, no?

If the datanode volume was previously initialized by today's (root) entrypoint, then yes. The contents have already been chowned to graylog:graylog, so an upgrade would find the ownership it expects. But a fresh volume still comes up root-owned, so new installs in security-hardened environments need the runtime fix regardless. Also, even if ownership is already correct, the container still can't run today's unconditional chown -R + setpriv as non-root becuase those hard-fail before it ever gets to the datanode process, regardless of any existing permissions.


Correct me if I'm wrong, but it seems to me we're actually agreeing on the goal (running datanode as non-root). The implementation choice is just how the entrypoint stops requiring root. Happy to switch this PR from the id -u branching to the server pattern (build-time USER graylog + a setup()-style runtime chown) for consistency sake (it would require modifying one more file and change current default entrypoint behavior, though)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants