Principle of Least Privilege
Learn how to implement minimal access controls to reduce your attack surface and limit potential damage.
The Principle of Least Privilege (PoLP) states that every user, process, or system should have only the minimum permissions necessary to perform their function. This simple concept is one of the most powerful security controls you can implement.
Why Least Privilege Matters
When permissions are overly broad:
- Larger attack surface: More paths for attackers to exploit
- Greater blast radius: Compromised accounts can do more damage
- Harder to audit: Too many permissions to track effectively
- Compliance issues: Violates regulatory requirements
Implementing Least Privilege
IAM Policies: Start with Deny
AWS IAM follows an implicit deny model: users have no permissions until explicitly granted. The key is to resist the temptation to grant broad permissions "just in case."
Good practice: Be specific about actions and resources. This policy allows reading from one specific S3 bucket only:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowS3ReadSpecificBucket",
"Effect": "Allow",
"Action": [
"s3:GetObject", // Read individual objects
"s3:ListBucket" // List bucket contents
// Note: No write, delete, or admin permissions
],
"Resource": [
"arn:aws:s3:::my-app-assets", // Bucket itself (for ListBucket)
"arn:aws:s3:::my-app-assets/*" // Objects in bucket (for GetObject)
]
}
]
}
Bad practice - this grants full S3 admin access to every bucket in the account:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:*", // Every S3 action including DeleteBucket!
"Resource": "*" // Every bucket in the account
// An attacker with this access could delete all your data
}
]
}
Real impact: In 2019, Capital One's breach exposed 100+ million records partly due to overly permissive IAM roles on EC2 instances.
Kubernetes RBAC
Kubernetes Role-Based Access Control (RBAC) lets you define precisely what actions each service account can perform. The principle is the same: start with nothing, add only what's needed.
Key concepts:
- Role: Defines permissions (what actions on what resources)
- RoleBinding: Grants a Role to a user/service account
- ClusterRole/ClusterRoleBinding: Same, but cluster-wide instead of namespace-scoped
# Role with minimal permissions
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: production # Role only applies in this namespace
rules:
- apiGroups: [""] # Core API group
resources: ["pods"] # Only pods, not secrets, configmaps, etc.
verbs: ["get", "list", "watch"] # Read-only, no create/delete
- apiGroups: [""]
resources: ["pods/log"] # Allow reading pod logs
verbs: ["get"] # But not streaming (no watch)
---
# Bind role to specific service account
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: production
subjects:
- kind: ServiceAccount
name: monitoring-sa
namespace: production
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
Database Permissions
Database credentials are a prime target for attackers. If your application uses a database account with full admin rights, a SQL injection vulnerability becomes catastrophic. Instead, create purpose-specific users with minimal permissions.
The principle: Your application probably only needs SELECT, INSERT, and UPDATE on specific tables. It almost never needs DROP, ALTER, or access to admin tables.
-- Create a read-only user for reporting
-- This user can only read data, never modify it
CREATE USER reporting_user WITH PASSWORD 'secure_password';
GRANT CONNECT ON DATABASE myapp TO reporting_user; -- Can connect
GRANT USAGE ON SCHEMA public TO reporting_user; -- Can see schema
GRANT SELECT ON ALL TABLES IN SCHEMA public TO reporting_user; -- Read-only
-- Create an app user with limited write access
-- Only the specific tables and operations the app needs
CREATE USER app_user WITH PASSWORD 'secure_password';
GRANT CONNECT ON DATABASE myapp TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE ON users, orders, products TO app_user;
-- Note: No DELETE permission (use soft deletes in app logic)
-- Note: No access to audit_logs, admin_settings tables
-- Note: No TRUNCATE, DROP, or ALTER permissions
Separate users for different functions: Reporting dashboards, background jobs, and the main application should each have their own database user with appropriate permissions.
Linux File Permissions
Linux file permissions are your first defense against privilege escalation. An attacker who compromises your application should not be able to read sensitive configuration files or modify executables.
Permission numbers explained:
- First digit: Owner permissions
- Second digit: Group permissions
- Third digit: Others permissions
- Values: 4=read, 2=write, 1=execute (add together)
# Application files - read/execute only
# 555 = r-xr-xr-x (everyone can read/execute, nobody can modify)
chmod 555 /opt/myapp/bin/*
# Configuration files - read only by app user
# 400 = r-------- (only owner can read, nobody else)
chmod 400 /opt/myapp/config/secrets.yml
chown appuser:appgroup /opt/myapp/config/secrets.yml
# Log directory - write only where needed
# 755 = rwxr-xr-x (owner full access, others can read/traverse)
chmod 755 /var/log/myapp
# 644 = rw-r--r-- (owner can write, others can only read)
chmod 644 /var/log/myapp/*.log
Common mistakes: Setting 777 on directories for "convenience", leaving secrets world-readable, or running applications as root.
Container Security Context
Containers inherit many default privileges that most applications don't need. A security context explicitly restricts what the container can do, limiting the damage if it's compromised.
Key settings explained:
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext: # Pod-level settings apply to all containers
runAsNonRoot: true # Kubernetes will reject containers trying to run as root
runAsUser: 1000 # Run as specific non-root UID
runAsGroup: 1000 # Run as specific GID
fsGroup: 1000 # Group for volume ownership
containers:
- name: app
image: myapp:latest
securityContext: # Container-specific settings
allowPrivilegeEscalation: false # Prevent sudo, setuid, etc.
readOnlyRootFilesystem: true # Can't write to container filesystem
capabilities:
drop: # Remove Linux capabilities
- ALL # Drop everything, then add back only what's needed
volumeMounts:
- name: tmp # Writable volume for temp files
mountPath: /tmp
- name: cache # Writable volume for cache
mountPath: /app/cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
Just-In-Time (JIT) Access
Instead of permanent permissions, grant temporary access when needed:
# Example: Temporary AWS credentials
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/AdminRole \
--role-session-name "emergency-access" \
--duration-seconds 3600
# Access expires after 1 hour
Implementing JIT with Teleport
# Teleport role with access requests
kind: role
version: v5
metadata:
name: developer
spec:
allow:
logins: ["{{internal.logins}}"]
node_labels:
env: ["dev", "staging"]
request:
roles: ["production-access"]
thresholds:
- approve: 1
deny: 1
Service Accounts and Workload Identity
Each application should have its own identity:
# Kubernetes: Dedicated service account per app
apiVersion: v1
kind: ServiceAccount
metadata:
name: payment-service
namespace: production
annotations:
# AWS IAM role for service account
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/PaymentServiceRole
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
spec:
template:
spec:
serviceAccountName: payment-service
automountServiceAccountToken: true
containers:
- name: payment
image: payment-service:latest
Auditing Permissions
Regularly review and revoke unnecessary permissions:
# AWS: Find unused IAM credentials
aws iam generate-credential-report
aws iam get-credential-report --output text --query Content | base64 -d
# AWS: Analyze IAM policies
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:user/developer \
--action-names s3:DeleteBucket
Kubernetes RBAC Audit
# List all cluster role bindings
kubectl get clusterrolebindings -o wide
# Find overly permissive bindings
kubectl get clusterrolebindings -o json | jq '
.items[] |
select(.roleRef.name == "cluster-admin") |
{name: .metadata.name, subjects: .subjects}
'
# Check what a service account can do
kubectl auth can-i --list --as=system:serviceaccount:default:my-sa
Common Least Privilege Mistakes
Mistake 1: Shared Service Accounts
# Bad: Multiple apps sharing one service account
serviceAccountName: shared-sa
# Good: Dedicated service account per app
serviceAccountName: order-service-sa
Mistake 2: Wildcard Permissions
// Bad: Wildcard actions
"Action": "ec2:*"
// Good: Specific actions
"Action": [
"ec2:DescribeInstances",
"ec2:StartInstances",
"ec2:StopInstances"
]
Mistake 3: Not Scoping Resources
// Bad: All resources
"Resource": "*"
// Good: Specific resources
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
]
Mistake 4: Permanent Admin Access
# Bad: Developer has permanent admin
aws iam attach-user-policy \
--user-name developer \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess
# Good: Require role assumption with MFA
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/AdminRole \
--serial-number arn:aws:iam::123456789012:mfa/developer \
--token-code 123456
Key Takeaways
- Start with zero permissions and add only what's needed
- Create dedicated identities for each application/service
- Use temporary credentials instead of permanent access
- Regularly audit and revoke unused permissions
- Scope permissions to specific resources, not wildcards
- Implement separation of duties for sensitive operations
Practice Exercise
Review permissions in your environment:
- List all IAM users/roles with admin-level access
- Identify service accounts shared between applications
- Find policies using wildcard (*) permissions
- Create a plan to reduce permissions without breaking functionality
Found an issue?