Container-safe Linux Users: A Complete Guide#
Introduction: Why Container Security Matters#
Imagine you’re building a house. You wouldn’t give every visitor the master key to every room, would you? The same principle applies to containers. When we run containers as the root user (the “master key” of Linux), we’re essentially giving our containerized applications unlimited power over the system. This creates a massive security vulnerability that attackers can exploit.
In this guide, we’ll explore how to create and use non-root users in containers, transforming your applications from security liabilities into well-protected services.
Understanding the Root Problem#
What is the Root User?#
The root user in Linux is like the administrator account on Windows – it has complete control over the system. It can:
- Read, write, and delete any file
- Install or remove software
- Modify system configurations
- Access any network port
- Kill any process
Why Running Containers as Root is Dangerous#
When a container runs as root, several risks emerge:
- Container Escape: If an attacker breaks out of the container, they have root access to the host system
- Privilege Escalation: Malicious code can perform system-level operations
- File System Access: Root can access and modify critical system files
- Network Exploitation: Root can bind to privileged ports (0-1023)
Let’s see this in action. Consider this vulnerable Dockerfile:
1
2
3
4
| # BAD EXAMPLE - Don't do this!
FROM ubuntu:20.04
COPY app.py /app/
CMD ["python3", "/app/app.py"]
|
When this container runs, it executes as root by default. If app.py
has a vulnerability, an attacker could potentially:
- Access the host’s file system
- Install malware
- Launch attacks on other systems
Creating Non-Root Users: The Safe Approach#
Method 1: Using adduser Command#
The adduser
command is the most straightforward way to create users. Here’s how it works:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| FROM ubuntu:20.04
# Create a new user called 'appuser'
RUN adduser --disabled-password --gecos '' appuser
# Copy your application
COPY app.py /app/
# Change ownership of the app directory to appuser
RUN chown -R appuser:appuser /app
# Switch to the non-root user
USER appuser
# Set working directory
WORKDIR /app
# Run the application
CMD ["python3", "app.py"]
|
Breaking down this Dockerfile:
adduser --disabled-password --gecos '' appuser
: Creates a user named ‘appuser’ without a password and without prompting for user informationchown -R appuser:appuser /app
: Changes the ownership of the /app
directory and all its contents to the appuser
USER appuser
: Switches the current user context to appuser
for all subsequent commands
Method 2: Using useradd Command (More Control)#
The useradd
command gives you more granular control:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| FROM ubuntu:20.04
# Create a system user with specific UID and GID
RUN useradd --create-home --shell /bin/bash --user-group --uid 1001 appuser
# Copy application files
COPY app.py /home/appuser/
# Set proper ownership
RUN chown -R appuser:appuser /home/appuser
# Switch to non-root user
USER appuser
# Set working directory
WORKDIR /home/appuser
CMD ["python3", "app.py"]
|
Command breakdown:
--create-home
: Creates a home directory for the user--shell /bin/bash
: Sets the default shell--user-group
: Creates a group with the same name as the user--uid 1001
: Assigns a specific user ID (useful for consistency across environments)
Method 3: Creating Users Without Home Directories (Minimal Approach)#
For applications that don’t need a home directory:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| FROM alpine:3.17
# Alpine uses adduser with different syntax
RUN adduser -D -s /bin/sh appuser
# Copy application
COPY app.py /app/
# Set ownership
RUN chown appuser:appuser /app
USER appuser
WORKDIR /app
CMD ["python3", "app.py"]
|
Alpine-specific notes:
-D
: Creates user without password-s /bin/sh
: Sets the shell (Alpine uses sh by default)
Practical Examples: Real-World Applications#
Example 1: Python Web Application#
Let’s create a secure Flask application:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| FROM python:3.9-slim
# Install dependencies first (better caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Create non-root user
RUN adduser --disabled-password --gecos '' flaskuser
# Create application directory
RUN mkdir /app && chown flaskuser:flaskuser /app
# Copy application code
COPY --chown=flaskuser:flaskuser . /app/
# Switch to non-root user
USER flaskuser
# Set working directory
WORKDIR /app
# Expose non-privileged port
EXPOSE 8080
# Run application
CMD ["python", "app.py"]
|
Key security features:
- Uses
--chown
flag in COPY to set ownership during copy - Exposes port 8080 (non-privileged) instead of 80
- Installs dependencies as root, then switches to non-root for runtime
Example 2: Node.js Application with Specific User ID#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| FROM node:16-alpine
# Create user with specific UID for consistency
RUN adduser -D -u 1001 nodeuser
# Set working directory
WORKDIR /app
# Copy package files and install dependencies as root
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# Copy application code and set ownership
COPY --chown=nodeuser:nodeuser . .
# Switch to non-root user
USER nodeuser
# Expose application port
EXPOSE 3000
CMD ["node", "server.js"]
|
Example 3: Multi-Stage Build with Security#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| # Build stage
FROM node:16-alpine AS builder
WORKDIR /build
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:16-alpine AS production
# Create non-root user
RUN adduser -D -u 1000 appuser
# Create app directory with proper permissions
RUN mkdir /app && chown appuser:appuser /app
# Copy built application from builder stage
COPY --from=builder --chown=appuser:appuser /build/dist /app/
COPY --from=builder --chown=appuser:appuser /build/package*.json /app/
# Install production dependencies
WORKDIR /app
RUN npm ci --only=production
# Switch to non-root user
USER appuser
EXPOSE 8000
CMD ["node", "index.js"]
|
Best Practices and Security Considerations#
1. Choose Appropriate User IDs#
1
2
3
4
| # Good: Use UID >= 1000 (standard for regular users)
RUN useradd --uid 1000 appuser
# Avoid: Using UID < 1000 (reserved for system users)
|
2. Handle File Permissions Correctly#
1
2
3
4
5
6
7
| # Create directories with proper permissions
RUN mkdir -p /app/data && \
chown -R appuser:appuser /app && \
chmod 755 /app
# Set specific permissions for sensitive files
RUN chmod 600 /app/config/secrets.conf
|
3. Use COPY –chown for Efficiency#
1
2
3
4
5
6
| # Efficient: Set ownership during copy
COPY --chown=appuser:appuser . /app/
# Less efficient: Copy then change ownership
COPY . /app/
RUN chown -R appuser:appuser /app/
|
4. Handle Temporary Directories#
1
2
3
4
5
6
7
8
| # Create and set permissions for temp directories
RUN mkdir -p /app/tmp && \
chown appuser:appuser /app/tmp && \
chmod 755 /app/tmp
USER appuser
# Your application can now write to /app/tmp
|
Common Pitfalls and Solutions#
Pitfall 1: Permission Denied Errors#
Problem: Your application fails with “Permission denied” errors.
Solution: Ensure the user owns the necessary directories:
1
2
3
| # Check what your application needs to write to
RUN mkdir -p /app/logs /app/uploads /app/cache && \
chown -R appuser:appuser /app
|
Pitfall 2: Can’t Bind to Privileged Ports#
Problem: Application fails to start on port 80 or 443.
Solution: Use non-privileged ports and handle routing externally:
1
2
3
4
| # Use port 8080 instead of 80
EXPOSE 8080
# Handle routing with reverse proxy (nginx, traefik, etc.)
|
Pitfall 3: File Ownership in Volumes#
Problem: Mounted volumes have wrong ownership.
Solution: Handle ownership in entrypoint script:
1
2
3
4
5
6
| # Create entrypoint script
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
USER appuser
ENTRYPOINT ["/entrypoint.sh"]
|
1
2
3
4
5
6
7
8
9
| #!/bin/bash
# entrypoint.sh
# Fix ownership of mounted volumes if needed
if [ -d "/app/data" ]; then
sudo chown -R appuser:appuser /app/data 2>/dev/null || true
fi
exec "$@"
|
Testing Your Secure Containers#
Verify User Context#
1
2
3
4
5
6
7
8
9
10
| # Build your image
docker build -t secure-app .
# Run and check the user
docker run --rm secure-app whoami
# Should output: appuser (not root)
# Check user ID
docker run --rm secure-app id
# Should show uid=1000(appuser) or similar
|
Test File Permissions#
1
2
3
4
5
6
7
| # Try to access restricted files
docker run --rm secure-app ls -la /root/
# Should show "Permission denied"
# Verify application files are accessible
docker run --rm secure-app ls -la /app/
# Should show files owned by appuser
|
Advanced Security Techniques#
Using Security Contexts in Kubernetes#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| apiVersion: apps/v1
kind: Deployment
metadata:
name: secure-app
spec:
template:
spec:
securityContext:
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: true
fsGroup: 1000
containers:
- name: app
image: secure-app:latest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
|
Read-Only Root Filesystem#
1
2
3
4
5
6
7
8
9
10
11
| FROM alpine:3.17
RUN adduser -D appuser && \
mkdir -p /app /tmp/app && \
chown -R appuser:appuser /app /tmp/app
USER appuser
WORKDIR /app
# Application will run with read-only root filesystem
# Temporary files go to /tmp/app
|
Conclusion#
Creating container-safe Linux users isn’t just about following best practices – it’s about building a robust defense against security threats. By implementing non-root users, you’re creating multiple layers of protection that significantly reduce your attack surface.
Remember these key principles:
- Never run production containers as root
- Create dedicated users for your applications
- Set appropriate file permissions
- Use non-privileged ports
- Test your security configurations
The extra effort you invest in container security today will save you from potential disasters tomorrow. Your applications will be more secure, your infrastructure more resilient, and your peace of mind greatly improved.
Start implementing these practices in your next container build, and make security a cornerstone of your development process, not an afterthought.