부트캠프에서 배운 내용을 정리한 글입니다.

  • 이제부터는 교재 없이 강사님이 제작한 자료와 수업 메모를 토대로 정리를 하기 때문에 글이 난잡해질 수 있습니다.
  • 최대한 정리를 해보았지만 시간 관계상 깔끔하게 정리하지 못해 미흡한 부분에 대해서는 미리 양해를 구합니다.

Easyrec Architecture
위의 아키텍처를 예시로 보았을 때 개발자 관점의 Frontend와 Backend 그리고 보안 관점의 Client-side와 Server-side는 무엇을 기준으로 구분하느냐에 따라 비슷하지만 그 관점이 달라진다.

  • Frontend : 사용자가 직접 상호작용하는 영역
  • Backend : 데이터를 처리하고, 로직을 수행하며, DB와 통신하는 영역

frontend는 사용자 경험을 향상시키기 위한 인터페이스 구현에 초점이 맞추어져 있으며, backend는 로직 안정성, 성능, 데이터 무결성, API 설계에 초점이 맞추어져 있다.

반면 보안 관점에서는

  • Client Side : 사용자의 단말에서 수행되는 보안 로직으로 사용자가 직접 접근하거나 변조할 수 있는 영역
  • Server Side : 웹 서버, DB, API, 인증 시스템 등 서버 인프라 내에서 수행되는 보안 로직

client-side는 공격자가 직접 접근하고 조작이 가능한 비신뢰 영역으로 보고있고, server-side에서는 공격자가 직접 접근할 수 없고 위조된 요청이나 취약점을 이용한 간접적인 접근만 가능한 부분으로 보고있다. 즉, 어디를 어디까지 신뢰 가능한가를 기준으로 나눈다고 볼 수 있다.

물론 보안에서 완벽하게 신뢰할 수 있는 구간이란 존재하지 않는다. 다만 중요한 점은, 동일한 시스템 구조라 할지라도 개발 관점에서는 기능적 역할을 중심으로, 보안의 관점에서는 신뢰 여부와 Trust Boundary 중심으로 구분한다는 것이다.

Web Application Architecture

Monolithic Architecture와 Cloud Architecture

┌───────────────────────────────────┐
│     Single Application Server  │
│ ┌───────────────────────────────┐ │
│ │    Presentation Layer (UI) │ │
│ └───────────────────────────────┘ │
│ ┌───────────────────────────────┐ │
│ │      Business Logic Layer  │ │
│ └───────────────────────────────┘ │
│ ┌───────────────────────────────┐ │
│ │       Data Access Layer    │ │
│ └───────────────────────────────┘ │
└──────────────────┬────────────────┘
                 ▼
            ┌─────────────┐
            │  Database  │
            └─────────────┘
  • 모든 기능이 하나의 통합된 코드베이스와 배포 단위로 구성.
  • UI, 비즈니스 로직, 데이터 액세스 계층이 하나의 애플리케이션으로 결합됨.
  • 전통적이고 가장 단순한 형태의 아키텍처
  • 모든 컴포넌트가 하나의 실행 파일 및 배포 단위로 패키징
  • 단일 코드베이스에서 모든 기능 관리, 하나의 프로세스로 실행

초기 기업 인프라는 물리 하드웨어(서버, 스토리지, 네트워크) → 네트워크 연결 및 구성을 통한 통합 → 소프트웨어 설치 및 운영 순으로 진행되었다.

이 방식에서는 하드웨어 엔지니어, 네트워크 엔지니어, 소프트웨어 개발자가 각각 분리된 역할로 작업해야 했으며, 서버 자원 확보와 설치, 네트워크 구성 및 라우팅/방화벽 설정, 소프트웨어 배포/설정까지 모든 단계들이 순차적이고 의존적이어서 전체적으로 설정 시간이 매우 길었다.

또한, 서버 프로비저닝 후 변경이 어려운 이유로 리소스가 고정되며, 확장성이 낮고, 과도하게 높아지는 비용 등의 문제로 다음의 문제점이 있었다.

  • 애플리케이션이 커질수록 유지보수가 어려우며 기술 스택 변경이 어려움
  • 특정 기능만 확장하기 어렵고 전체를 확장해야하며 작은 변경에도 전체 애플리케이션 재배포 필요

