0

Principle of least privilege — only grant access that is actually needed

Beginner5 min read·eng-19-011
interviewsecurity

Concept

Principle of Least Privilege (PoLP) — grant each component, user, or service ONLY the minimum permissions needed to perform its function. Nothing more.

Why PoLP matters:

  • Limits blast radius: If an account is compromised, attackers can only do what that account was allowed to do.
  • Reduces attack surface: Fewer permissions = fewer ways to be exploited.
  • Contains mistakes: A developer bug that performs an unintended action is constrained by permissions.

Applied at different layers:

  • Database users: App's DB user has SELECT, INSERT, UPDATE, DELETE — not DROP TABLE, CREATE USER, GRANT.
  • File system: PHP-FPM runs as www-data with read access to app, write access only to storage/ and bootstrap/cache/.
  • API keys / service tokens: Scope tokens to only what's needed. A cron job that sends emails doesn't need permission to delete users.
  • IAM roles (AWS): EC2 instance role has s3:GetObject and s3:PutObject for your specific bucket — not s3:* on all buckets.
  • Laravel Gates: Admin gate requires explicit is_admin flag — not "anyone logged in is an admin."
  • Sanctum token scopes: Mobile app token can read-profile — can't delete-account.

PoLP examples in PHP:

  • Database credentials: GRANT SELECT, INSERT, UPDATE ON app_db.* TO 'app_user'@'%' — not GRANT ALL.
  • Queue workers: Only read from queue, dispatch events — not access user auth endpoints.
  • Read-only reporting queries: Use a read-only DB replica connection with a read-only user.

Code Example

php
<?php
// SANCTUM TOKEN SCOPES — limit what a token can do
$token = $user->createToken('mobile-app', ['read-profile', 'create-order']);
// This token CANNOT: delete orders, read payment info, change password

Route::middleware(['auth:sanctum', 'abilities:read-profile'])->get('/profile', fn() => auth()->user());
Route::middleware(['auth:sanctum', 'abilities:create-order'])->post('/orders', [OrderController::class, 'store']);
Route::middleware(['auth:sanctum', 'abilities:delete-order'])->delete('/orders/{order}', ...);
// DELETE returns 403 — mobile-app token doesn't have delete-order ability

// DATABASE — least privilege
// MySQL: grant only what's needed
// GRANT SELECT, INSERT, UPDATE, DELETE ON `zira`.* TO 'zira_app'@'%';
// NOT: GRANT ALL PRIVILEGES ON *.* TO 'zira_app'@'%'

// For migrations (run separately, not with app user):
// GRANT ALTER, CREATE, DROP, INDEX ON `zira`.* TO 'zira_migrations'@'localhost';

// LARAVEL POLICIES — explicit per-action authorization
class OrderPolicy
{
    public function view(User $user, Order $order): bool
    {
        return $user->id === $order->user_id; // only owner
    }

    public function delete(User $user, Order $order): bool
    {
        return $user->isAdmin(); // only admins can delete
        // NOT: return true; (everyone) or return auth()->check(); (all authenticated)
    }

    public function refund(User $user, Order $order): bool
    {
        return $user->hasRole('billing'); // only billing role
    }
}

// AWS IAM — least privilege role for Laravel app on EC2
// Role policy (only S3 access for the specific bucket):
/*
{
    "Effect": "Allow",
    "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
    "Resource": "arn:aws:s3:::my-app-bucket/*"
}
// NOT: "Action": "s3:*", "Resource": "*"
*/

// FILESYSTEM — PHP-FPM should not own source files
// Run as www-data, source files owned by deploy user
// chmod 755 app/ bootstrap/ config/
// chmod 775 storage/ bootstrap/cache/  ← writable only where needed