開發基於Django和Websocket的堡壘機
WebSSH有很多,基於Django的Web服務也有很多,使用Paramiko在Python中進行SSH訪問的就更多了。但是通過gevent將三者結合起來,實現通過瀏覽器訪問的堡壘機就很少見了。本文將簡要介紹下我開發的IronFort堡壘機,其詳細內容在我的官方網站liujiangblog.com的影片教程中。
一、堡壘機概述
百度百科:堡壘機,在一個特定的網路環境下,為了保障網路和數據不受來自外部和內部用戶的入侵和破壞,而運用各種技術手段實時收集和監控網路環境中每一個組成部分的系統狀態、安全事件、網路活動,以便集中報警、及時處理及審計定責。
對於一個中型以上的公司,當用戶和職員人數較多,公司所屬伺服器也數量較大的情況下,其伺服器上的帳號管理難度將急劇增加,參考下面的圖片:
這其中必然存在很多問題,例如:
- 用戶、主機、帳號數量太多,工作量大,管理混亂;
- 每個人員的許可權和可使用帳號沒有系統管理,等級區分不明;
- 用戶直接掌握主機的帳號密碼;
- 密碼可能交叉使用;
- 離職人員可能還可以使用公司的帳號;
- 內部人員可以跳過防火牆,直接使用帳號在機房內訪問;
- 內部人員離職前設下木馬或暗門,一段時間後再爆發;
- 對人員的訪問記錄、過往操作沒有日誌和審計,缺乏事後追蹤手段;
- 其它風險
在運行初期,公司可能採取Excel表格等工具,使用人工管理的方式,靠『人治』和道德水平約束,但當公司體量逐漸變大的時候,這種方式必然遭到淘汰,於是就出現了堡壘機的概念,如下圖所示:
這種架構帶來如下的好處:
- 用戶不能直接訪問遠程主機,而是需要通過堡壘機跳轉;
- 用戶不再掌握遠程主機的帳號密碼,只有訪問堡壘機的帳號;
- 限制用戶登錄遠程主機後的修改密碼能力,不允許修改;
- 堡壘機的用戶、遠程主機的用戶、用戶密碼、用戶許可權等等都被統一集中管理,大量節省人工成本;
- 用戶在登錄堡壘機後所進行的一切操作將被記錄下來,用於後期的行為審計;
- 由於沒有遠程主機帳號密碼,即使進入機房也無法直連主機;
- 還可以實現批量命令執行、文件分發等附帶功能;
- 其它收益。
堡壘機的核心概念是用戶不再掌握帳號密碼,用戶的行為被記錄用於審計。堡壘機主要針對的是內部網路和內部人員,對於人員流動性較強、體量大、行業風險高的企業需求特彆強烈,比如金融行業。
堡壘機已經擁有商業產品,多數以硬體伺服器為載體進行銷售,價格幾十萬不等。也有開源的解決方案,但這些方案有的不是基於瀏覽器,介面不夠友好,日誌記錄困難;有的基於Tornado,並且只能進行簡單的命令執行功能,而公司使用的是Django;更多的情況是與公司需求不一致,需要二次開發,維護和升級困難,等等不一而足。
『授人以魚不如授人以漁』,自己掌握了開發堡壘機的核心技能,就可以快速、方便、靈活的針對公司具體需求進行訂製開發,既為公司節省了購置硬體經費,又利於維護升級。
二、 IronFort堡壘機體系架構
IronFort堡壘機的體系架構如下圖所示:
一個完整的通訊過程如下:
- 用戶通過使用支援HTML5的瀏覽器,在HTTP的基礎上,向堡壘機發送websocket請求;
- 堡壘機上使用gevent接收websocket請求並轉發給Django;
- Django接收請求後,調用paramiko建立與遠程主機的ssh通道;
- 遠程主機執行用戶的命令後,通過ssh返回數據給Django;
- Django通過gevent以websocket的形式返回給用戶瀏覽器;
- 用戶瀏覽器使用term.js插件模擬Linux終端,顯示遠程主機返回的結果。
核心機制就是這樣,下面我們來看下開發過程。
三、開發簡介
1. 項目創建
堡壘機本身通常是布置在Linux主機上的,比ubuntu16.04,對外以HTTP的形式提供服務。
首先需要建立虛擬環境,並安裝Python3.6以及Django2.0,不再贅述。
使用django-admin startproject
和python manage.py startapp app_name
分別創建項目和app。
此時,可以嘗試運行Django服務,可以看到歡迎介面。
2. ORM模型
任何一個Web項目都必須在深入分析項目需求的情況下,首先設計好ORM模型,也就是資料庫的表結構。
IronFort中設計了六個模型,分別是:
- 遠程主機
- 遠程主機用戶
- 遠程主機綁定的用戶
- 堡壘機用戶
- 堡壘機用戶組
- 日誌
這裡需要提醒的是:
- 每個遠程主機賬戶可以綁定多個遠程主機,兩者實際是多對多的關係;
- 堡壘機用戶不能直接綁定遠程主機;
- 堡壘機用戶綁定的實際是一個主機+主機賬戶的對象;
- 考慮賬戶是否激活或者被經用的enabled屬性;
- 考慮某些欄位的unique_together屬性;
關於模型設計,每個人有每個人的需求和想法,這其中有很多坑和需要注意的地方,限於篇幅,無法展開論述。在我的個人網站liujiangblog.com的影片教程中有詳細的講解。
模型設計好了,可以同時註冊Django的admin後台。然後makemigrations、migrate和createsuperuser,重啟伺服器後就可以在admin中創建測試用例了,如下圖所示:
3. url和路由
url的設計並不複雜,沒有太多的複雜頁面,下面是項目中使用的一些url:
from django.contrib import admin
from django.urls import path, re_path
from fort import views
urlpatterns = [
path('admin/', admin.site.urls),
path('', views.login),
path('login/', views.login),
path('logout/', views.logout),
path('index/', views.index),
path('log/', views.get_log),
path('host/<int:user_bind_host_id>/', views.connect),
]
Django2.0的url語法向flask等框架靠攏了,但依然可以使用正則模式。關於2.0和之前版本的區別,可以查看我曾經寫過的一篇博文Django 2.0 新特性 搶先看!。其實不是重度使用者,基本感受不出變化來,該怎麼用還是怎麼用。最大的區別也就在url編寫,和Python2及3的支援。
4. 前端框架AdminLTE
為了讓用戶介面美觀,我這裡使用了基於bootstrap的開源框架AdminLTE。
AdminLTE託管在GitHub上,可以通過下面的地址下載:
//github.com/almasaeed2010/AdminLTE/releases
AdminLTE自帶JQuery和Bootstrap3,無需另外下載。
AdminLTE自帶多種配色皮膚,可根據需要實時調整。
AdminLTE是移動端自適應的,無需單獨考慮。
AdminLTE自帶大量插件,比如datatables,可根據需要載入。
但是AdminLTE的源文件包內,缺少font-awesome-4.6.3和ionicons-2.0.1這兩個圖標插件,它是通過CDN的形式載入的,如果網路不太好,載入可能比較困難或者緩慢,最好用本地靜態文件的形式,請自定下載並引入項目內。
我們不需要AdminLTE那麼多的功能,只需要它的基本框架。在其源碼包內,對index文件進行裁剪和靜態文件導入處理,形成一個基本的base.html用於拓展,在它的基礎上,我們可以擴展出index和log頁面。
5. 堡壘機用戶登錄頁面
堡壘機用戶登錄頁面不需要使用AdminLTE,最好是單獨一個簡單的頁面,展示的內容越少越好。
而用戶登錄的處理視圖就很簡單了,直接使用Django內置的Auth認證系統。
使用Django自帶的authenticate和login方法就可以完成用戶驗證和登錄會話。
既然有了登錄,必然就要有登出。為了限制未登錄用戶訪問堡壘機系統,所有的相關視圖都必須先使用裝飾器進行是否登錄驗證。
通常而言,堡壘機不需要提供面向用戶的註冊頁面。堡壘機用戶的註冊都是超級管理員掌控的,在後台進行!
6. 主機帳號頁面
也就是我們堡壘機用戶登錄進系統後,顯示的默認頁面index。這裡將通過表格的形式,列出當前堡壘機用戶可以使用的遠程主機帳號。視圖很簡單:
@login_required(login_url='/login/')
def index(request):
# ...通過ORM的API查詢可使用的帳號
return render(request, 'fort/index.html', locals())
主機賬戶的前端頁面index基於base.html,使用datatable插件,提供搜索、排序和分頁等高級功能,其展示效果如下圖:
7. 在瀏覽器中打開websocket通道
百度百科:WebSocket協議是基於TCP的一種新的網路協議。它實現了瀏覽器與伺服器全雙工(full-duplex)通訊——允許伺服器主動發送資訊給客戶端。
本文不打算成為一篇websocket的科普文,有興趣深入研究的可以查看部落格園的精華博文WebSocket協議:5分鐘從入門到精通
簡單的說,有以下幾點:
- HTTP本身是無狀態連接,不支援實時通訊;
- websocket基於HTML5,需要瀏覽器支援;
- 通過在http報頭中添加upgrade屬性,申請通訊協議升級為websocket;
- 升級成為websocket通訊後,可以實現瀏覽器和遠程伺服器之間的全雙工實時通訊。
關於websocket的使用教程,可以參考阮一峰專家的博文WebSocket 教程
其具體API如下圖所示:
要簡單的創建並使用一個websocket,按下面的套路就可以了:
- 使用
new WebSocket(url, [protocol] );
創建ws對象 - 使用ws,調用onopen、onmessage、onerror和onclose方法處理通訊過程中的數據
- 使用ws,調用send方法發送數據給後端伺服器
- 使用ws,調用close方法,關閉websocket連接。
我們在主機帳號表格中隱藏一個主機帳號id的欄位,通過js程式碼獲取該欄位的值,然後啟動websocket通訊,傳遞這個id作為參數之一,用於構造websocket通訊使用的url。
在瀏覽器模擬Linux終端方面,我使用的是term.js插件。這是一個開源在github上的瀏覽器模擬Linux終端的js插件,地址為://github.com/chjj/term.js
。其官方文檔比較簡單,有興趣的同學可以深入研讀其源程式碼,或者使用xterm作為替代。
最終效果如下:
因為此時後端還沒有完成,所以是連接不上任何主機的。
8. 創建websocket伺服器
Django本身是一個同步Web框架,也不支援websocket。所以你使用它的runserver,是無法接收和處理websocket請求的。為了解決這個問題,可以使用gevent這個Python的第三方非同步網路框架。
gevent基於greelet協程庫,自帶有WSGI伺服器,並且其擴展庫gevent-websocket支援websocket通訊。
請先用pip install gevent gevent-websocket
安裝這兩個庫。
在IronFort項目根目錄下創建一個start_ironfort.py
腳本,以後這就是我們的服務啟動腳本了。
from gevent import monkey
monkey.patch_all()
from gevent.pywsgi import WSGIServer
from geventwebsocket.handler import WebSocketHandler
from ironfort.wsgi import application
print('ironfort is running ......')
ws_server = WSGIServer(
(host, port),
application,
log=None,
handler_class=WebSocketHandler
)
try:
ws_server.serve_forever()
except KeyboardInterrupt:
print('伺服器關閉......')
pass
核心要點是,使用gevent的WSGIServer伺服器代替DJango的runserver,使用geventwebsocket的WebSocketHandler來處理瀏覽器發送過來的websocket通訊請求,並將其轉發到Django的application。
我們知道Django的通訊入口就存在於from ironfort.wsgi import application
中的這個方法。通過gevent的幫助,我們讓Django具備了接收websocket通訊請求的能力。
運行python start_ironfort
可以啟動新的伺服器,在瀏覽器驗證一下,都可以正常訪問。
9. 在Django中創建視圖處理websocket請求
我們前面的根路由中已經寫了相關的url,這裡再貼出來:
path('host/<int:user_bind_host_id>/', views.connect),
這樣,以ws://ip:port/host/15/
形式的url請求,將被轉發到connect視圖進行處理,這其中傳遞了『15』這個主機帳號id的參數。具體connect視圖局部程式碼如下:
@login_required(login_url='/login/')
def connect(request, user_bind_host_id):
# 如果當前請求不是websocket請求則退出
# ...省略
# 獲取remote_user_bind_host
bridge = WSSHBridge(request.environ.get('wsgi.websocket'), request.user)
try:
bridge.open(
host_ip=remote_user_bind_host.host.ip,
port=remote_user_bind_host.host.port,
username=remote_user_bind_host.remote_user.remote_user_name,
password=remote_user_bind_host.remote_user.password
)
except Exception as e:
message = '嘗試連接{0}的過程中發生錯誤:\n {1}'.format(
remote_user_bind_host.remote_user.remote_user_name, e)
print(message)
add_log(request.user, message, log_type='2')
return HttpResponse("錯誤!無法建立SSH連接!")
bridge.shell()
request.environ.get('wsgi.websocket').close()
print('用戶斷開連接.....')
return HttpResponse("200, ok")
說明:
- 獲取id對應的遠程帳號;
- 調用WSSHBridge()方法,傳入websocket對象和當前用戶,創建一個websocket和ssh通訊的橋接類,這個類一會我們會介紹。
- 調用open方法啟動ssh通訊;
- 調用shell方法啟動終端環境;
- 通訊結束後調用close方法,關閉通道。
那麼這裡的WSSHBridge類是什麼呢?
10. WSSHBridge橋接通訊類
WSSHBridge:
import gevent
from gevent.socket import wait_read, wait_write
import paramiko
import json
class WSSHBridge:
"""
橋接websocket和SSH的核心類
"""
def __init__(self, websocket, user):
self.user = user
self._websocket = websocket
self._tasks = []
#...
def open(self, host_ip, port=22, username=None, password=None):
""" 建立SSH連接 """
pass
def _forward_inbound(self, channel):
""" 正向數據轉發,websocket -> ssh """
pass
def _forward_outbound(self, channel):
""" 反向數據轉發,ssh -> websocket """
pass
def _bridge(self, channel):
""" 橋接websocket和ssh """
pass
def close(self):
""" 結束橋接會話 """
pass
def shell(self):
""" 啟動一個shell通訊介面 """
pass
首先需要pip install paramiko
安裝模組。
WSSHBridge類,本質上就是橋接websocket通道和paramiko打開的ssh通道,進行數據雙向轉發。
open方法調用paramiko的相關API,傳入主機ip、port、用戶名和密碼,打開ssh通道,_forward_inbound
和_forward_outbound
方法分別實現數據的正向和反向轉發。
核心的關鍵是_bridge
方法:
self._tasks = [
gevent.spawn(self._forward_inbound, channel),
gevent.spawn(self._forward_outbound, channel),
]
gevent.joinall(self._tasks)
使用gevent的spawn方法創建了兩個協同任務,然後調用joinall方法等待它們任務結束。這樣就實現了數據在websocket通道和ssh通道之間的一發一收,一收一發的通訊機制。
這一步完成後,重啟伺服器,我們就可以來展示整個通訊過程了。
首先是,連接成功:
其次是類似Python這種互動式命令:
然後是top這種動態命令結果返回:
最後是vim這種編輯環境:
可以看到,我們是支援彩色輸出的:
11. 日誌記錄和行為審計
關於用戶操作,在數據由websocket往ssh發送過程中,可以保存用戶通過前端Linux模擬器終端所敲擊的所有按鍵記錄,並且很規整的以回車鍵進行分隔,非常容易判別。
我們只需要創建一個日誌模型,編寫一個保存日誌的方法,然後在需要的位置保存日誌即可。
日誌展示頁面非常類似主機賬戶的頁面,同樣使用datatable插件進行處理,最終效果如下圖所示:
至此,基於Webssh的堡壘機核心功能就開發完畢了。限於篇幅,不可能點點滴滴、枝葉不漏的全部敘述,我這裡也只是一個拋磚引玉的過程。
四、總結
遠程主機的創建、主機帳號的管理、堡壘機用戶和用戶組的管理,這一系列的工作,目前我還是放在admin後台中進行。後期,大家可以將它遷移到堡壘機頁面中一起管理。如果將IronFort用於生產環境,添加批量命令執行、文件分發功能,進行系統部署上線、結合Linux運維等等,必然需要大量的額外工作和安全機制,這些就留給大家自己去研究了。