이러한 단점은 현재의 레거시 시스템까지 이어져 다음의 다양한 보안취약점을 일으키고 있다.

  • 비대해진 코드베이스로 인한 취약점 발생 시 영향범위 구분 어려움.
  • 패치 적용 시 전체 서비스 재배포가 필요하며, 보안 업데이트가 지연되거나 누락될 수 있음.
  • 첫 번째와 같은 맥락으로 한 부분의 성능 또는 보안 문제 발생 시 전체 시스템에 영향을 미침.
  • 기술 스택 변경의 어려움으로 지원이 끊긴 오래된 프레임워크, 구버전 라이브러리를 지속적으로 사용하면서 생기는 CVE(공개 취약점) 대응의 지연.

기업이 빠르게 변화하는 비즈니스 요구사항에 대응하려면 빠른 설정, 확장, 배포 속도가 필수적이었다. 기존 물리 인프라는 이런 유연성과 자동화 측면에서 한계가 있었고 AWS는 이 한계를 극복하기 위해 하드웨어, 네트워크, 소프트웨어 스택을 추상화하고 표준화된 API로 제공하는 모델로 전환했다.

그렇게 탄생한 것이 바로 클라우드 아키텍처이다.

외부 접속자에 따른 고가용성 로드밸런스를 해야한다.

  • Scale-In과 Scale-Out
  • Scale-Up과 Scale-Down

가상화는 갑작스러운 대응에 어렵다. 이는 가상화의 한계다.

모노리식은 웹과 DB의 결합이 본래 목적이었고 갑작스러운 발전에 대응하지 못했다. 이 분명한 한계를 극복한 것이 바로 클라우드이다.

  • 클라우드를 이용해서 모노리식 구조는 매우 비효율적이다.
  • 마이크로서비스 아키텍처는 컨테이너를 만들었을 때 확장성이 장점이며 무분별하게 확장할 시 효율에 문제가 있다.
    DevOps Pipline

여담) 온프레미스 환경에서 클라우드 환경으로의 전환이 진행중이다. 온프레미스 환경보다는 클라우드 환경을 준비해야하고 앞으로의 프로젝트도 이렇게 준비하는 것이 맞다고 강사님이 이야기하셨다.

API TMI)
딕셔너리를 이용해서 API를 내부에서 동작하는 것과 외부에서 동작하는 것을 구분한다.

API 점검에서 API 팬테스트에서 딕셔너리를 요청하여 화이트박스 테스트를 하고 제공되지 않으면 블랙박스 테스트를 한다. 블랙박스는 말그대로 수단과 방법을 가리지 않고 어떻게든 뚫는 것이다.

C 언어의 구조체. 형식을 구조화해서 만든 것을 API라고 할 수 있다. 더 각광받은 것이 웹에서 모바일과 서드파티에서 딕셔너리를 만들고 전달하면서 시작했다. 일종의 모바일이 촉매제가 되어 API 개발에 너무 큰 프로젝트가 되므로 마이크로서비스 아키첵처가 더욱 주목받은 것이다.

Serverless Architecture

람다 등을 이용해서 클라우드에 요청을 하는 방식. 그러니깐 처리 로직을 클라우드의 함수(?)에 던지고 클라우드에 있는 DB까지 이용료가 부과된다.

오버라이트 등의 문제가 발생하지 않도록 설계를 잘해야한다.


앞서 언급했듯이 가상화는 오토스케일링이 구조적으로 어렵다. 그래서 등장한 것이 컨테이너이다. 컨테이너는 가상화가 아니다. 따라서 가상화와 다르게 보안에 취약할 수 있다. 가상화는 명령을 실행할 수 있는 명령어 모음이다.

가상화는 기존 CPU에 VCPU를 추가로 만들고 우리가 쓰고있는 하드웨어에 이걸 붙이고 VRAM과 VDISK를 만들어서 올린다. 장점으로는 아이솔레이션이 되고 실제가 아닌 전부 가상이므로 모든 가상환경마다 OS를 올리고 하드웨어를 공유하여 사용한다. 디스크 IO가 발생하면 그 속도는 당연히 느려질 수 밖에 없다. 이 VDI를 많이 사용했지만 부하가 늘어날수록 하드웨어 성능은 계속 부족해지니 가상화를 사용하지 않는 것이 더 이득인 수준까지 온다.

격리하고 OS를 설치한다는 이 근본적인 문제를 컨테이너는 극복할 수 있었다. 현대 세상은 코드 베이스로 돌아가는 이 컨테이너가 주류라고 볼 수 있다.

간단 도커 실습

https://docs.rockylinux.org/9/ko/gemstones/containers/docker/

설치

도커 저장소를 추가한다.

만약 명령어가 작동하지 않는다면 dnf 업데이트가 필요하므로 아래의 명령어 입력.

sudo dnf update -y

