Skip to content

Security: run web container as non-root user and add no-new-privileges#1606

Open
Oykunle wants to merge 2 commits into
OpenEnergyDashboard:developmentfrom
Oykunle:test-oed-docker
Open

Security: run web container as non-root user and add no-new-privileges#1606
Oykunle wants to merge 2 commits into
OpenEnergyDashboard:developmentfrom
Oykunle:test-oed-docker

Conversation

@Oykunle
Copy link
Copy Markdown

@Oykunle Oykunle commented Apr 15, 2026

Description

This PR improves container security by addressing privilege escalation risks in the OED web container.

Previously, the web container ran as the root user, which increases the potential impact if the container is compromised. This change ensures the container runs as a non-root user and prevents privilege escalation.

Changes include:

  • Added a custom non-root user (appuser) and group (appgroup) with fixed UID/GID 10001
  • Updated the Dockerfile to run the application as this non-root user
  • Added no-new-privileges:true to the web service configuration

Context:

PR #1559 introduced similar protections for the cypress container but did not extend them to the web container due to concerns about development behavior. This PR extends that hardening effort to the web container.

Validation:

  • Verified the running web container executes as appuser (uid/gid 10001) using docker exec
  • Confirmed correct ownership and write permissions on /usr/src/app
  • Performed write test in /usr/src/app to confirm no permission issues
  • Confirmed application startup/install process works under the non-root user
  • Tested against upstream OED development branch

Type of change

  • Note merging this changes the database configuration.
  • This change requires a documentation update

Checklist

  • I have followed the OED pull request ideas
  • I have removed text in ( ) from the issue request
  • You acknowledge that every person contributing to this work has signed the OED Contributing License Agreement and each author is listed in the Description section

Limitations

  • This change focuses on privilege escalation mitigation and does not include all possible Docker hardening measures (e.g., read-only root filesystem or resource limits), as those may impact development workflows and require further evaluation.
  • Additional hardening improvements can be addressed in future work if desired.

@huss
Copy link
Copy Markdown
Member

huss commented Apr 16, 2026

Issue #1607 was opened to address the limitations listed in the PR description.

Copy link
Copy Markdown
Member

@huss huss left a comment

Choose a reason for hiding this comment

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

@Oykunle Thank you for working on this issue. I made one code comment to consider. I also wanted to ask a few overall questions:

  • I would like to ask how you tested the user within the Docker OED web container and the file owner. I'm not saying there is an issue but when I opened a terminal on that container it started as root and files were owned by the normal user (such as node). Thanks.
  • You applied these changes to the web container. Do you have thoughts on changes to the database container? Should they apply to the cypress container (though security on this is probably less important)?

I did some testing for production and non (developer) running and did not find any issues. I could not test a new clone because of the startup issue with OED development. Once that is resolved I should be able to finish all testing.

Let me know if anything is not clear or you have questions/thoughts.

Comment thread containers/web/Dockerfile
@Oykunle
Copy link
Copy Markdown
Author

Oykunle commented Apr 17, 2026

@huss,

For verifying the user and file ownership, I tested it directly using: docker compose run --rm --entrypoint sh web -lc 'whoami && id'
That returned appuser with uid/gid 10001, confirming the container is running as the non-root user.

I also checked the app directory: docker compose run --rm --entrypoint sh web -lc 'ls -ld /usr/src/app'

and confirmed it’s owned by appuser:appgroup. I also did a quick write test (touch / rm) in that directory to make sure there were no permission issues with the mounted volume.

For the terminal showing root, I suspect that depends on how the shell/session is opened. In my checks, the runtime execution context (via docker compose run ...) is using the non-root user.

For the database container, I didn’t apply the same changes here since it has different persistence and filesystem requirements, and I didn’t want to expand the scope of this PR without validating that more carefully. I’d be open to exploring that separately if we want to extend the hardening there.

For the cypress container, PR #1559 already introduced similar protections, so I treated the web container as the main missing piece.

Also good to hear your testing didn’t surface any issues. Let me know if there’s anything else you’d like me to check or validate.

@huss
Copy link
Copy Markdown
Member

huss commented Apr 20, 2026

