🔒 보안

RBAC

Role-Based Access Control (역할 기반 접근 제어)

사용자의 역할(Role)에 따라 시스템 접근 권한을 부여하는 보안 모델입니다. 개별 사용자에게 직접 권한을 부여하는 대신, 역할(admin, editor, viewer 등)에 권한을 정의하고 사용자에게 역할을 할당합니다. 94.7%의 조직이 RBAC를 사용한 경험이 있으며, 86.6%가 현재 최우선 접근 제어 모델로 채택하고 있습니다.

📖 상세 설명

RBAC(Role-Based Access Control)는 1992년 NIST에서 처음 공식화된 접근 제어 모델로, "누가 무엇을 할 수 있는가"를 역할 단위로 관리합니다. 핵심 아이디어는 간단합니다: 권한(Permission)을 역할(Role)에 부여하고, 사용자(User)에게 역할을 할당합니다. 예를 들어, "editor" 역할에 create, read, update 권한을 부여하면, editor 역할을 가진 모든 사용자가 해당 권한을 갖게 됩니다.

RBAC는 ACL(Access Control List) 방식의 한계를 극복하기 위해 등장했습니다. ACL은 각 리소스마다 사용자별 권한을 직접 지정하는데, 사용자와 리소스가 늘어나면 관리가 기하급수적으로 복잡해집니다. RBAC는 역할이라는 추상화 계층을 도입해 이 문제를 해결합니다. 신입 직원이 입사하면 적절한 역할만 할당하면 되고, 퇴사 시 역할만 회수하면 됩니다.

RBAC의 핵심 원칙 중 하나는 최소 권한 원칙(Principle of Least Privilege)입니다. 사용자에게 업무 수행에 필요한 최소한의 권한만 부여해야 합니다. 또한 직무 분리(Separation of Duties, SoD)를 통해 상호 배타적인 역할을 한 사용자가 동시에 보유하지 못하게 합니다. 예를 들어, 구매 요청자와 승인자 역할은 같은 사람이 가질 수 없습니다.

현대 시스템에서 RBAC는 단독으로 사용되기보다 ABAC(속성 기반)와 결합한 하이브리드 방식으로 진화하고 있습니다. 기본 권한은 RBAC로 관리하되, 세밀한 조건(시간, 위치, 데이터 민감도)은 ABAC로 제어합니다. Kubernetes, AWS IAM, Azure RBAC 등 대부분의 클라우드 플랫폼이 RBAC를 기본 접근 제어 모델로 채택하고 있으며, Zero Trust 아키텍처에서도 핵심 구성 요소로 활용됩니다.

💻 코드 예제

// RBAC 구현 예제 - Node.js/Express
const express = require('express');
const app = express();

// 1. 역할-권한 매핑 정의
const ROLE_PERMISSIONS = {
    admin: ['users:create', 'users:read', 'users:update', 'users:delete',
            'posts:create', 'posts:read', 'posts:update', 'posts:delete'],
    editor: ['posts:create', 'posts:read', 'posts:update', 'users:read'],
    viewer: ['posts:read', 'users:read']
};

// 2. 사용자-역할 매핑 (실제로는 DB에서 조회)
const USER_ROLES = {
    'user_001': ['admin'],
    'user_002': ['editor'],
    'user_003': ['viewer'],
    'user_004': ['editor', 'viewer']  // 복수 역할 가능
};

// 3. 권한 확인 함수
function hasPermission(userId, permission) {
    const userRoles = USER_ROLES[userId] || [];

    // 사용자의 모든 역할에서 권한 확인
    return userRoles.some(role => {
        const permissions = ROLE_PERMISSIONS[role] || [];
        return permissions.includes(permission);
    });
}

// 4. RBAC 미들웨어
function rbac(requiredPermission) {
    return (req, res, next) => {
        const userId = req.user?.id;  // 인증 미들웨어에서 설정

        if (!userId) {
            return res.status(401).json({
                error: 'Unauthorized',
                message: '인증이 필요합니다'
            });
        }

        if (!hasPermission(userId, requiredPermission)) {
            // 감사 로그 기록
            console.log(`[RBAC DENIED] User: ${userId}, Permission: ${requiredPermission}`);

            return res.status(403).json({
                error: 'Forbidden',
                message: `'${requiredPermission}' 권한이 필요합니다`
            });
        }

        // 감사 로그 기록
        console.log(`[RBAC GRANTED] User: ${userId}, Permission: ${requiredPermission}`);
        next();
    };
}

// 5. 라우트에 RBAC 적용
app.get('/api/users', rbac('users:read'), (req, res) => {
    res.json({ users: ['user1', 'user2'] });
});

app.post('/api/users', rbac('users:create'), (req, res) => {
    res.json({ message: 'User created' });
});

app.delete('/api/users/:id', rbac('users:delete'), (req, res) => {
    res.json({ message: 'User deleted' });
});