이후 위의 링크를 따라서 필요한 패키지를 설치하고 docker 서비스를 시작과 동시에 활성화하는 명령어를 입력한다.

[user@localhost ~]$ sudo usermod -aG docker user

docker 사용자 추가. docker 명령을 쓸 때 매번 sudo를 치지 않기 위해서, 즉 해당 사용자에게 Docker 데몬에 접근할 수 있는 권한을 부여하기 위해서 위의 명령어를 입력했다.
다만, docker 그룹은 사실상 root와 거의 동일한 권한을 의미한다. 따라서 오직 신뢰 가능한 사용자만 docker 그룹에 넣어야 하며 가능하면 rootless docker를 고려해야 한다.

이후 잠시 exit하고 docker 그룹을 확인한다.

[user@localhost ~]$ id
uid=1000(user) gid=1000(user) groups= ... 996(docker) ...
[user@localhost ~]$ docker -v
Docker version 28.5.1, build e180ab8
[user@localhost ~]$ docker images
REPOSITORY   TAG       IMAGE ID   CREATED   SIZE
[user@localhost ~]$ docker image ls
REPOSITORY   TAG       IMAGE ID   CREATED   SIZE
[user@localhost ~]$ docker search ubuntu
NAME                             DESCRIPTION                                     STARS     OFFICIAL
ubuntu                           Ubuntu is a Debian-based Linux operating sys…   17709     [OK]
ubuntu/squid                     Squid is a caching proxy for the Web. Long-t…   119
 ...
ubuntu/telegraf                  Telegraf collects, processes, aggregates & w…   4  
ubuntu/chiselled-jre             [MOVED TO ubuntu/jre] Chiselled JRE: distrol…   3  
[user@localhost ~]$ docker pull ubuntu:latest
latest: Pulling from library/ubuntu
4b3ffd8ccb52: Pull complete
Digest: sha256:6646..
Status: Downloaded newer image for ubuntu:latest
docker.io/library/ubuntu:latest

위에서 도커에서 버전을 확인하고 이미지를 검색한다음 ubuntu 이미지를 다운로드 했다.

[user@localhost ~]$ docker container ls
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
[user@localhost ~]$ docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
[user@localhost ~]$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
ubuntu       latest    97bed23a3497   2 weeks ago   78.1MB

이미지만 다운로드 받고 아직 컨테이너에 넣지는 않았다.
자식 프로세스를 하나 더 만들어서 그 안에서 명령어를 실행시켜준다.

inspect 명령어로 JSON포맷의 이미지 세부정보를 확인할 수 있다. 이 뜻은 파이썬 같은 언어로 해당 데이터를 다룰 수 있다는 뜻이다.

[user@localhost ~]$ docker container run --name first-ubuntu ubuntu:latest
[user@localhost ~]$ docker ps -a
CONTAINER ID   IMAGE           COMMAND       CREATED          STATUS                      PORTS     NAMES
51e2b2e6e71b   ubuntu:latest   "/bin/bash"   23 seconds ago   Exited (0) 22 seconds ago             first-ubuntu
[user@localhost ~]$ docker container ls
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

STATUS가 Exited (0)인 것과 ls 했을 때 표시되지 않는 것을 확인할 수 있다. Ubuntu 기본 이미지(위의 ubuntu:latest)는 실행할 명령을 지정하지 않으면 /bin/bash 를 기본 entrypoint로 실행한다. 하지만 tty(-it) 옵션 없이 bash가 실행되면 즉시 종료된다. 따라서 컨테이너는 해야 할 작업 없으니 종료 상태가 된다.

따라서 현재 컨테이너 상태는 Exited이기 때문에 docker container ls 명령어인 Running 중인 컨테이너에 보이지 않고 종료된 컨테이너 포함 전체를 표시하는 docker ps -a 명령어에 표시가 되는 것이다.

키 값이나 접속 정보가 든 코드를 허브에 올렸을 때 생기는 보안 문제가 발생한 적이 있으며 이는 매우 위험하다.
Docker Hub에 올린 이미지 안에 민감정보가 포함되면 보안 사고가 일어난다. 특히 설정 파일, ssh private key, db 접속 정보, api key/token, 인증서 등이 도커 이미지 안에 포함될 수 있고 Docker Hub에 push하면 이 곳이 공개 저장소면 누구나 pull 가능하고, 프라이빗 저장소라도 내부 인원이 실수로 유출할 수 있다. 따라서 주의가 필요하다.

아무튼 아래에서 우분투 이미지를 이용해서 명령어로 열었다. 우분투 이미지로 만들면서 ubuntu_1 이라는 컨테이너를 만들면서 /bin/bash라는 쉘을 만들었다.

