학원에서 수행했던 세 개의 프로젝트를 하나의 도메인에서 subpath로 구분해 서비스를 하려고 했는데 example.com/projectname 이런 방식으로 subpath를 넣으려고 하니 제대로 작동하지 않았습니다. Vanilla JS를 사용한 프로젝트도 있고 React를 사용해서 제작한 서비스도 있어서 해결 방법이 서로 달랐습니다. 원래는 문제를 해결한 다음 바로 글을 쓰려고 했는데 귀찮아서 미루다 보니 지금은 그때만큼 기억이 생생하지 않아서 전에 정리한 것 위주로 간략하게 적어보겠습니다.


1. Vanilla JS + Express

  1차 프로젝트는 템플릿 엔진은 ejs를 사용했고 프론트에서는 Vanilla JS, 백엔드에서는 Express를 사용해서 프로젝트를 제작했습니다.

  • Nginx 설정 수정
    location /<subPathName> {
        proxy_pass http://localhost:<포트 번호>;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

 

  먼저 .env 파일을 열어 포트 번호가 겹치지 않게 수정해 준 후에 pm2 라이브러리를 이용해서 프로젝트를 실행했습니다. 그러고 나서 /etc/nginx/sites-enabled 폴더에 있는 사이트 설정 파일을 에디터로 열고 원하는 subpath와 앞에서 설정한 포트번호를 추가했습니다. proxy_pass 부분에서 주소 뒤에 '/'을 붙였다가 링크나 이미지가 제대로 작동을 하지 않았었는데 '/'를 지우니 제대로 작동했습니다.

  • proxy_pass http://localhost:3000;
    → /subPathName/img/logo.png로 접속 시 http://localhost:3000/subPathName/img/logo.png로 전달
  • proxy_pass http://localhost:3000/;
    → /subPathName/img/logo.png로 접속 시 http://localhost:3000/img/logo.png로 전달
  • 프론트 코드 수정 (JavaScript, HTML)
async function getLoginStatus() {
  try {
    const response = await fetch(`/<subPathName>/auth/status`);
    if (!response.ok) {
      throw new Error("데이터를 불러오는 중에 문제가 발생했습니다.");
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error(error);
  }
}

async function goToUserPage() {
  try {
    const logintrue = await getLoginStatus();

    if (logintrue.status === true) {
      window.location.href = `/<subPathName>/userpage?user=${logintrue.data.userId}`;
    } else {
      alert("로그인이 필요한 서비스입니다.");
      window.location.href = `/<subPathName>/login`;
    }
  } catch (error) {
    console.error(error);
    alert("로그인이 필요한 서비스입니다.");
    window.location.href = `/<subPathName>login`;
  }
}

 

  그 후에 JaveScript 코드에 있는 fetch, window.location.href의 경로 등에 subpath를 앞에 추가했습니다.

<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Network</title>

    <link rel="stylesheet" href="/<subPathName>/main.css" />
    <link rel="stylesheet" href="/<subPathName>/layout/header.css" />
    <link rel="stylesheet" href="/<subPathName>/network/network.css" />
    <link rel="icon" href="/<subPathName>/img/favicon.png" type="image/x-icon" />
  </head>
  <body>
    <div class="container">
      <!-- header -->
      <div class="header">
        <div class="logo">
          <a href="/<subPathName>">
            <img class="logo-image" src="/<subPathName>/img/logo.png" alt="logo" />
          </a>
        </div>
    <script src="/<subPathName>/layout/header.js"></script>
    <script src="/<subPathName>/network/network.js"></script>
  </body>
</html>

 

  html 파일 역시 경로 앞에 subpath를 추가했습니다.

  • 백엔드 코드 수정
app.set("views", __dirname + "/views");

app.use("/<subPathName>", express.static(path.join(__dirname, "/views")));
app.use("/<subPathName>", express.static(path.join(".", "uploads/profileImg")));

app.get("/<subPathName>", (req, res) => {
  res.render("network/network.html");
});

app.get("/<subPathName>/login", (req, res) => {
  res.render("login/login.html");
});

app.use("/<subPathName>/auth", authRouter);
app.use("/<subPathName>/users", userRouter);

app.use((req, res, next) => {
  res.redirect("/<subPathName>/404");
});

 

  index.js 파일을 위와 같이 수정했습니다. router 파일의 get, post와 같은 경로에는 subpath를 추가하지 않습니다. 이미 route로 접속할 때 subpath가 추가되도록 설정을 해주었기 때문입니다.


2. React + Express

  2차 프로젝트는 React와 Express를 이용해서 서비스를 제작했습니다. React는 CRA를 이용해서 초기 세팅을 했습니다.

  • Nginx 설정 수정
    location /<subPathName> {
        alias /var/www/project2/client/build;
        index index.html;
        try_files $uri $uri/ /<subPathName>/index.html;
    }

    location /<subPathName>/api {
        proxy_pass http://localhost:<포트 번호>;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

 

  프론트 코드의 경우 npm run build를 이용해서 build를 해준 뒤 static 파일로 연결을 해주었고, 백엔드 코드의 경우 pm2로 프로젝트를 실행했습니다. static 경로를 설정할 때 root가 아닌 alias를 사용해야 location에서 설정한 subpath로 접속 시 alias에서 설정해 준 경로로 변환해서 접속을 하게 됩니다.

  • 프론트 코드 수정 (package.json, App.js)
{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "homepage": "/<subPathName>",
  "dependencies": {
  ...
  }
}

 

  먼저 package.json 파일을 열어 homepage 항목을 추가하고 subpath를 설정했습니다.

function App() {
  return (
    <PageWrapper className='App'>
      <Router basename='/<subPathName>'>
        <AppContent />
      </Router>
    </PageWrapper>
  );
}

 

  App.js를 열어 Router basename에 subpath를 추가했습니다. Routh path에는 추가하지 않아도 됩니다.

const OptionContainer = styled.div`
  span {
    font-size: 32px;
    line-height: 60px;
    font-weight: bold;
    margin: 0 15px;
  }
  select {
    margin-left: 15px;
    width: 150px;
    font-size: 25px;
    border-style: none;
    border-bottom: 3px solid #5fc3c8;
    appearance: none;
    background: url('./img/recommendInput/polygon1.png') no-repeat right 10px
      center;
    background-size: 15px;
  }
}

 

  App.js까지 수정해 주었다면 따로 수정해 줄 부분은 없지만 만약 이미지 url 경로가 '../img'로 되어 있다면 부모 디렉터리의 주소가 기준이 되기 때문에 이미지가 제대로 표시되지 않습니다. './img'로 바꿔줘야 현재 디렉터리를 기준으로 해서 이미지가 제대로 표시됩니다.

  • 백엔드 코드 수정 (1차와 동일)
app.use('/<subPathName>',express.static(path.join(__dirname, 'client')));

app.use('/<subPathName>/api/allResearch', allResearchRouter);

 

  1차 프로젝트에서 했던 것과 같이 static, router 경로에 subpath를 추가했습니다.


3. React (Vite) + Express, TypeScript

  3차 프로젝트는 TypeScript, React (Vite), Express를 이용해서 제작했습니다.

  • Nginx 설정 수정

  2차 프로젝트와 다른 점은 build 할 때 npm run build가 아닌 vite build를 사용했다는 것입니다. 다른 설정은 동일합니다.

  • 프론트 코드 수정 (vite.config.ts, App.tsx)
export default defineConfig({
  base: '/<subPathName>',
  plugins: [react(), svgr()],
  resolve: {
    alias: {
      '/img': '/src/assets/img'
    }
  }
});

 

  2차에서는 package.json 파일을 수정했지만 이번에는 vite.config.ts 파일을 수정했습니다. base 항목에 subpath를 추가했습니다.

const App = () => {
  return (
    <Router basename="/<subPathName>">
      <Routes>
        <Route path='/' element={<Home />} />
        <Route path='/search' element={<Search />} />
        <Route path='*' element={<NotFound />} />
      </Routes>
    </Router>
  );
};

 

  2차에서 했던 것과 같이 App.tsx 파일의 Router basename에 subpath를 추가했습니다.

const refreshApi = axios.create({
  baseURL: '/',
  withCredentials: true
});

// 토큰 갱신
const refreshAuthToken = async () => {
  try {
    const response = await refreshApi.post(`/<subPathName>/api/auth/token`);
    return response.data.token;
  } catch (err) {
    console.error('Refresh token failed:', err);
    throw err;
  }
};

 

  만약 fetch나 axios 경로가 있다면 여기에도 subpath를 추가해 줍니다.

  • 백엔드 코드 수정 (1차, 2차와 동일)

  앞에서 했던 것과 동일하게 static, router 경로에 subpath를 추가했습니다.

1. PostgreSQL 설치하기

sudo apt-get update
sudo apt install postgresql

 

  위와 같이 입력해서 PostgreSQL을 설치해 줍니다.

sudo systemctl is-enabled postgresql

# disabled일 경우
sudo systemctl enable postgresql

 

  설치가 완료되면 서버를 재시작할 때마다 PostgreSQL이 자동으로 실행되도록 설정이 되어 있을 것입니다. 위의 명령어를 입력해서 확인한 후에 만약 disabled라고 뜬다면 자동으로 시작되도록 설정해 주세요.


2. 포트 변경하고 방화벽 설정하기

  PostgreSQL은 기본적으로 5432 포트를 사용합니다. 5432 포트는 잘 알려져 있는 포트이기 때문에 보안 상 좋지 않으므로 다른 포트를 사용해 주는 것이 좋습니다.

sudo vim /etc/postgresql/<Version>/main/postgresql.conf # 16 버전이면 /etc/postgresql/16/main

port = 5432 # 다른 포트 번호로 수정

 

  위와 같이 에디터를 열어서 'port=5432' 부분을 찾아 포트를 수정해 줍니다.

sudo systemctl restart postgresql

 

  포트를 수정한 후 PostgreSQL을 재시작해주세요.

sudo ufw allow 5432/tcp # 다른 포트로 수정한 경우 다른 포트 입력
sudo ufw status
sudo ufw reload

 

  위와 같이 입력해서 방화벽에서 포트를 허용해 줍니다. 5432 포트 대신 다른 포트를 설정했다면 변경한 포트 번호를 입력합니다.


3. 외부 접속 허용하기

  DBeaver와 같은 외부 프로그램을 이용해서 접속하려면 외부 접속을 허용해주어야 합니다. 이후 SSL 인증서나 특정 IP만 접속 가능하게 하는 등 절차를 추가하는 것이 좋습니다.

sudo vim /etc/postgresql/<Version>/main/postgresql.conf

listen_addresses = '*'

 

  PostgreSQL 설정 파일을 에디터로 열고 listen_addresses 항목을 '*'로 설정한 후 저장해 주세요. 기본값은 'localhost'로 되어 있어 localhost만 접속이 가능하지만 '*'으로 설정하면 모든 ip의 연결을 허용합니다. 모든 IP가 아닌 특정 IP의 연결만 허용하고 싶다면 특정 IP를 적어주면 됩니다. 여러 IP의 연결을 허용하고 싶다면 콤마(,)로 구분하면 됩니다.

sudo vim /etc/postgresql/16/main/pg_hba.conf

 

  listen_addresses 설정이 끝났으면 접근 권한을 설정해주어야 합니다. pg_hba.conf 파일을 에디터로 열어주세요.

# 모든 IP를 허용하려면
host all all 0.0.0.0/0 scram-sha-256

# 특정 IP를 허용하려면
host all all <허용할 IP>/32 scram-sha-256

 

  모든 IP를 허용하려면 0.0.0.0/0을 입력하면 되지만 당연히 보안 상 좋지 않으므로 특정 IP만 허용하는 것이 좋습니다. IP 뒤의 '/0', '/32'는 서브넷 마스크로 '/24'는 0부터 255까지, '/32'는 한 개의 IP만을 의미합니다. 예를 들어 '192.168.0.0/24'라고 입력한다면 192.168.0.0부터 192.168.0.255까지의 범위를 의미하고, '192.168.0.1/32'라고 입력한다면 192.168.0.1 하나를 의미합니다.

 

  host 뒤의 첫 번째 all 부분은 허용할 Database, 두 번째 all 부분은 허용할 사용자를 의미합니다. 두 부분을 모두 all로 설정하면 모든 데이터베이스에 모든 사용자가 접근을 할 수 있습니다.

 

  scram-sha-256 부분은 인증 방법을 설정할 수 있습니다. trust, password, md5 등으로 설정을 할 수도 있습니다. trust는 암호나 인증 없이 접속이 가능하고, password는 암호화되지 않은 암호로 접속이 가능합니다. md5는 md5로 암호화된 암호를 요구하지만 오래된 암호화 방식으로 보안상 취약점이 있으므로 scram-sha-256으로 설정하는 것이 좋습니다.


4. postgres 계정 비밀번호 설정하기

sudo -u postgres psql
ALTER USER postgres WITH PASSWORD '변경할 password';

# exit or \q 로 psql을 종료한 후
sudo systemctl restart postgresql

 

  위와 같이 psql에 접속한 후 비밀번호를 변경합니다. 만약 기본 비밀번호를 입력하라고 나와서 변경할 수가 없다면 pg_hba.conf에서 잠깐 trust로 변경하여 모든 접속을 허용을 한 후에 비밀번호를 변경하고 다시 scram-sha-256으로 변경합니다.


5. 데이터베이스, 테이블, 사용자 생성 후 권한 부여

1) 데이터베이스 생성

sudo -i -u postgres
psql

CREATE DATABASE example1;

\l # 데이터베이스 목록 확인
\q # psql 종료

 

  위와 같이 psql에 접속을 한 후 데이터베이스를 생성해 줍니다. 데이터베이스의 목록을 보려면 '\l'을 입력하면 됩니다.

 

2) 테이블 생성

\c example1 # example1 데이터베이스 접속

CREATE TABLE example1_table (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100)
);

\dt # 테이블 목록 확인

 

  psql에 접속 후 '\c 데이터베이스명'을 입력하면 데이터베이스에 접속할 수 있습니다. CREATE TABLE 명령어를 이용하여 테이블을 생성한 후 '\dt'를 입력하여 제대로 생성되었는지 확인할 수 있습니다.

 

3) 사용자 생성 및 권한 부여

CREATE USER username1 WITH PASSWORD 'password';
GRANT CONNECT ON DATABASE example1 TO username1;

\du # 유저 목록 확인

 

  위와 같이 유저를 생성하고 특정 데이터베이스에 접근할 수 있는 권한을 부여할 수 있습니다. 유저 생성 후 '\du'를 입력하여 제대로 생성이 되었는지 확인할 수 있습니다.

\c example1 # example1 데이터베이스 접속

# 앞으로 생성될 테이블에도 권한 부여
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO username1;

# 이미 존재하는 모든 테이블에만 권한 부여
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO username1;

# 특정 테이블에만 권한 부여
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.example1_table TO username1;

# 부여된 권한 조회
SELECT * FROM information_schema.role_table_grants WHERE grantee = 'username1';

 

  데이터베이스에 접속한 후 위와 같이 입력하면 특정 유저에게 테이블과 관련된 권한을 부여할 수 있습니다. GRANT 명령어만 쓰면 이미 존재하는 테이블에만 권한이 부여되므로 앞으로 생성될 테이블에도 권한이 부여가 되도록 하려면 ALTER DEFAULT PRIVILEGES 명령어를 사용하면 됩니다.


6. 기타 유용한 보안 설정 (로그 활성화, 환경 설정 파일 권한 변경, public 사용 제한)

1) 로그 활성화하기

sudo vim /etc/postgresql/16/main/postgresql.conf

# all(모든 로그), ddl(CREATE, ALTER, DROP 등), mod(ddl + INSERT, UPDATE, DELETE 등)
log_statement = 'all'

# 100 milliseconds 이상 걸리는 모든 로그 저장, 0으로 설정하면 모든 로그 저장
log_min_duration_statement = 100 

logging_collector = on
log_directory = 'log'
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'

sudo systemctl restart postgresql

 

  로그를 활성화하여 침입 유무를 확인하거나 침입 시도 의심 사례를 분석할 수 있습니다. PostgreSQL 설정 파일을 에디터로 열고 위와 같이 수정해 주세요. log_statement를 'ddl'로 설정하면 CREATE, ALTER , DROP과 같은 DDL(Data Definition Language) 쿼리만 로그에 기록되고, 'mod'로 설정하면 DDL 쿼리와 함께 INSERT, UPDATE, DELETE 같은 DML(Data Manipulation Language)도 함께 저장이 됩니다. 'all'로 설정하면 SELECT, REVOKE와 같은 모든 로그를 저장합니다. 

sudo -i -u postgres
cd <PostgreSQL Version>/main/log

 

  postgres 계정으로 접속해서 로그 폴더에 들어간 후 ls 명령어로 파일 목록을 확인해 보면 로그 파일이 생성되는 것을 볼 수 있습니다. cat 명령어 등을 사용하여 로그 내용을 확인할 수 있습니다.

 

2) 환경 설정 파일 권한 변경

cd /etc/postgresql/<Version>/main

ls -l # 권한 확인

sudo chmod 600 pg_hba.conf
sudo chmod 600 pg_ident.conf
sudo chmod 600 postgresql.conf
sudo chmod 600 pg_ctl.conf

 

  환경 설정 파일이 허가되지 않은 사용자에 의해서 변경되지 않도록 설정 파일의 권한을 600으로 변경합니다.

 

3) public 사용 제한

 

  PostgreSQL에서 DB를 생성할 경우 Default로 public 스키마가 생성됩니다. 다른 스키마를 생성하지 않고 테이블을 생성할 경우 기본적으로 public 스키마 안에 생성되고, public 스키마는 모든 개체에서 접근이 가능하므로 정보 유출 등의 가능성이 있습니다.

\c example1 # example1 데이터베이스 접속

# public 스키마에서 모든 사용자의 권한 제거
REVOKE ALL ON SCHEMA public FROM public;

# public schemad에서 특정 사용자에게 스키마 사용 권한 부여
GRANT USAGE ON SCHEMA public TO specific_user;

 

  위와 같은 절차는 DB별로 해주어야 하며, 추후에 DB가 생성되면 생성된 DB에도 해주어야 합니다. 이 설정들은 한국인터넷진흥원(KISA)의 클라우드 취약점 점검 가이드의 PostgreSQL 항목 중 일부를 발췌했습니다. 이외에도 유용한 설정들이 많으니 클라우드 취약점 점검 가이드를 참고해 주세요.

 

  미니 PC를 웹서버 용으로 쓰고 있어서 전원을 끄지 않고 24시간 동안 계속 가동하고 있습니다. 가끔 SSH로 접속해서 온도를 확인하는데 CPU는 평상시 40도 정도로 유지가 되고 있었지만 NVMe SSD의 온도는 항상 70도 이상으로 나왔습니다. NVMe SSD가 기본으로 장착되어서 나오는 모델들은 방열판이 달려있는 경우가 많은데, 제가 구입한 FIREBAT AM02 모델은 M.2 SATA SSD가 기본으로 장착되어 있어서 방열판이 따로 없었습니다. 전에 NVMe SSD로 교체하고 기본으로 달려있었던 Wicgtyp M.2 SATA SSD를 FIREBAT SSD 인클로저에 장착했었는데, 그때 장착하지 않은 방열판이 남아있어서 달아보기로 했습니다.

 

  FIREBAT SSD 인클로저의 기본 구성품에 있던 서멀 패드와 알루미늄 바디입니다. 방열판은 Aliexpress에서 1~2달러 정도면 구입할 수 있는 저가형 방열판입니다. 솔직히 효과는 크지 않을 것이라고 생각하지만 그래도 안 달아주는 것보다는 나을 것 같습니다.

 

  방열판을 장착하기 전 사진입니다. 기존에는 Wicgtyp M.2 SATA SSD가 달려있었지만 제거하고 WD BLACK SN770 NVMe SSD 500GB 모델을 장착했습니다.

 

  먼저 서멀 패드를 붙여주었습니다.

 

  그다음 방열판을 올려주었습니다. 어차피 닫아놓고 쓸 것이긴 하지만 서멀 패드 크기가 더 커서 삐져나오는 부분은 아쉽네요.

 

  방열판을 설치하고 난 후 약 1.5cm 정도 여유 공간이 있었습니다. 공간이 넉넉해서 더 큰 방열판을 장착할 수 있을 것 같습니다.

 

  방열판을 장착하고 나서 어느 정도 시간이 지난 후에 온도를 확인해 봤습니다. 72.8도에서 65.8도로 7도 정도 낮아졌네요. 예상한 대로 저가형 방열판이라 극적인 효과는 없었지만 65도 정도면 나쁘지 않은 것 같습니다. 당분간은 이 상태로 사용해 보고 나중에 필요하면 더 좋은 방열판으로 교체해야겠습니다.

1. Nginx 버전 정보 감추기

  미니 PC를 웹서버로 쓰고 있는데 웹페이지를 로드할 때 개발자모드로 Network 탭을 보면 서버의 IP주소와 어떤 툴을 이용해서 서비스를 하고 있는지 다 나와 있어서 보안 쪽으로 걱정이 되었습니다. 검색을 해보니 IP만 가지고는 해킹을 하기 어렵다고 해서 안심이 되었지만, Nginx의 어느 버전을 쓰고 있는지 노출이 되면 그 버전의 취약점을 이용해서 서버를 공격할 수 있다고 해서 최소한 버전만이라도 숨겨야겠다고 생각했습니다.

 

  아무런 작업도 해주지 않는다면 위와 같이 Remote Address에서 서버의 IP주소와 Response Headers의 Server에서 Nginx 버전이 그대로 노출되고 있는 것을 볼 수 있습니다. 

# server_tokens를 on으로 변경 후 저장
sudo vim /etc/nginx/nginx.conf

# Nginx 재시작
sudo nginx -t
sudo systemctl restart nginx

 

  에디터로 nginx.conf 파일을 열어주고 server_tokens가 off로 되어 있는 것을 on으로 바꿔주고 저장을 합니다.

 

  수정을 마쳤으면 sudo nginx -t로 nginx 설정 파일이 제대로 작동하는지 확인하고 재시작을 합니다.

 

  재시작을 하고 다시 웹사이트에 들어가서 개발자 모드로 확인을 해보면 Nginx의 버전이 사라진 것을 확인할 수 있습니다. Nginx를 사용하는 것까지 숨기고 다른 임의의 서버명을 표시하고 싶다면 Nginx의 Headers-More라는 모듈을 사용해야 하지만 모듈을 설치하고 다시 컴파일해줘야 하는 번거로움이 있습니다. 그리고 여전히 서버의 IP 주소가 노출되고 있어서 프록시를 적용하는 방법을 찾아봤는데 Cloudflare를 사용하면 쉽게 IP 주소와 서버의 정보가 숨겨진다고 해서 적용을 해보았습니다.


2. Cloudflare 적용하기

  Cloudflare에 가입한 후에 기존 도메인을 추가해 줍니다.

  

  도메인을 입력하고 DNS 레코드 빠른 스캔을 선택해 주세요.

 

  요금제는 Free를 선택해 주세요.

 

  도메인을 구입한 곳의 네임 서버 페이지에 들어가서 네임 서버를 수정해 줍니다. 

 

  저는 가비아에서 도메인을 구매했는데 기존 네임 서버 3개가 적혀 있는 것을 지운 후에 Cloudflare의 네임 서버를 입력했습니다.

 

  네임 서버를 수정해 주면 Cloudflare에 도메인 등록이 완료가 됩니다. Dashboard의 DNS 메뉴를 눌러보면 DNS가 자동으로 추가되고 프록시가 적용된 것을 확인할 수 있습니다. 참고로 무료 요금제를 선택했을 때 프록시 설정이 적용되어 있으면 사이트 속도가 다소 느려집니다. 무료 요금제는 망 사용료 때문에 한국의 CDN을 이용할 수 없어서 그렇다고 합니다.

 

  Dashboard에서 SSL/TLS 메뉴를 누른 후에 구성을 클릭해 주세요.

 

  기본값은 가변으로 되어있을 것입니다. '가변'은 클라이언트와 Cloudflare 사이에서는 HTTPS, Cloudflare와 Server 사이에서는 HTTP로 작동합니다. 서버가 HTTPS 인증서를 발급받지 않은 경우 선택합니다. '전체'는 클라이언트와 Cloudflare, Server 모두 HTTPS를 적용하고 서버의 인증서가 만료되거나 자체 인증된 인증서라도 허용합니다. '전체(엄격)'은 서버의 인증서가 유효해야 합니다. 저는 certbot을 이용해서 Let's Encrypt의 인증서를 발급받았기 때문에 '전체(엄격)'을 선택했습니다.

 

  다른 설정은 따로 안 건드려주고, 보안의 봇 메뉴를 클릭해서 봇 차단모드 정도만 켜주었습니다.

 

  모든 설정이 완료되었더라도 다시 웹페이지로 들어가서 개발자 모드로 확인을 해보면 여전히 서버의 정보가 그대로 표시되고 있을 것입니다. 그 이유는 DNS 설정이 바로 적용되는 것이 아니라 시간이 걸리기 때문입니다. 최대 24시간 정도 걸린다고 하는데 저의 경우는 1시간쯤 후에 적용됐습니다.

 

  Cloudflare 설정이 적용된 후에 개발자 모드의 Network 탭을 확인해 보면 서버의 IP가 노출되었던 Remote Address에는 Cloudflare의 프록시 IP 주소가 적혀있고, Server에는 nginx가 아닌 cloudflare가 적혀있는 것을 볼 수 있습니다.

 

  Cloudflare 연결 전 웹사이트의 인증서를 확인해 보면 certbot으로 발급받은 Let's Encrypt 인증서가 표시되고 있었습니다.

 

  Cloudflare 연결 후에는 발급 기관이 Google Trust Services로 바뀐 것을 볼 수 있습니다.

ping <도메인 이름>

 

  마지막으로 터미널에서 ping 도메인 이름을 입력하면 전에는 서버의 IP 주소가 그대로 노출이 됐었습니다. Cloudflare 적용 후에는 서버의 IP 주소가 아닌 Cloudflare의 프록시 주소가 표시가 됩니다.

 

  터미널에서 표시된 주소를 직접 브라우저에 입력해 보면 'Direct IP access not allowed'라는 Cloudflare 페이지가 표시됩니다.

 

  그나저나 서버를 열어둔 지 하루 밖에 안 됐는데 벌써 요청 수가 1,000번 가까이 되네요. 국적도 다양하고 벌써 차단된 공격도 보입니다. 앞으로 보안 공부를 더 철저히 해야겠습니다.

1. 구입한 도메인을 서버에 연결하기

  미니 PC에 Nginx를 설치하고 도메인 사이트에서 구입한 닷컴 도메인을 연결했습니다. 먼저 도메인을 서버에 연결하기 위해서는 도메인을 구입한 사이트의 DNS 관리 페이지에 들어가야 합니다.

 

  저는 가비아에서 구매를 해서 가비아를 기준으로 설명드리겠습니다. 먼저 DNS 정보 메뉴에 들어가서 DNS 관리 버튼을 클릭합니다.

 

  구매한 도메인을 체크하고 DNS 설정을 클릭합니다.

 

  레코드 추가를 눌러서 호스트는 www, @, 값/위치는 서버의 ip주소를 입력하고 TTL은 3600으로 설정하고 저장을 누릅니다. @는 도메인 주소를 입력했을 때 서버의 주소로 접속하기 위해서 설정하고, www는 www가 붙은 도메인 주소로 접속하기 위해서 설정해 줍니다. TTL은 DNS 서버가 정보를 캐시에 유지하는 시간을 의미하는데 TTL을 너무 낮게 설정하면 DNS 서버가 자주 갱신 요청을 해야 하므로 서버의 부하가 증가하고, TTL을 길게 설정하면 도메인 설정을 변경했을 때 변경사항이 적용되는데 오래 걸리므로 일반적으로 3600(1시간)으로 설정한다고 합니다.


2. Nginx 설정 수정하기

sudo vim /etc/nginx/sites-available/<도메인 이름>.conf

 

  도메인 사이트에서 DNS 설정이 끝났으면 서버의 Nginx 설정 파일을 수정해줘야 합니다. 위 명령어를 입력해서 nginx의 sites-available 폴더에 파일을 생성해 줍니다. 설정파일이라는 것을 구분하기 위해 '.conf'를 붙이는 것일 뿐 생략해도 무방합니다.

server {
    listen 80;
    server_name example.com www.example.com;

    root /var/www/html;
    index index.html index.htm index.nginx-debian.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

 

   에디터가 열렸으면 i를 눌러서 insert mode에 진입해서 위 코드를 붙여 넣기 한 후에 server_name 부분에 구입한 도메인 주소를 입력해 주세요. 그리고 esc를 누른 후에 :wq를 입력하고 저장을 합니다.

sudo ln -s /etc/nginx/sites-available/<도메인 이름>.conf /etc/nginx/sites-enabled/

 

  편집이 끝났다면 위의 명령어를 입력해서 sites-enabled 폴더에 심볼릭 링크를 형성해 줍니다. 심볼릭 링크는 윈도우의 바로가기 같은 기능으로 sites-available 폴더에서는 nginx의 모든 설정 파일을 저장해 놓고, 서버에 직접 적용할 설정 파일만 sites-enabled 폴더에 심볼릭 링크를 연결해서 사용을 합니다. 이제 도메인 주소를 웹브라우저에 입력하면 서버에 연결이 될 것입니다.


3. HTTPS 적용을 위해 SSL/TLS 인증서 받기

sudo apt-get update
sudo apt-get install certbot
sudo apt-get install python3-certbot-nginx

 

  웹페이지에 HTTPS 적용을 하기 위해서는 먼저 인증서를 발급받아야 합니다. 우분투 버전이 18.04 이상이면 python3-certbot-nginx를 설치하면 되지만, 그보다 오래된 버전이라면 python-certbot-nginx를 설치해야 한다고 합니다.

sudo certbot --nginx -d example.com -d www.example.com

 

  위의 명령어에서 example.com 부분을 구매한 도매인으로 수정하고 실행하면 인증서 발급 절차가 시작됩니다. 이메일을 입력하라는 문구가 나오는데, 이메일을 입력하고 약관에 동의하면 인증서가 발급이 됩니다.

sudo vim /etc/nginx/sites-available/<도메인 이름>.conf

 

  인증서를 발급받은 후에 다시 conf 파일을 확인해 보면 자동으로 아래와 같이 수정된 것을 볼 수 있습니다.

server {
    root /var/www/html;
    index index.nginx-debian.html;

    location / {
        try_files $uri $uri/ =404;
    }

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

server {
    if ($host = www.example.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    if ($host = example.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    listen 80;
    server_name example.com www.example.com;
    return 404; # managed by Certbot
}

 

  발급받은 인증서는 90일 이후에 만료되므로 cron 작업 스케줄러를 이용하여 자동으로 갱신되도록 하는 것이 좋습니다. 인증서를 갱신하기 위해서는 일시적으로 80번 포트를 열어주어야 하기 때문에 root 사용자의 crontab을 설정하겠습니다.

sudo crontab -e

 

  crontab -e를 입력하면 원하는 에디터를 설정하라는 문구가 나옵니다.

 

  저는 vim을 선호하기 때문에 2번을 선택했습니다.

0 0 * * * ufw allow 80 && /usr/bin/certbot renew --quiet && ufw deny 80

 

  에디터가 열렸다면 위와 같이 입력하고 저장해 줍니다.

 

  위와 같이 입력하면 매일 0시에 cerbot이 인증서를 갱신하도록 설정합니다. --quiet는 작업을 조용하게 진행하며, 오류가 없다면 로그를 남기지 않습니다. 만약 시간을 다르게 설정하고 싶다면 순서대로 시, 분, day of month(매일), mon(매월), day of week(요일) 값을 다르게 주어서 변경할 수 있습니다. 매일 명령어가 실행되어도 실제로 인증서 갱신 작업은 만료가 가까워질 때만 수행되므로 서버에 무리가 가지 않습니다. 인증서를 갱신할 때에는 80번 포트와 443 포트가 열려 있어야 하기 때문에 임시로 80번 포트를 열고 인증서 갱신이 완료되면 80번 포트를 닫도록 설정을 했습니다. 이 작업은 동기적으로 이루어져서 인증서 갱신이 완료된 후에 80번 포트가 닫힐 것입니다.

# 인증서 상태 확인
sudo certbot certificates

# 임시로 방화벽 비활성화 or 80번 포트 허용
sudo ufw disable / sudo ufw allow 80

# 인증서 갱신
sudo certbot renew

# 다시 방화벽 활성화 / 80번 포트 차단
sudo ufw enable / sudo ufw deny 80

 

  만약 수동으로 인증서 갱신을 진행하려면 위와 같은 단계로 진행하면 됩니다.


4. 방화벽 설정 변경하기

sudo ufw allow 'Nginx HTTPS'
sudo ufw reload
sudo ufw status

 

  마지막으로 http 포트인 80 포트의 접근을 막고, https 포트인 443번 포트로만 접속이 되도록 방화벽의 설정을 변경합니다.

1. SSH 설치, 포트 변경하기, 방화벽 등록

  서버에 우분투를 설치했으면, 외부에서 터미널을 이용해 접속을 할 수 있도록 SSH 설정을 해주어야 합니다. 먼저 터미널을 실행하고, 아래와 같이 SSH 서버를 설치해 줍니다.

sudo apt-get update
sudo apt-get install openssh-server

 

  SSH 기본 포트는 22번 포트인데 이 포트를 그대로 사용하는 것은 보안 상 좋지 않기 때문에 22번 포트가 아닌 다른 포트를 사용해 주는 것이 좋습니다. 포트를 변경하기 위해서 아래의 명령어를 이용해 ssh 설정 파일을 수정해 줍니다. 

sudo vim /etc/ssh/sshd_config

 

  저는 기본 vi 에디터보다 vim에디터를 선호하지만, vi나 nano를 이용해도 됩니다. 에디터가 실행되었으면 i를 눌러 insert 모드에 진입하고 기본포트가 적혀있는 #Port 22 아래에 원하는 포트번호를 입력합니다. 그다음 esc를 눌러서 insert 모드에서 빠져나온 후 :wq를 입력해서 저장을 해줍니다.

 

  포트 번호 수정이 완료되었다면 이제 SSH에 접속할 때 아래와 같이 '-p <포트번호>'를 붙여서 접속합니다.

ssh -p <포트 번호> username@ip주소

 

  마지막으로 방화벽을 활성화하고 설정한 SSH 포트를 허용해 줍니다.

sudo ufw enable
sudo ufw allow <포트 번호>
sudo ufw reload
sudo ufw status

 

  만약 서버의 ip 주소를 모르겠다면 net-tools를 설치한 후에 ifconfig 명령어를 통해서 확인할 수 있습니다.

sudo apt install net-tools
ifconfig

2. SSH 키 등록하고 비밀번호 접속 막기

  SSH 키를 생성해서 키를 가지고 있는 사용자만 접속할 수 있게 하면 로그인할 때 굳이 비밀번호를 입력하지 않아도 되고 보안을 크게 향상시킬 수 있습니다. 이 작업은 외부 컴퓨터에서 SSH 접속을 한 상태에서 진행하면 키 파일을 따로 추출하지 않아도 키 내용을 복사해서 Termius에 바로 붙여 넣을 수 있기 때문에 서버 내부가 아닌 SSH 터미널에서 진행하시는 것을 권장합니다. 먼저 터미널에서 아래의 명령어를 입력해서 개인키를 생성해 주세요.

ssh-keygen -t rsa -b 4096 -m PEM

 

  'Enter passphrase (empty for no passphrase)' 입력 창에서 아무것도 입력하지 않고 Enter를 누르면 비밀번호가 없는 키가 만들어지고, 비밀번호를 입력하면 비밀번호가 있는 키가 만들어집니다. 비밀번호를 만들어도 나중에 접속 프로그램에서 설정해 줄 때 한 번만 입력하면 되기 때문에, 저는 비밀번호가 있는 키를 만들었습니다. 비밀번호 입력까지 마치고 Enter를 누르면 아래와 같이 키가 생성된 것을 볼 수 있습니다.

 

 cat 명령어를 이용해서 id_rsa.pub 파일의 public key 내용이 출력되면 Ctrl+Shift+C를 눌러서 복사를 해줍니다. 그리고 echo "public key" 부분에 복사한 내용을 Ctrl+Shift+V를 이용해서 붙여 넣으면 authorized_keys 파일에 public key가 추가됩니다. 다시 cat 명령어를 통해 authorized_keys의 내용을 확인해 보면 붙여넣기한 공개 키가 잘 등록된 것을 볼 수 있습니다.

cat ~/.ssh/id_rsa.pub
echo "public key" >> ~/.ssh/authorized_keys

cat ~/.ssh/authorized_keys

 

  .ssh폴더 안에는 authorized_keys, id_rsa, id_rsa.pub 파일이 있습니다. id_rsa 파일은 private key 파일이고 id_rsa.pub 파일은 public key 파일입니다. 나중에 다른 컴퓨터에서 ssh를 이용해 접속하기 위해서는 private key인 id_rsa 파일이 필요하니 파일을 따로 복사해 두거나 아래와 같이 cat 명령어를 이용해서 파일의 내용을 복사해두어야 합니다. 

cat ~/.ssh/id_rsa

 

  등록을 마쳤으면 이제 ssh 설정에 들어가서 비밀번호로 로그인하는 기능을 막아야 합니다. 다시 에디터를 이용하여 sshd_config 파일의 설정을 변경해 줍니다.

sudo vim /etc/ssh/sshd_config

 

  설정 파일이 열렸으면 i를 눌러 insert 모드로 들어가서  #로 주석처리 되어있는 부분을 수정해 줍니다.

 

  먼저 PubkeyAuthentication을 Yes로 바꿔주고, AuthorizedKeysFile 부분이 주석처리가 되어있는 것을 #를 삭제해서 해제해 줍니다. 그다음 PasswordAuthentication을 no로 바꿔주면 비밀번호를 이용하여 로그인을 할 수 없게 되고 키가 있어야만 로그인이 가능해집니다. 수정을 다 했다면 esc를 누른 후에 :wq를 눌러 저장을 하고 에디터를 빠져나옵니다. 

 

  참고로 PermitRootLogin을 no로 설정하면 root 계정으로 로그인이 불가능해지고, prohibit-password로 설정하면 키를 가지고 있는 사용자의 root 계정 로그인만 허용합니다. 이미 키를 사용해서만 로그인이 되게 해 놨기 때문에 굳이 수정하지는 않았습니다.


3. Termius를 이용해서 SSH 접속하기

  SSH 접속 프로그램으로 putty와 같은 프로그램을 많이 사용하지만, 저는 UI가 예쁘고 한 번 설정해 두면 맥과 윈도우, 리눅스 모두에서 사용할 수 있는 Termius를 더 선호하는 편입니다. Termius를 이용해서 발급받은 키를 이용해 SSH 접속을 하는 과정은 다음과 같습니다.

 

  먼저 프로그램을 설치해서 로그인을 한 후에 NEW HOST를 눌러서 HOST 등록을 해줍니다. Address 부분에는 SSH 서버의 IP를 입력해 줍니다. SSH on <포트 번호> port에서 위에서 설정해 준 SSH의 포트 번호를 입력하고, Username을 입력해 줍니다.

 

  '+ Key, Certicicate, FIDO2' 버튼을 클릭해서 Key를 선택하고 label 이름을 입력한 후 엔터를 누르면 아래와 같이 New Key 화면으로 넘어갑니다. import 탭에서 Import from key file을 눌러서 서버에서 복사해 온 id_rsa 파일을 가져오거나, 복사한 id_rsa 파일의 private key 내용을 Paste 탭을 눌러서 Private key 부분에 붙여넣기를 해줍니다. 만약 앞에서 설정해 준 Passphrase가 있다면 입력해 줍니다.

 

  키 입력이 완료되면 다시 Host 설정창으로 돌아와서 Connect를 누르면 SSH 접속이 정상적으로 되는 것을 확인할 수 있습니다.

  미니 PC를 웹서버 용도로만 쓸 예정이라 큰 모니터에 연결을 할 필요는 없고 가끔 서버 상태 확인 용도로 작은 모니터가 필요할 것 같아 Hagibis IPS 미니 모니터를 구입했습니다.

 

  Aliexpress에서 28.70달러에 구입을 했고, 리뷰를 보니 대부분 해상도가 업그레이드된 2세대를 받았다고 해서 바로 구매 버튼을 눌렀습니다.

 

  상자를 보면 1세대인지 2세대인지 알 수 있다고 했는데, 막상 물건을 받아보니 몇 세대인지 적혀있지 않았습니다. 검색해 보니 SKU ID가 다르다고 하는데 뜯어서 직접 연결해서 해상도를 확인하기 전까지는 2세대인지 확인할 수 없었습니다. 다행히도 컴퓨터에 연결한 후에 해상도가 960x640으로 떠서 2세대인 것을 알았습니다. SKU ID가 609790이거나 아래 Q/HBSKJ에 2022가 써져 있으면 아마도 2세대인 것 같습니다.

 

  여하튼 AM02는 Type C 포트가 있어서 미니 모니터와 C to C 케이블을 이용해 바로 연결을 할 수 있습니다. 다른 C to C 케이블로 연결했을 때는 화면이 나오지 않았는데, 제품에 동봉된 케이블을 이용해서 연결하니 제대로 화면이 표시됐습니다. 저는 각도를 바꿔주는 젠더를 사용해서 깔끔하게 선 방향을 바꿔주었습니다. 만약 Type C 포트가 뒤에도 있었으면 더 좋았을 것 같다는 생각이 들기도 합니다.

 

  우분투에 Stacer를 설치해서 실시간으로 CPU와 Memory, Disk 모니터링을 해봤습니다. Stacer가 리소스를 많이 차지하지 않을 것 같았는데, 은근히 리소스를 많이 사용해서 프로세스를 종료하고 모니터를 꺼놨습니다. 미니 모니터는 책상 꾸미기 용으로 그냥 올려놓고, 가끔 미니 pc 상태를 확인할 때만 켜야 할 것 같습니다.

 

  AM02 모델에 기본으로 들어가 있는 Wicgtyp SSD가 어떤 제품인지 찾아봤습니다. FIREBAT의 제품뿐만 아니라 다른 제조사의 미니 PC에도 자주 탑재되는 SSD인 것 같습니다. NVMe 모델인 NV900-512 모델이 들어갔으면 좋았겠지만, 일반 M.2 SATA SSD 모델인 N900-512 모델이 들어가 있습니다.

 

  SSD 정보를 확인해 보면 Micron의 MLC 낸드와 실리콘모션의 SM2259XA 컨트롤러로 구성되어 있는 것을 볼 수 있습니다. 위 프로그램에서 나온 정보가 정확하지 않을 수도 있으니 참고용으로 봐주시길 바랍니다. (프로그램 링크)

 

  컨트롤러 제조사의 공식 문서를 보니 모델명 뒤에 XT가 붙은 모델은 DRAM-less 모델인 것 같네요.

 

  SSD 리뷰를 찾아보니 몇 달 정도 썼는데 갑자기 급사했다는 얘기가 있어서, 메인 SSD로 사용하기에는 불안했습니다. 그래서 SSD 인클로저를 구입해 외장 저장장치로 사용하기로 했습니다. 마침 Aliexpress 꽁돈대첩에서 미니 PC와 같은 회사의 SSD 인클로저가 세일 중이라 약 7,000원 정도에 NVMe와 SATA가 모두 지원되는 모델을 구입을 했습니다. 디자인이 Orico의 인클로저와 똑같은 걸로 봐서는 같은 공장에서 생산된 물건인 것 같습니다.

 

  구성품은 방열판과 서멀 패드, 고무 나사 2개, C to C 케이블입니다. C to USB 젠더가 없기 때문에 USB 포트에서 사용하기 위해서는 젠더를 따로 구입해야 합니다. 저는 일반 SATA SSD를 넣었기 때문에 굳이 써멀패드와 방열판은 설치하지 않았습니다. 참고로 연결 후 디스크 속성을 확인해 보면 모델명이 Realtek RTL9210B-CG로 표시됩니다.

 

  CristalDiskMark로 속도 측정을 해본 결과 컨트롤러 스펙 시트만큼 속도가 나오는 것을 볼 수 있습니다.

+ Recent posts