// 6. 역할 기반 UI 렌더링용 API
app.get('/api/my-permissions', (req, res) => {
    const userId = req.user?.id;
    const userRoles = USER_ROLES[userId] || [];

    const permissions = [...new Set(
        userRoles.flatMap(role => ROLE_PERMISSIONS[role] || [])
    )];

    res.json({ roles: userRoles, permissions });
});

app.listen(3000);
# RBAC 구현 예제 - Python/Flask
from functools import wraps
from flask import Flask, request, jsonify, g
from typing import List, Set
import logging

app = Flask(__name__)
logging.basicConfig(level=logging.INFO)

# 1. 역할-권한 매핑 (계층적 RBAC)
ROLE_HIERARCHY = {
    'admin': ['editor', 'viewer'],  # admin은 editor, viewer 권한 상속
    'editor': ['viewer'],            # editor는 viewer 권한 상속
    'viewer': []
}

ROLE_PERMISSIONS = {
    'admin': {'users:create', 'users:delete', 'settings:manage'},
    'editor': {'posts:create', 'posts:update', 'posts:delete'},
    'viewer': {'posts:read', 'users:read'}
}

# 2. 계층적 권한 계산
def get_effective_permissions(roles: List[str]) -> Set[str]:
    """역할 계층을 고려한 실제 권한 계산"""
    permissions = set()

    def collect_permissions(role: str, visited: set):
        if role in visited:
            return
        visited.add(role)

        # 현재 역할의 권한 추가
        permissions.update(ROLE_PERMISSIONS.get(role, set()))

        # 상속받는 역할의 권한 추가
        for inherited_role in ROLE_HIERARCHY.get(role, []):
            collect_permissions(inherited_role, visited)

    for role in roles:
        collect_permissions(role, set())

    return permissions

# 3. RBAC 데코레이터
def require_permission(permission: str):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            user_id = g.get('user_id')
            user_roles = g.get('user_roles', [])

            if not user_id:
                return jsonify({'error': 'Unauthorized'}), 401

            effective_permissions = get_effective_permissions(user_roles)

            if permission not in effective_permissions:
                logging.warning(
                    f"[RBAC DENIED] user={user_id} "
                    f"permission={permission} roles={user_roles}"
                )
                return jsonify({
                    'error': 'Forbidden',
                    'required_permission': permission
                }), 403

            logging.info(
                f"[RBAC GRANTED] user={user_id} "
                f"permission={permission}"
            )
            return f(*args, **kwargs)
        return wrapper
    return decorator

# 4. 직무 분리(SoD) 검증
MUTUALLY_EXCLUSIVE_ROLES = [
    {'purchase_requester', 'purchase_approver'},
    {'developer', 'production_deployer'}
]

def validate_sod(user_roles: List[str]) -> bool:
    """직무 분리 규칙 위반 확인"""
    role_set = set(user_roles)
    for exclusive_set in MUTUALLY_EXCLUSIVE_ROLES:
        if len(role_set & exclusive_set) > 1:
            return False
    return True

# 5. 라우트 예제
@app.route('/api/posts', methods=['GET'])
@require_permission('posts:read')
def get_posts():
    return jsonify({'posts': ['post1', 'post2']})

@app.route('/api/posts', methods=['POST'])
@require_permission('posts:create')
def create_post():
    return jsonify({'message': 'Post created'})

@app.route('/api/users', methods=['DELETE'])
@require_permission('users:delete')
def delete_user():
    return jsonify({'message': 'User deleted'})

# 6. 역할 할당 API (관리자 전용)
@app.route('/api/roles/assign', methods=['POST'])
@require_permission('settings:manage')
def assign_role():
    data = request.json
    user_id = data.get('user_id')
    new_roles = data.get('roles', [])

    # 직무 분리 검증
    if not validate_sod(new_roles):
        return jsonify({
            'error': 'SoD Violation',
            'message': '상호 배타적인 역할을 동시에 할당할 수 없습니다'
        }), 400

    # 역할 할당 로직...
    return jsonify({'message': f'Roles assigned to {user_id}'})

if __name__ == '__main__':
    app.run(debug=True)
// RBAC 구현 예제 - Go/Gin
package main

import (
    "log"
    "net/http"
    "sync"

    "github.com/gin-gonic/gin"
)

// 역할-권한 매핑
var rolePermissions = map[string][]string{
    "admin":  {"users:create", "users:read", "users:update", "users:delete",
               "posts:create", "posts:read", "posts:update", "posts:delete"},
    "editor": {"posts:create", "posts:read", "posts:update", "users:read"},
    "viewer": {"posts:read", "users:read"},
}

// 사용자-역할 저장소 (실제로는 DB 사용)
type RBACStore struct {
    mu        sync.RWMutex
    userRoles map[string][]string
}