[user@localhost ~]$ docker container run -it --name ubuntu_1 ubuntu /bin/bash
root@9980049dd25e:/#
root@9980049dd25e:/# id
uid=0(root) gid=0(root) groups=0(root)

이후 다른 터미널로 접속해서 다음의 명령어를 작성한다.

[user@localhost ~]$ docker container ls
CONTAINER ID   IMAGE     COMMAND       CREATED         STATUS         PORTS     NAMES
9980049dd25e   ubuntu    "/bin/bash"   2 minutes ago   Up 2 minutes             ubuntu_1
[user@localhost ~]$ docker ps
CONTAINER ID   IMAGE     COMMAND       CREATED         STATUS         PORTS     NAMES
9980049dd25e   ubuntu    "/bin/bash"   2 minutes ago   Up 2 minutes             ubuntu_1
[user@localhost ~]$ docker ps -a
CONTAINER ID   IMAGE           COMMAND       CREATED          STATUS                      PORTS     NAMES
9980049dd25e   ubuntu          "/bin/bash"   2 minutes ago    Up 2 minutes                          ubuntu_1
51e2b2e6e71b   ubuntu:latest   "/bin/bash"   17 minutes ago   Exited (0) 17 minutes ago             first-ubuntu
[user@localhost ~]$

이제 서로 다른 터미널에서 ps -ef 명령어를 실행한다.

root@9980049dd25e:/# ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  0 06:07 pts/0    00:00:00 /bin/bash
root          11       1  0 06:12 pts/0    00:00:00 ps -ef
[user@localhost ~]$ ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  0 12:58 ?        00:00:07 /usr/lib/systemd/systemd --switc
root           2       0  0 12:58 ?        00:00:00 [kthreadd]
...
root       59657       2  0 15:12 ?        00:00:00 [kworker/1:0-ata_sff]
user       59906   59290  0 15:13 pts/1    00:00:00 ps -ef
[user@localhost ~]$

위의 예시가 바로 컨테이너가 가상화가 아니라는 증거이다. 일반 사용자 계정으로 ps -ef 하면 무수히 많은 프로세스가 표시되지만 컨테이너는 접속하니깐 쉘과 ps -ef 프로세스 단 2개만 동작하고 있다. 이것은 최소한의 운영체제로서 동작하기 위한 필요충분 조건이 전혀 아니라고 보여진다. 즉, 해당 환경에서 명령어를 실행할 수 있는 라이브러리를 제공하는 것이 바로 컨테이너이다.

이렇듯 우분투 이미지만 올릴 수 있듯이 웹 소스코드를 올려놓고 컨테이너로 마운트하는 등의 작업을 할 수 있다. 스케일링하는 데에 이만한 장점이 없다.

해커 관점에서도 컨테이너에 접근해서 쉘 권한을 얻으려고 하거나 얻더라도 할 수 있는 활동이 매우 제한적이고 어렵다. 이건 전체 시스템을 컨테이너로 세분화할 수록 그 난이도가 증가할 수 밖에 없다. 물론 라이브러리 자체의 취약점이 존재한다면 이건 어쩔 수 없다..

TMI) EOL (End of Life)

EOL은 제품 및 소프트웨어의 공식 지원이 종료되는 시점을 의미한다.

제품이 EOL 되었다는 뜻은 CVE나 다양한 잠재적 보안 취약점에 노출되어있을 가능성이 높다. EOL은 다음의 사이트에서 찾아볼 수 있다. https://endoflife.date/docker-engine
EOL 된 버전을 사용하지 않는 것이 좋지만 피치못할 경우 위의 이유로 버전 정보를 숨기는 것은 매우 중요하다.

[user@localhost ~]$ docker container run -it --name ubuntu_2 ubuntu /bin/bash
root@927f12fef889:/# [user@localhost ~]$ docker ps -a
CONTAINER ID   IMAGE           COMMAND       CREATED          STATUS                       PORTS     NAMES
927f12fef889   ubuntu          "/bin/bash"   13 seconds ago   Up 13 seconds                          ubuntu_2
9980049dd25e   ubuntu          "/bin/bash"   40 minutes ago   Exited (130) 2 minutes ago             ubuntu_1
51e2b2e6e71b   ubuntu:latest   "/bin/bash"   54 minutes ago   Exited (0) 54 minutes ago              first-ubuntu

Ctrl+p+q로 빠져나오면 컨테이너가 중지되지 않고 서비스가 지속된다.

