[프로젝트] 접근 제어 - 웹 브라우저
(설명) 본 프로젝트에서는 로컬 서버의 ~/.ssh/config 파일에 대상 서버(HostName)와 중앙 서버(ProxyJump)를 등록하여
로컬 서버에서 대상 서버에 ssh 접속 시, 중앙 서버를 거쳐 대상 서버에 접속되는 상황을 구현했습니다.
이때 중앙 서버의 방화벽 정책(iptables)을 통하여 대상 서버로의 접근을 허용/차단 할 수 있습니다.
※ 아래의 코드는 기본 전체 허용 입니다.
● 환경
(OS) Rocky Linux release 8.10 (Green Obsidian)
(서버 1) 192.168.112.222, 로컬 서버 - 사용자
(서버 2) 192.168.112.218, 중앙 서버 - 접근 제어
(a) 로컬 서버
(1) 구조
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
local_server/
├── app.py
├── templates/
│ └── index.html
├── requirements.txt
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
(2) 파일
(파일) requirements.txt
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
Flask
Flask-SQLAlchemy
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
(파일) ~/.ssh/config
※ 대상 서버와 중앙 서버(프록시 서버)를 등록하는 파일입니다.
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
Host slave
HostName 192.168.112.220
User root
ProxyJump root@192.168.112.219
Host galera1
HostName 192.168.112.210
User root
ProxyJump root@192.168.112.219
...
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
(파일) app.py
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
# Import the libraries
from flask import Flask, render_template, request, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy
import os
# Creat Instance
# - app : Flask 인스턴스
app = Flask(__name__)
# Create Secret Key
app.secret_key = 'supersecretkey'
# Configure SQLAlchemy
# - ssh_config.db : 생성할 데이터베이스명
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///ssh_config.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
# (1) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///ssh_config.db'
# : SQLAlchemy 클래스 Import and SQLAlchemy 데이터베이스 URI 지정
# (2) db = SQLAlchemy(app) : SQLAlchemy 클래스 인스턴스 생성 및 Flask 인스턴스 app 인수 전달 초기화
# - db : 인스턴스
# Define the database model
# - SSHConfigEntry : 사용자 정의 모델
class SSHConfigEntry(db.Model):
id = db.Column(db.Integer, primary_key=True)
host = db.Column(db.String(80), nullable=False)
hostname = db.Column(db.String(120), nullable=False)
user = db.Column(db.String(80), nullable=False)
# (1) db.Model : db.Model 상속받아 데이터베이스 테이블과 매핑
# Initialize the database
# - app_context() : Flask 애플리케이션 컨텍스트
# : 데이터베이스 등의 리소스 접근 시 필요
with app.app_context():
# - create_all() : 함수
# db.create_all() : db.Model 상속받는 모든 모델에 대해 데이터베이스 생성
db.create_all()
# ~/.ssh/config 파일 및 데이터베이스에 새 항목 추가
def add_ssh_config_entry(host, hostname, user):
# - os.path.expanduser() : 실제 경로로 확장
# - os.path.dirname() : 다렉터리 부분만 추출
config_path = os.path.expanduser("~/.ssh/config")
config_dir = os.path.dirname(config_path)
# ~/.ssh 디렉토리가 존재하지 않으면 생성
# - os.path.exists() : 존재 여부 출력(True/False)
if not os.path.exists(config_dir):
# - os.makedirs : 해당 경로 생성
os.makedirs(config_dir)
# ~/.ssh/config 파일이 존재하지 않으면 생성
if not os.path.exists(config_path):
# Write the current config file
with open(config_path, 'w') as config_file:
# 파일 생성 후 기본 내용 작성
config_file.write("# SSH Config File\n")
# 새 항목 추가
new_entry = f"""
Host {host}
HostName {hostname}
User {user}
ProxyJump root@192.168.112.219
"""
# ~/.ssh/config 파일에 새 항목 추가
# Append the current config file
with open(config_path, 'a') as config_file:
config_file.write(new_entry)
# Save to database
# 인스턴스 생성
# - entry : 인스턴스
entry = SSHConfigEntry(host=host, hostname=hostname, user=user)
# - session.add() : 인스턴스를 데이터베이스 세션에 추가
# - session.commit() : 세션에 추가된 모든 변경사항을 데이터베이스에 영구적으로 반영
db.session.add(entry)
db.session.commit()
# 메시지 반환
return f"New entry added to {config_path} and database."
# ~/.ssh/config 파일 및 데이터베이스에 해당 항목 제거
def delete_ssh_config_entry(entry_id):
# - os.path.expanduser() : 실제 경로로 확장
# - SSHConfigEntry.query.get() : 데이터베이스에서 항목 가져오기
config_path = os.path.expanduser("~/.ssh/config")
entry = SSHConfigEntry.query.get(entry_id)
# 해당 항목 존재 시, ~/.ssh/config 파일에서 제거하기 위해서 그 외 호스트 블록 저장하는 리스트 생성하여 덮어쓰기
if entry:
# Read the current config file
with open(config_path, 'r') as config_file:
# - config_file.readlines() : 파일의 모든 라인을 리스트로 읽어들이기
lines = config_file.readlines()
# ~/.ssh/config 파일에서 삭제할 항목 찾기
# 해당 line이 삭제할 항목이 아닐 경우(skip = False일 경우) new_lines에 저장
new_lines = []
skip = False
for line in lines:
# - startswith() : 문자열이 특정 접두사로 시작하는지 확인
if line.startswith(f"Host {entry.host}"):
skip = True
# - line.strip() == "" : 양쪽의 모든 공백 제거 후, 문자열 비었는지 확인
# : 호스트 블록의 끝으로 식별하여 다음 호스트 블록 처리하기 위한 것
if skip and line.strip() == "":
skip = False
continue
if not skip:
new_lines.append(line)
# Write the new config back to the file
# : 생성한 new_lines으로 덮어쓰기
with open(config_path, 'w') as config_file:
config_file.writelines(new_lines)
# Remove from the database
# - session.delete() : 인스턴스를 데이터베이스 세션에서 제거
# - session.commit() : 세션에 추가된 모든 변경사항을 데이터베이스에 영구적으로 반영
db.session.delete(entry)
db.session.commit()
# 메시지 반환
return f"Entry {entry.host} deleted from {config_path} and database."
else:
return "Entry not found."
@app.route('/')
def index():
entries = SSHConfigEntry.query.all()
return render_template('index.html', entries=entries)
@app.route('/add', methods=['POST'])
def add_entry():
host = request.form['host']
hostname = request.form['hostname']
user = request.form['user']
message = add_ssh_config_entry(host, hostname, user)
flash(message)
return redirect(url_for('index'))
@app.route('/delete/<int:entry_id>', methods=['POST'])
def delete_entry(entry_id):
message = delete_ssh_config_entry(entry_id)
flash(message)
return redirect(url_for('index'))
# 어플리케이션 실행
# - if __name__ == '__main__': 해당 스크립트가 직접 실행도리 때만 아래의 코드 실행
# - run() : 어플리케이션을 로컬 개발 서버에서 실행
if __name__ == '__main__':
app.run(host='0.0.0.0', port=2024)
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
(파일) index.html
-----------------------------------------------------------------------------------------------------------------------------------------------------------------<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>SSH Config Manager</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
</head>
<body>
# "container" : 페이지 내용 가운데 정렬
<div class="container">
# "mt-5" : 상단 여백
<h1 class="mt-5">SSH Config Manager</h1>
# 추가 폼
<form action="{{ url_for('add_entry') }}" method="post" >
<div class="form-group">
<label for="host">Host</label>
<input type="text" class="form-control" id="host" name="host" required>
</div>
<div class="form-group">
<label for="hostname">HostName</label>
<input type="text" class="form-control" id="hostname" name="hostname" required>
</div>
<div class="form-group">
<label for="user">User</label>
<input type="text" class="form-control" id="user" name="user" required>
</div>
<button type="submit" class="btn btn-primary">Add Entry</button>
</form>
# 기존 목록
<h2 class="mt-5">Existing Entries</h2>
<ul class="list-group">
{% for entry in entries %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<strong>Host:</strong> {{ entry.host }}<br>
<strong>HostName:</strong> {{ entry.hostname }}<br>
<strong>User:</strong> {{ entry.user }}
</div>
<form method="post" action="{{ url_for('delete_entry', entry_id=entry.id) }}">
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</li>
{% endfor %}
</ul>
# 플래시(flash) 메시지
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="alert alert-success mt-3" role="alert">
{{ messages[0] }}
</div>
{% endif %}
{% endwith %}
</div>
</body>
</html>
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
● 실행
Step1. 선수 작업 진행
[참고] https://uyijune15.tistory.com/222
[정리] 선수 작업
1) python3 설치# yum install -y python3 2) 최신 패키지 업데이트# yum -y update# pip install --upgrade pip 3) 필요 패키지 설치# yum -y install gcc# pip3 install gunicorn
uyijune15.tistory.com
Step2. 방화벽 설정
# firewall-cmd --permanent --add-port=2024/tcp
# firewall-cmd --reload
Step3. 작업 디렉토리 이동
Step4. 필수 패키지 설치
# pip3 install -r requirements.txt
Step5. 실행
# gunicorn -w 4 -b 0.0.0.0:2024 app:app
● 테스트
(b) 중앙 서버 (프록시 서버)
(1) 구조
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
proxy_server/
├── app.py
├── templates/
│ └── index.html
├── requirements.txt
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
(2) 파일
(파일) requirements.txt
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
Flask
Flask-SQLAlchemy
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
(파일) app.py
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
from flask import Flask, render_template, request, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy
import subprocess
app = Flask(__name__)
app.secret_key = 'supersecretkey'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///iptables_rules.db'
db = SQLAlchemy(app)
class Rule(db.Model):
id = db.Column(db.Integer, primary_key=True)
ip = db.Column(db.String(15), unique=True, nullable=False)
with app.app_context():
db.create_all()
def add_iptables_rule(ip):
try:
command = f"sudo iptables -A OUTPUT -p tcp -d {ip} --dport 22 -j REJECT"
subprocess.check_call(command, shell=True)
new_rule = Rule(ip=ip)
db.session.add(new_rule)
db.session.commit()
return f"Rule added for IP: {ip}"
except subprocess.CalledProcessError as e:
return f"Failed to add rule: {e}"
def delete_iptables_rule(ip):
try:
command = f"sudo iptables -D OUTPUT -p tcp -d {ip} --dport 22 -j REJECT"
subprocess.check_call(command, shell=True)
rule = Rule.query.filter_by(ip=ip).first()
if rule:
db.session.delete(rule)
db.session.commit()
return f"Rule deleted for IP: {ip}"
except subprocess.CalledProcessError as e:
return f"Failed to delete rule: {e}"
@app.route('/')
def index():
rules = Rule.query.all()
return render_template('index.html', rules=rules)
@app.route('/add', methods=['POST'])
def add_rule():
ip = request.form['ip']
if Rule.query.filter_by(ip=ip).first():
message = f"Rule already exists for IP: {ip}"
else:
message = add_iptables_rule(ip)
flash(message)
return redirect(url_for('index'))
@app.route('/delete', methods=['POST'])
def delete_rule():
ip = request.form['ip']
message = delete_iptables_rule(ip)
flash(message)
return redirect(url_for('index'))
if __name__ == '__main__':
app.run(host='0.0.0.0', port=2024)
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
(파일) index.html
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>IPTables Rule Manager</title>
</head>
<body>
<h1>Manage IPTables Rules</h1>
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<h2>Current Rules</h2>
<table>
<tr>
<th>IP Address</th>
<th>Action</th>
</tr>
{% for rule in rules %}
<tr>
<td>{{ rule.ip }}</td>
<td>
<form action="{{ url_for('delete_rule') }}" method="post" style="display:inline;">
<input type="hidden" name="ip" value="{{ rule.ip }}">
<input type="submit" value="Delete">
</form>
</td>
</tr>
{% endfor %}
</table>
<h2>Add Rule</h2>
<form action="{{ url_for('add_rule') }}" method="post">
<label for="ip">IP Address:</label>
<input type="text" id="ip" name="ip">
<input type="submit" value="Add">
</form>
</body>
</html>
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
● 실행
Step1. 선수 작업 진행
[참고] https://uyijune15.tistory.com/222
[정리] 선수 작업
1) python3 설치# yum install -y python3 2) 최신 패키지 업데이트# yum -y update# pip install --upgrade pip 3) 필요 패키지 설치# yum -y install gcc# pip3 install gunicorn
uyijune15.tistory.com
Step2. 방화벽 설정
# firewall-cmd --permanent --add-port=2024/tcp
# firewall-cmd --reload
Step3. 작업 디렉토리 이동
Step4. 필수 패키지 설치
# pip3 install -r requirements.txt
Step5. 실행
# gunicorn -w 4 -b 0.0.0.0:2024 app:app
● 테스트
● 접속 제어 테스트
=> 로컬 서버에서 호스트 등록 후 ssh 접속 시
=> 중앙 서버(프록시 서버)에서 방화벽 정책 등록 후 ssh 접속 시