운영 권한은 필요할 때만: 공용 계정과 상시 권한 없애기

아프로·2026년 11월 2일

운영 권한은 필요할 때만 표지

2024년 5월 일본 DMM Bitcoin에서 약 3.08억 달러가 빠져나갔습니다. 지갑 업체 직원이 받은 가짜 채용 시험이 악성 Python 코드였고, 공격자는 그 직원의 세션 쿠키로 거래를 변조했습니다(코인 해킹 12년 연대기, 국내편 10편). 캐피타(해외편 09편)에서는 백업 관리자 계정이 도메인 관리자 권한을 갖고 있었고, 고권한 계정의 사용을 통제·기록하는 체계가 없었습니다(ICO). 운영 권한이 늘 열려 있으면 직원 노트북 한 대, 세션 하나가 그 권한 전체로 이어집니다.

이 시리즈의 공격을 실제 쇼핑몰 코드에서 직접 재연하고 막아 보는 인프런 강의가 열렸습니다. 실제 해킹 사고로 배우는 Spring Boot·Express 시큐어 코딩 · 이 글과 이어지는 수업: 13-5, 13-6

한눈에 보기

공용 admin 계정과 상시 권한 대신 개인 계정, 재인증, 시간 제한 권한, 접근 기록으로 운영 권한을 여는 흐름

위험어디서 생기나막는 법 한 줄OWASP
누가 했는지 모름여러 사람이 함께 쓰는 운영 계정회사 SSO 개인 계정으로만 운영 화면에 들어온다A07
늘 열린 운영 권한관리자 역할을 계정에 상시 부여사유를 받고 정해진 시간만 권한을 준다A01
세션 하나로 권한 상승로그인만 돼 있으면 되는 권한 요청권한을 올릴 때 다시 인증한다A07
접근 흔적이 없음운영 API에 기록 없음모든 운영 접근을 감사 로그로, 상시 권한 0을 정기 점검A01·A09

1. 공용 계정 대신 개인 계정

무엇이 문제인가

운영 도구에 admin 계정 하나를 두고 여럿이 비밀번호를 나눠 쓰면, 접근 기록에는 admin만 남습니다. 사고가 나도 누가 한 일인지 가릴 수 없습니다. 한 사람이 팀을 떠나면 모두의 비밀번호를 바꿔야 하니 회수가 미뤄집니다. 국내편 10편이 정리한 로닌 브리지 사고에서도 회수되지 않은 권한이 서명 확보에 쓰였습니다.

계정 하나가 닿는 범위도 문제입니다. SGI서울보증에서는 보도에 따르면 관리자 계정 1개가 서버 32대를 관리했습니다(국내편 06편).

막는 코드

특정 회사 코드가 아니라 개념을 보여 주는 예시입니다. 값은 가짜입니다. 위험한 모양은 설정 파일에 공용 계정을 두는 것입니다.

# application.yml (위험한 모양)
spring.security.user:
  name: admin
  password: FAKE-shared-password

고친 모양은 운영 경로를 회사 SSO(한 번 로그인으로 여러 사내 시스템에 들어가는 통합 인증)의 개인 계정으로만 엽니다.

@Bean
SecurityFilterChain opsChain(HttpSecurity http) throws Exception {
    return http.securityMatcher("/ops/**")
            .oauth2Login(Customizer.withDefaults())           // 사람마다 다른 계정
            .authorizeHttpRequests(a -> a.anyRequest().authenticated())
            .build();
}

확인하는 법

설정 파일과 비밀 저장소에서 admin, ops, root 같은 공용 이름의 계정을 찾습니다. 운영 접근 기록의 행위자 칸이 모두 개인 계정인지 봅니다.

2. 시간 제한 권한 상승

무엇이 문제인가

개인 계정이어도 관리자 역할을 늘 붙여 두면, 그 사람의 노트북이나 세션이 넘어가는 순간 운영 권한도 같이 넘어갑니다. 운영 작업은 하루 중 잠깐인데 권한은 24시간 열려 있습니다. 시간 제한 권한 상승(평소에는 권한이 없고, 필요할 때 사유를 남기고 정해진 시간만 받는 방식)은 열려 있는 시간을 줄입니다.

막는 코드

권한 부여 기록에 만료 시각을 두고, 요청마다 만료되지 않은 기록이 있는지 확인합니다.

public record OpsGrant(String user, String reason, Instant expiresAt) {}

@Component("opsAccess")
public class OpsAccess {
    public boolean active(Authentication auth) {
        return grants.findActive(auth.getName(), Instant.now()).isPresent();
    }
}

@PreAuthorize("@opsAccess.active(authentication)")
@GetMapping("/ops/orders/{id}")
public OrderDetail opsOrder(@PathVariable long id) { ... }

권한은 계정에 저장된 역할이 아니라 만료 시각이 있는 기록에서 나옵니다. 시간이 지나면 따로 회수하지 않아도 다음 요청부터 403입니다.

확인하는 법

권한을 받은 뒤 테스트 시계를 만료 시각 너머로 옮기고, 같은 운영 API가 403을 돌려주는지 봅니다.

3. 권한을 올릴 때 다시 인증

무엇이 문제인가

DMM Bitcoin에서 공격자는 직원의 세션 쿠키로 거래를 변조했습니다. 로그인돼 있기만 하면 권한 상승을 요청할 수 있다면, 세션을 가진 쪽도 권한을 올릴 수 있습니다. 권한 상승처럼 위험한 동작은 방금 인증했는지를 따로 확인해야 합니다.

막는 코드

OIDC 토큰의 인증 시각(auth_time)을 보고, 5분이 지났으면 다시 인증하게 합니다.