[user@localhost ~]$ docker container start ubuntu_1
ubuntu_1
[user@localhost ~]$ docker attach ubuntu_1
root@9980049dd25e:/# exit
[user@localhost ~]$ docker attach ubuntu_2
root@927f12fef889:/# exit
exit
[user@localhost ~]$ docker ps -a
CONTAINER ID   IMAGE           COMMAND       CREATED             STATUS                         PORTS     NAMES
927f12fef889   ubuntu          "/bin/bash"   6 minutes ago       Exited (0) 6 seconds ago                 ubuntu_2
9980049dd25e   ubuntu          "/bin/bash"   46 minutes ago      Exited (127) 23 seconds ago              ubuntu_1
51e2b2e6e71b   ubuntu:latest   "/bin/bash"   About an hour ago   Exited (0) About an hour ago             first-ubuntu

exit 후엔 start하고 attach로 들어갈 수 있고 Ctrl+p+q 로 중지하지 않고 나갈 수 있다. 사용하지 않는 컨테이너를 stop하지 않고 나가고 이게 쌓이다보면 리소스가 낭비된다.

root@9980049dd25e:/# cat /etc/*release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=24.04
DISTRIB_CODENAME=noble
DISTRIB_DESCRIPTION="Ubuntu 24.04.3 LTS"
PRETTY_NAME="Ubuntu 24.04.3 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04.3 LTS (Noble Numbat)"
VERSION_CODENAME=noble
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=noble
LOGO=ubuntu-logo

해당 명령어는 어느 환경에서든 운영체제 종류와 버전을 확인할 수 있는 명령어이다. uname 이라던가 이런거 사용하지말고 이거 쓰는게 좋다.

간혹 쉘도 ps명령어도 없는 그런 종류의 컨테이너도 있지만 이건 작정하고 은닉한거라 확인하는 게 어렵다.