@Oykunle I've spent a little time trying to figure out the user and owner of files.

  • I tried docker exec -it oed-web-1 /bin/bash and it ran as root.
  • I tried docker exec -it -u appuser oed-web-1 /bin/bash but it failed because there is no entry in the passwd file. I thought this might because of how you set up the user to stop login.
  • I tried your command of docker compose run --rm --entrypoint sh web -lc 'whoami && id' and it says root with ids of 0.
  • I tried your command of docker compose run --rm --entrypoint sh web -lc 'ls -ld /usr/src/app and it says node owns the directory.

Note I did all of these after removing the current oed docker containers (docker compose down) so they were clearly recreated. Thus, I'm seeing a different result than you and I'm not sure why. I wish I could try a clean install to see if that gives what you see but cannot due to the install issues. I would like to figure this out to understand and also see if this means there may be steps needed for a current site to get the new security level. It also means my previous testing may not have actually run against the new user setup.

I welcome any and all thoughts at this point.

@Oykunle
Copy link
Copy Markdown
Author

Oykunle commented Apr 20, 2026

@huss

Thanks again for taking the time to check this.

After your comment, I re-tested from a completely clean state and was initially able to reproduce the same behavior you saw (container running as root). It turned out the Dockerfile in this branch was not in the expected state at that point, so the non-root user setup was not actually being applied.

I’ve now restored the intended Dockerfile changes and re-verified everything from scratch:

docker compose down -v
docker compose build --no-cache web
docker compose up -d web

Then checked the running container directly:

docker exec -it $(docker ps -qf "name=web") whoami
docker exec -it $(docker ps -qf "name=web") id
docker exec -it $(docker ps -qf "name=web") ls -ld /usr/src/app

This now returns:
• appuser (uid/gid 10001)
• /usr/src/app owned by appuser:appgroup

So the web container is now correctly running as the non-root user, and file ownership is aligned with that user. I also verified write access in /usr/src/app to ensure there are no permission issues.

Regarding the earlier observation where some shells appeared as root, the difference seems to come from how the shell is opened. In some cases, docker exec may default to root, but the container’s runtime user defined in the Dockerfile (USER appuser) is now correctly applied.

Let me know if you’d like me to check anything else.

@huss
Copy link
Copy Markdown
Member

huss commented Apr 26, 2026

@Oykunle Thank you for the additional information. My experience was that until I did:

docker compose down -v
docker compose build --no-cache web
docker compose up -d web

It stayed as root but afterward it was the appuser. Given this can you elaborate on these:

  • Do you know which are the critical ones and are all three needed?
  • I think this may mean that all developers and sites need to do this for it to take effect. Is that correct?

If it is needed then OED needs to work out how an upgrade for this change would be put into effect for existing sites. Developers might be less critical but that needs some thinking too. Any thoughts?

@Oykunle
Copy link
Copy Markdown
Author

Oykunle commented Apr 28, 2026

@Oykunle Thank you for the additional information. My experience was that until I did:

docker compose down -v
docker compose build --no-cache web
docker compose up -d web

It stayed as root but afterward it was the appuser. Given this can you elaborate on these:

  • Do you know which are the critical ones and are all three needed?
  • I think this may mean that all developers and sites need to do this for it to take effect. Is that correct?

If it is needed then OED needs to work out how an upgrade for this change would be put into effect for existing sites. Developers might be less critical but that needs some thinking too. Any thoughts?

@huss

Thanks for checking this out.

From what I saw while testing, the main thing that makes the difference is rebuilding the web image:

docker compose build --no-cache web

That’s what actually picks up the updated Dockerfile with USER appuser. The docker compose down -v step just helps make sure everything is fully cleaned up (especially if something old is being reused), and docker compose up -d web is just to bring the container back up.

So all three together are a good, safe way to do it, but the rebuild is really the important part.

I also agree with your point, existing setups will keep running as root unless they rebuild the image. So this change won’t take effect automatically.

For upgrades, I think the best approach would be to clearly document this (maybe in release notes) with the exact commands to run. Developers might not be as critical, but they’d still need to rebuild locally if they want to see the updated behavior.

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