@PostMapping("/ops/elevate")
public OpsGrant elevate(@RequestParam String reason, @AuthenticationPrincipal OidcUser user) {
    Instant authAt = user.getAuthenticatedAt();
    if (authAt == null || authAt.isBefore(Instant.now().minus(Duration.ofMinutes(5)))) {
        throw new StepUpRequiredException("reauth required");     // 다시 로그인·MFA로 보낸다
    }
    OpsGrant g = new OpsGrant(user.getName(), reason, Instant.now().plus(Duration.ofHours(1)));
    return grants.save(g);
}

StepUpRequiredException은 설명용 이름입니다. 강의에서 만드는 방어 부품 예시에서는 이 검사를 @RequireStepUp 한 줄로 붙입니다. 5분과 1시간은 예시 값입니다.

확인하는 법

인증 시각이 10분 전인 테스트 토큰으로 권한 상승을 요청해 거부되는지, 방금 인증한 토큰으로는 만료 시각이 있는 권한이 생기는지 봅니다.

4. 모든 운영 접근을 기록

무엇이 문제인가

권한을 시간으로 묶어도, 그 시간에 무엇을 했는지 남지 않으면 사고 뒤에 범위를 정할 수 없습니다. 상시 권한이 다시 생겨도 아무도 모릅니다. 급한 장애 대응 때 붙인 관리자 역할이 그대로 남는 경우가 대표적입니다.

막는 코드

운영 API에는 감사 로그를 붙이고(16편의 @Audited), 상시 권한이 남아 있는지 정기적으로 확인합니다.

@Audited(action = "ops.access")                 // 강의에서 만드는 방어 부품 예시
@PreAuthorize("@opsAccess.active(authentication)")
@PostMapping("/ops/orders/{id}/refund")
public RefundResult opsRefund(@PathVariable long id) { ... }

@Scheduled(cron = "0 0 9 * * MON")
void reviewStandingAccess() {
    List<String> standing = users.findWithPermanentRole("OPS");
    if (!standing.isEmpty()) alerts.raise("P2", "standing-ops-access", standing);
}

부여 기록에는 사유가 남고, 접근 기록에는 행위자가 남습니다. 둘을 맞춰 보면 "누가, 왜, 무엇을"이 한 줄로 이어집니다. 팀을 떠난 사람의 계정은 SSO에서 끄는 것만으로 운영 접근도 함께 막힙니다.

확인하는 법

테스트 계정에 운영 역할을 상시로 붙인 뒤 점검 작업을 실행해 경보가 생기는지 봅니다. 목표는 상시 운영 권한 0입니다.

점검 체크리스트

  • 운영 도구와 서버에 여러 사람이 함께 쓰는 계정이 없고, 회사 SSO 개인 계정으로만 들어온다
  • 운영 권한은 계정에 상시로 붙어 있지 않고, 사유와 만료 시각이 있는 기록으로만 생긴다
  • 권한을 올릴 때 최근 인증(재로그인·MFA)을 확인한다
  • 모든 운영 API 호출이 행위자·대상·사유와 함께 감사 로그로 남는다
  • 상시 운영 권한이 생기면 정기 점검에서 경보가 울린다
  • 팀을 떠난 사람의 계정을 끄면 운영 접근도 함께 막힌다

정리

공용 계정은 누가 했는지를 지우고, 상시 권한은 노트북 한 대와 세션 하나를 운영 권한 전체로 만듭니다. 운영 권한은 평소에 비워 두고, 개인 계정으로 사유를 남기고 다시 인증한 뒤 정해진 시간만 받습니다. 그 시간에 한 일은 모두 감사 로그에 남기고, 상시 권한이 다시 생기지 않았는지 정기적으로 확인합니다.

함께 보면 좋은 글

보안을 더 알고 싶다면

오늘 본 사고의 원인은 대부분 서버와 코드의 기본기에서 막을 수 있는 것들이었습니다. 직접 손으로 막아 보고 싶다면 이런 강의가 있습니다.

실제 해킹 사고로 배우는 Spring Boot·Express 시큐어 코딩

  • AI로 자동화된 해킹 시대, 오픈소스 쇼핑몰 코드에서 실제 사고 22건을 재연하고 직접 취약점을 찾아 한 건씩 막습니다.
  • OWASP Top 10:2025 전체와 목록 밖 위험 20가지를 102개 수업으로 다루고, 취약점 37개를 모두 막은 최종 소스를 가져갑니다. Spring Boot와 Express 모두 다룹니다.
  • 이 글과 이어지는 수업: 13-5, 13-6

서버 보안 기초부터 차근차근 시작하고 싶다면 이 강의도 있습니다.

백엔드 개발자를 위한 서버 보안 실무: 해킹 방어부터 정보유출 방지까지

  • 서버를 공격하는 흐름을 로그로 보고, 내 서버와 Spring·Express 코드에서 직접 막아 봅니다.
  • SSH 하드닝, 인젝션, 인증·인가, 시크릿과 개인정보 보호, 침해사고 첫 한 시간까지 8개 섹션 54개 수업입니다.
  • 이 글과 이어지는 수업: 1-1, 3-6, 6-7

출처와 확인 날짜

수치와 사실은 2026년 11월 2일 기준 공개 자료입니다. 이 글은 법률 자문이 아닙니다.


이전 글 오류가 나면 문이 열린다: 실패하면 막는 예외 처리
다음 글 백업이 같이 잠기지 않게: 불변 백업과 복구 훈련
시리즈 전체 목록 해킹 방식 VS 방어 방법 전체 목록

profile
고인물 개발자(OG)

0개의 댓글