[user@localhost ~]$ ps -ef | grep ssh[d]
root         782       1  0 12:59 ?        00:00:00 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
root       42039     782  0 14:20 ?        00:00:00 sshd: user [priv]
root       42043     782  0 14:20 ?        00:00:00 sshd: user [priv]
...
[user@localhost ~]$ cat /var/run/sshd.pid
782
[user@localhost ~]$ pgrep sshd
782
42039
42043
...
[user@localhost ~]$ pstree -p 782
sshd(782)─┬─sshd(42039)───sshd(42044)───bash(42068)───docker(74931)─┬─{docker}(7493+
          │                                                         ├─{docker}(7493+
          │                                                         ├─{docker}(7493+
          │                                                         ├─{docker}(7493+
          │                                                         ├─{docker}(7493+
          │                                                         ├─{docker}(7493+
          │                                                         ├─{docker}(7493+
          │                                                         └─{docker}(7698+
          ├─sshd(42043)───sshd(42048)───sftp-server(42049)
          ├─sshd(59261)───sshd(59266)───bash(59290)───pstree(76982)
          └─sshd(59265)───sshd(59270)───sftp-server(59271)
[user@localhost ~]$

위의 ps 명령어를 사용하지 못할 때는 다음의 방법을 사용한다.

[user@localhost ~]$ sudo ls -l /proc/782/exe
[sudo] password for user:
lrwxrwxrwx. 1 root root 0 Oct 20 12:59 /proc/782/exe -> /usr/sbin/sshd

커널이 로드되는 위치의 proc 아래에 번호가 PID 번호이고 exe가 있으면 찾을 수 있다. ps 명령어에 의존하지 않고도 사용자가 동작시키는 프로세스를 찾을 수 있다.

[user@localhost ~]$ for i in $(sudo ls /proc | grep ^[0-9] | sort -n); do
> sudo ls -l /proc/$i/exe 2> /dev/null
> done
[sudo] password for user:
lrwxrwxrwx. 1 root root 0 Oct 20 12:59 /proc/1/exe -> /usr/lib/systemd/systemd
lrwxrwxrwx. 1 root root 0 Oct 20 16:21 /proc/2/exe
lrwxrwxrwx. 1 root root 0 Oct 20 16:21 /proc/3/exe
lrwxrwxrwx. 1 root root 0 Oct 20 16:21 /proc/4/exe
lrwxrwxrwx. 1 root root 0 Oct 20 16:21 /proc/5/exe
...
lrwxrwxrwx. 1 root root 0 Oct 20 16:22 /proc/77029/exe
lrwxrwxrwx. 1 root root 0 Oct 20 16:22 /proc/77543/exe
# 또는
[user@localhost ~]$ for i in `sudo ls /proc | grep ^[0-9] | sort -n`
> do
> sudo ls -l /proc/$i/exe 2> /dev/null
> done

PID 중 exe로 되어있는 리스트를 가져올 때

컨테이너 환경 보안 점검에서 잘 사용할 수 있다. 아주 중요한 스킬이며 꼭 기억해두는 것이 좋다.

  • /proc 디렉터리는 커널이 현재 실행 중인 프로세스마다 PID 이름으로 만든 디렉터리를 포함하고 있다.
  • grep ^[0-9] 는 이름이 숫자로 시작하는 디렉터리만 추출한다. 이는 /proc 에는 cpuinfo , meminfo , sys, net 등의 비프로세스 디렉터리도 존재하기 때문이다.
  • sort -n 으로 PID를 숫자 크기로 정렬한다.
  • for i in $( ... ) 반복문으로 각각의 프로세스 디렉터리를 검색한다.

웹서비스 동작

[user@localhost ~]$ docker container run --privileged -it --name web2 -p 172.17.0.1:8008:80 ubuntu:latest /bin/bash
root@6e0f0739721c:/# apt-get update
root@6e0f0739721c:/# apt-get install -y apache2 
root@6e0f0739721c:/# service apache2 start
root@6e0f0739721c:/# echo “ubuntu test page” > /var/www/html/index.html
 <ctrl + p + q>
[user@localhost ~]$ curl  http://172.17.0.1:8008
ubuntu test page

attach 후 명령어를 입력했다.

[user@localhost ~]$ docker ps
CONTAINER ID   IMAGE           COMMAND       CREATED         STATUS         PORTS                     NAMES
6e0f0739721c   ubuntu:latest   "/bin/bash"   9 minutes ago   Up 9 minutes   10.0.2.130:8008->80/tcp   web2

HTTP

http를 볼 때 두 가지를 알고있어야 한다.

  • Method : 통신 방식이다. 그 내용이 꽤 방대해서 여기서는 생략한다. 일반적인 레거시 방식에서는 GET방식과 POST방식만 있다. 이 외에 다른 방식도 지원한다면 이게 보안취약점을 유발한다.
  • Status code : 클라이언트 요청에 대한 응답으로 서버가 반환하는 세 자리 숫자 코드이다. 디버깅 용도로 사용될 수 있고 공격자 입장에서는 정보 수집 용도로 사용될 수 있다. 네트워크 통신에서는 ICMP와 유사하다.
https://search.naver.com/search.naver?where=nexearch&sm=top_hty&fbm=0&ie=utf8&query=linux&ackey=lzb0lovb
          도메인과 파일경로            |   변수와 값
------------------------------------------------------------
where=nexearch
& <<<<< 구분 기호 (chatgpt 자세한 설명 필요
sm=top_hty
&
fbm=0
&
ie=utf8
&
query=linux
&
ackey=lzb0lovb

이를 설계한 내부의 개발자가 DB나 Web Application으로 넘기는 값이 있을 것이고, 이를 파라미터라고 부른다. 파라미터의 값이 필터링이 제대로 되지 않아 공격이 가능하면 이것은 인젝션 공격인 것이고 이 공격지점을 공격벡터라고 부른다.

API 딕셔너리를 통해 우리가 호출할 수 있는 목록이 정해져있고 브라우저에서 사이트에 접근한 경로의 흐름이 중요한게 아니라 변수 값에 어떤 것을 넘기는 게 중요하다. 파라미터가 네이버에서 알아서 붙여준것을 API라고는 볼 수는 없다. 물론 내부자 입장에서는 API로 볼수도 있을 것이다.

외부에서 사용하는 API는 인증만 된다면 사용할 수 있도록 인증하고 Auth를 이용하여 API 키를 이용한 인증 대행으로 처리해주는 로직으로 얻을 수 있는 보안적 이점이 있다.

또한 인증과 계정에 필요한 보안 솔루션에 들어가는 비용이 api를 사용하는 비용보다 훨씬 많으 들어가므로 이것도 금전적 이점이 있다.

API 인증 사용으로 얻는 보안적 이점

  1. 클라이언트가 직접 비밀정보를 가지지 않아도 된다.
    • 외부 서비스가 직접 DB 계정, 내부 서버 주소, 토큰, 민감 정보를 가지지 않아도 됨.
    • 모든 인증은 Auth 서버가 대행한다.
    • 데이터 유출 가능성이 낮아진다.
  2. API Key / Token을 중앙에서 통제할 수 있다.
    • 특정 클라이언트의 키를 비활성화 가능
    • 만료기간(Expiration)을 강제할 수 있음
    • 권한(Role)을 클라이언트별로 다르게 줄 수 있음
    • 공격 또는 키 유출 상황에서도 빠르게 차단 가능
  3. 인증과 권한 부여(Authorization)를 분리할 수 있다.
    • Auth 서버가 인증을 끝내고, API Gateway가 권한을 검증해서 트래픽을 허락/차단한다.
  4. API Key는 재발급/로테이션이 쉽다.
    • 클라이언트가 해킹당하거나 키가 유출되면 즉시 키를 폐기하고 새로운 키를 재발급할 수 있다.
  5. Rate Limit / Throttle을 적용할 수 있다.
    • Auth 서버 또는 API Gateway가 발급된 키 단위로 트래픽 제한, 요청 횟수 제한, DDoS 완화를 할 수 있다.
    • 무차별 대입 공격, 과도한 호출, 비용 폭주 방지.
  6. Audit Log(감사 로그) 확보 가능.
    • 누가, 언제, 어떤 API를 호출했는지 Auth 시스템에 에러/성공 로그가 축적된다.
  7. 서비스 내부 구조를 숨길 수 있다.
    • Auth 서버 뒤에서 API Gateway가 서비스 구조를 숨긴다.
    • 실제 서버 주소 노출 없음. DB 연결 정보 노출 없음. 내부 네트워크 구조 노출 없음.
  8. API Token으로 Scope(권한 범위)를 제한할 수 있다.
    • 토큰마다 다른 권한을 부여할 수 있어 유출 시 피해 범위를 제한할 수 있다.

API 실습

Python과 Flask를 준비한다.

Python 실습코드를 준비한다.

# app.py
from flask import Flask, request, jsonify

app = Flask(__name__)

# 1. 기본 Hello API
@app.route("/hello")
def hello():
    return jsonify(message="Hello, API!")

# 2. 경로 파라미터
@app.route("/hello/<name>")
def hello_name(name):
    return jsonify(message=f"Hello, {name}!")

# 3. 쿼리 파라미터
@app.route("/greet")
def greet():
    lang = request.args.get("lang", "en")
    if lang == "ko":
        return jsonify(message="안녕하세요!")
    elif lang == "es":
        return jsonify(message="¡Hola!")
    else:
        return jsonify(message="Hello!")

# 4. POST 요청 처리
@app.route("/echo", methods=["POST"])
def echo():
    data = request.get_json(silent=True)  # 파싱 실패 시 None 반환
    if data is None:
        return jsonify(error="Invalid or missing JSON"), 400
    return jsonify(you_sent=data)

if __name__ == "__main__":
    app.run(debug=True)

소스코드를 실행한다.

루프백으로 접속해서 이것저것 입력해 본다.




브라우저의 URL에 파라미터를 넘기는 GET방식이 보인다.

일반적으로 GET으로 민감정보를 넘기면 URL에 그대로 노출되기 때문에 절대 권장하지 않는 방식이라고 한다. 그렇다고 POST 방식은 안전하냐고 한다면 그것도 절대 안전하지 않다. 결국 GET방식이든 POST방식이든 안전한 건 없고 http 그 자체가 안전하지 않은 것이 핵심이다. 그래서 https를 사용해야 한다.

이건 POST방식이다.



postman으로 테스트 해보았다.

REST API

REST API는 HTTP 기반에서 자원을 URI로 표현하고, 해당 자원에 대한 행위를 HTTP 메서드(GET, POST, PUT, DELETE 등)로 구분하는 방식의 API 구조이다.

주요 특징은 다음과 같다:

  • URI로 자원을 식별한다. 예: /users/1, /products/123
  • HTTP 메서드로 동작을 표현한다.
    • GET : 조회
    • POST : 생성
    • PUT : 전체 수정
    • PATCH : 부분 수정
    • DELETE : 삭제
  • 서버는 요청 상태를 저장하지 않는 Stateless 원칙을 따른다.
  • 요청/응답 포맷은 보통 JSON을 사용한다.

이러한 표준화된 방식 덕분에 다양한 플랫폼·언어에서 공통된 방식으로 API를 사용할 수 있다.

REST API 테스트







REST API는 브라우저에서 직접 테스트하기 어려운 경우가 많기 때문에, Postman과 같은 API 테스트 도구를 사용한다. GET 요청 테스트, POST 요청 테스트 등의 다양한 REST API 요청을 보낼 수 있으며, raw 포맷으로 파라미터를 넣을 수 있다. POST 요청의 Body를 JSON 형태로 직접 작성할 수 있다는 뜻이다.

새로 시작 시 초기화 확인

MSA 구조의 이해와 실습

Docker 명령어 기반의 MSA 실습

디렉토리 구조

msa-docker-manual/
├── user-service/
│   ├── app.py
│   └── Dockerfile
├── product-service/
│   ├── app.py
│   └── Dockerfile
├── order-service/
│   ├── app.py
│   └── Dockerfile

/srv/msa-docker-manual 디렉토리를 만들고 그 하위에 실습을 구성하였다. 이후 vi 편집기를 통해 코드를 집어넣었다.

Dockerfile (공통)

FROM python:3.9
WORKDIR /app
COPY app.py .
RUN pip install flask requests
CMD ["python", "app.py"]

서비스 코드

user-service/users.py

from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/users")
def get_users():
    return jsonify([{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}])

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5001)

product-service/products.py

from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/products")
def get_products():
    return jsonify([{"id": 101, "name": "Laptop"}, {"id": 102, "name": "Phone"}])

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5002)

order-service/orders.py

from flask import Flask, jsonify
import requests

app = Flask(__name__)

@app.route("/orders")
def get_orders():
    users = requests.get("http://user-service:5001/users").json()
    products = requests.get("http://product-service:5002/products").json()
    return jsonify({
        "order_id": 9001,
        "user": users[0],
        "product": products[1]
    })

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5003)

docker.service 상태 확인

실행 명령어

docker network create msa-net

docker build -t user-service ./user-service
docker build -t product-service ./product-service
docker build -t order-service ./order-service

네트워크 생성 후 각 서비스 빌드

컨테이너 실행

docker run -d --name user-service --network msa-net -p 5001:5001 user-service
docker run -d --name product-service --network msa-net -p 5002:5002 product-service
docker run -d --name order-service --network msa-net -p 5003:5003 order-service

-network msa-net 옵션을 통해 컨테이너들이 같은 네트워크에서 통신 가능하게 설정한다.

테스트

curl http://localhost:5003/orders


VM 외부 웹 브라우저를 통해 접속한 결과는 위와 같다.
이후 아래의 명령어를 통해

docker network inspect msa-net

실제로 어떤 네트워크 설정이 되었는지 확인한다.
이 명령은 Docker 네트워크의 상세한 설정을 JSON 형태로 보여주는데, 주요 확인 포인트만 고르면 다음과 같다.

[user@localhost msa-docker-manual]$ docker network inspect msa-net
"Name": "msa-net",
"Driver": "bridge",
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
  • Name: 네트워크 이름 (msa-net)
  • Driver: 브리지 네트워크
  • Subnet: 컨테이너가 사용할 IP 대역
  • Gateway: 컨테이너들이 사용하는 게이트웨이 IP

즉, msa-net 안의 컨테이너들은 모두 172.18.0.0/16 범위의 IP를 부여받는다.

inspect 결과의 핵심은 아래와 같이 요약했다.

"Containers": {
    "0c9846...": {
        "Name": "user-service",
        "IPv4Address": "172.18.0.2/16"
    },
    "771c82...": {
        "Name": "product-service",
        "IPv4Address": "172.18.0.3/16"
    },
    "adcd06...": {
        "Name": "order-service",
        "IPv4Address": "172.18.0.4/16"
    }
}
  • user-service : 172.18.0.2, 사용자 목록 제공
  • product-service : 172.18.0.3, 상품 목록 제공
  • order-service : 172.18.0.4, 사용자/상품 서비스 호출 후 주문 데이터 생성

inspect 명령어는 각 컨테이너가 내부 네트워크에서 어떤 IP로 동작하고 있는지, 어떻게 서로 통신하는지를 확인하는 정보이며 MSA 구조에서 서비스 간 연동이 정상적으로 이루어지고 있음을 검증한다.

세션 실습

네이버 세션을 통한 로그인 실습

먼저 이미 네이버에 로그인된 웹브라우저의 값을 복사하고 새로운 브라우저를 시크릿모드로 네이버에 접속한다.


이후 F5를 통해 새로고침하면 로그인이 된다.

이후 다음날 장소까지 바꾸어 실습했는데 로그인이 된 것처럼 보였지만, 로그인 됐을 때처럼 정상적인 동작은 하지 않았다. 이게 만약 정상 작동했으면 분위기 싸해질뻔 했다..


※ 본 글은 비영리적 목적에 한해 자유롭게 이용 가능합니다. 단, 동일한 라이선스를 적용해야 하며, 상업적 이용은 금지됩니다.

0개의 댓글