var store = &RBACStore{
    userRoles: map[string][]string{
        "user_001": {"admin"},
        "user_002": {"editor"},
        "user_003": {"viewer"},
    },
}

// 권한 확인 함수
func (s *RBACStore) HasPermission(userID, permission string) bool {
    s.mu.RLock()
    defer s.mu.RUnlock()

    roles, exists := s.userRoles[userID]
    if !exists {
        return false
    }

    for _, role := range roles {
        permissions, ok := rolePermissions[role]
        if !ok {
            continue
        }
        for _, p := range permissions {
            if p == permission {
                return true
            }
        }
    }
    return false
}

// RBAC 미들웨어
func RBACMiddleware(permission string) gin.HandlerFunc {
    return func(c *gin.Context) {
        userID := c.GetString("userID") // 인증 미들웨어에서 설정

        if userID == "" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error":   "Unauthorized",
                "message": "인증이 필요합니다",
            })
            c.Abort()
            return
        }

        if !store.HasPermission(userID, permission) {
            log.Printf("[RBAC DENIED] User: %s, Permission: %s", userID, permission)
            c.JSON(http.StatusForbidden, gin.H{
                "error":   "Forbidden",
                "message": permission + " 권한이 필요합니다",
            })
            c.Abort()
            return
        }

        log.Printf("[RBAC GRANTED] User: %s, Permission: %s", userID, permission)
        c.Next()
    }
}

// 인증 미들웨어 (예시)
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 실제로는 JWT 토큰 검증 등
        userID := c.GetHeader("X-User-ID")
        if userID != "" {
            c.Set("userID", userID)
        }
        c.Next()
    }
}

func main() {
    r := gin.Default()
    r.Use(AuthMiddleware())

    // 라우트에 RBAC 적용
    api := r.Group("/api")
    {
        // 누구나 읽기 가능
        api.GET("/posts", RBACMiddleware("posts:read"), func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"posts": []string{"post1", "post2"}})
        })

        // editor 이상만 생성 가능
        api.POST("/posts", RBACMiddleware("posts:create"), func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"message": "Post created"})
        })

        // admin만 사용자 삭제 가능
        api.DELETE("/users/:id", RBACMiddleware("users:delete"), func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"message": "User deleted"})
        })
    }

    r.Run(":8080")
}

🗣️ 실무에서 이렇게 말하세요

💬 권한 설계 회의에서
"RBAC로 기본 권한 체계를 잡읍시다. admin, editor, viewer 세 역할로 시작하고, 필요하면 나중에 세분화하면 됩니다. 중요한 건 최소 권한 원칙이에요 - 각 역할에 꼭 필요한 권한만 부여하고, 예외 요청은 시간 제한을 두고 승인합시다."
💬 코드 리뷰에서
"여기 역할 체크가 하드코딩되어 있네요. if (user.role === 'admin') 대신 hasPermission(user, 'users:delete') 형태로 바꿔주세요. 나중에 manager 역할에도 삭제 권한을 줘야 하면 이 코드 다 찾아서 고쳐야 해요. 권한은 역할이 아니라 기능 단위로 체크해야 합니다."
💬 보안 감사 대응에서
"분기별 접근 권한 리뷰를 수행하고 있습니다. 지난 분기에 사용되지 않은 권한을 가진 계정 47개를 식별해서 역할을 조정했고, 퇴사자 권한 회수는 HR 시스템과 연동해서 24시간 내 자동 처리됩니다. 모든 권한 변경은 감사 로그에 기록됩니다."

⚠️ 주의사항 & 베스트 프랙티스

Role Explosion (역할 폭발)

세밀한 제어를 위해 역할이 기하급수적으로 늘어나는 현상입니다. "engineering_viewer_project_a_readonly_asia" 같은 역할이 수백 개 생기면 관리가 불가능합니다. 역할은 20개 이하로 유지하고, 세부 조건은 ABAC로 처리하세요.

권한 누적 (Privilege Creep)

직무 변경 시 이전 역할을 회수하지 않아 권한이 계속 쌓이는 현상입니다. 부서 이동이나 승진 시 기존 역할을 반드시 검토하고, 분기별 접근 권한 리뷰를 수행하세요.

예외의 일상화

"급해서" 임시로 admin 권한을 부여하고 회수하지 않는 패턴입니다. 예외 권한은 반드시 만료 시간을 설정하고, 승인 기록을 남기세요. 가능하면 Just-In-Time(JIT) 접근 방식을 도입하세요.

RBAC 베스트 프랙티스

역할은 직무 기반으로 정의하고, 권한은 리소스:액션 형식(posts:delete)으로 세분화하세요. 복수 역할 할당을 허용하되 직무 분리(SoD) 규칙을 적용하고, 모든 권한 변경은 감사 로그에 기록하세요. 자동화된 프로비저닝/디프로비저닝을 구축하고, AI 기반 역할 마이닝으로 최적화하세요.

🔗 관련 용어

📚 더 배우기