IronFort---基於Django2.0和Websocket的堡壘機

原創內容,轉載需在頂部明顯位置註明來源及做者!侵權必究!

該實戰項目已經錄製視頻教程,並與網易雲課堂合做,直達連接


WebSSH有不少,基於Django的Web服務也有不少,使用Paramiko在Python中進行SSH訪問的就更多了。可是經過gevent將三者結合起來,實現經過瀏覽器訪問的堡壘機就不多見了。本文將簡要介紹下我開發的IronFort堡壘機,其詳細內容在http://study.163.com/course/courseMain.htm?courseId=1005453031&share=2&shareId=400000000495004視頻教程中。html

1、堡壘機概述

百度百科:堡壘機,在一個特定的網絡環境下,爲了保障網絡和數據不受來自外部和內部用戶的入侵和破壞,而運用各類技術手段實時收集和監控網絡環境中每個組成部分的系統狀態、安全事件、網絡活動,以便集中報警、及時處理及審計定責。前端

對於一箇中型以上的公司,當用戶和職員人數較多,公司所屬服務器也數量較大的狀況下,其服務器上的賬號管理難度將急劇增長,參考下面的圖片:python

1.png-153.5kB

這其中必然存在不少問題,例如:git

  • 用戶、主機、帳號數量太多,工做量大,管理混亂;
  • 每一個人員的權限和可以使用帳號沒有系統管理,等級區分不明;
  • 用戶直接掌握主機的賬號密碼;
  • 密碼可能交叉使用;
  • 離職人員可能還可使用公司的賬號;
  • 內部人員能夠跳過防火牆,直接使用賬號在機房內訪問;
  • 內部人員離職前設下木馬或暗門,一段時間後再爆發;
  • 對人員的訪問記錄、過往操做沒有日誌和審計,缺少過後追蹤手段;
  • 其它風險

在運行初期,公司可能採起Excel表格等工具,使用人工管理的方式,靠‘人治’和道德水平約束,但當公司體量逐漸變大的時候,這種方式必然遭到淘汰,因而就出現了堡壘機的概念,以下圖所示:github

2.png-148.7kB

這種架構帶來以下的好處:web

  • 用戶不能直接訪問遠程主機,而是須要經過堡壘機跳轉;
  • 用戶再也不掌握遠程主機的賬號密碼,只有訪問堡壘機的賬號;
  • 限制用戶登陸遠程主機後的修改密碼能力,不容許修改;
  • 堡壘機的用戶、遠程主機的用戶、用戶密碼、用戶權限等等都被統一集中管理,大量節省人工成本;
  • 用戶在登陸堡壘機後所進行的一切操做將被記錄下來,用於後期的行爲審計;
  • 因爲沒有遠程主機賬號密碼,即便進入機房也沒法直連主機;
  • 還能夠實現批量命令執行、文件分發等附帶功能;
  • 其它收益。

堡壘機的核心概念是用戶再也不掌握賬號密碼,用戶的行爲被記錄用於審計。堡壘機主要針對的是內部網絡和內部人員,對於人員流動性較強、體量大、行業風險高的企業需求特別強烈,好比金融行業。shell

堡壘機已經擁有商業產品,多數以硬件服務器爲載體進行銷售,價格幾十萬不等。也有開源的解決方案,但這些方案有的不是基於瀏覽器,界面不夠友好,日誌記錄困難;有的基於Tornado,而且只能進行簡單的命令執行功能,而公司使用的是Django;更多的狀況是與公司需求不一致,須要二次開發,維護和升級困難,等等不一而足。數據庫

‘授人以魚不如授人以漁’,本身掌握了開發堡壘機的核心技能,就能夠快速、方便、靈活的針對公司具體需求進行定製開發,既爲公司節省了購置硬件經費,又利於維護升級。django

2、 IronFort堡壘機體系架構

IronFort堡壘機的體系架構以下圖所示:json

3.png-78.4kB

一個完整的通訊過程以下:

  1. 用戶經過使用支持HTML5的瀏覽器,在HTTP的基礎上,向堡壘機發送websocket請求;
  2. 堡壘機上使用gevent接收websocket請求並轉發給Django;
  3. Django接收請求後,調用paramiko創建與遠程主機的ssh通道;
  4. 遠程主機執行用戶的命令後,經過ssh返回數據給Django;
  5. Django經過gevent以websocket的形式返回給用戶瀏覽器;
  6. 用戶瀏覽器使用term.js插件模擬Linux終端,顯示遠程主機返回的結果。

核心機制就是這樣,下面咱們來看下開發過程。

3、開發簡介

1. 項目建立

堡壘機自己一般是佈置在Linux主機上的,比ubuntu16.04,對外以HTTP的形式提供服務。

首先須要創建虛擬環境,並安裝Python3.6以及Django2.0,再也不贅述。

使用django-admin startprojectpython manage.py startapp app_name分別建立項目和app。

此時,能夠嘗試運行Django服務,若是看到下面的頁面,表示Ok。

QQ截圖20180130104720.jpg-35.9kB

Django2.0的歡迎界面比之前漂亮了一點,還帶連接,與時俱進呀。

2. ORM模型

任何一個Web項目都必須在深刻分析項目需求的狀況下,首先設計好ORM模型,也就是數據庫的表結構。

IronFort中設計了六個模型,分別是:

  • 遠程主機
  • 遠程主機用戶
  • 遠程主機綁定的用戶
  • 堡壘機用戶
  • 堡壘機用戶組
  • 日誌

這裏須要提醒的是:

  • 每一個遠程主機帳戶能夠綁定多個遠程主機,二者實際是多對多的關係;
  • 堡壘機用戶不能直接綁定遠程主機;
  • 堡壘機用戶綁定的實際是一個主機+主機帳戶的對象;
  • 考慮帳戶是否激活或者被經用的enabled屬性;
  • 考慮某些字段的unique_together屬性;

關於模型設計,每一個人有每一個人的需求和想法,這其中有不少坑和須要注意的地方,限於篇幅,沒法展開論述。在個人我的網站liujiangblog.com的視頻教程中有詳細的講解。

模型設計好了,能夠同時註冊Django的admin後臺。而後makemigrations、migrate和createsuperuser,重啓服務器後就能夠在admin中建立測試用例了,以下圖所示:

9-admin後臺.jpg-127.8kB

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。

adminlet.png-668.8kB

AdminLTE託管在GitHub上,能夠經過下面的地址下載:

https://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,最好是單獨一個簡單的頁面,展現的內容越少越好。

1-登陸界面.jpg-108.6kB

而用戶登陸的處理視圖就很簡單了,直接使用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插件,提供搜索、排序和分頁等高級功能,其展現效果以下圖:

2-主機賬號顯示頁面.jpg-255kB

7. 在瀏覽器中打開websocket通道

百度百科:WebSocket協議是基於TCP的一種新的網絡協議。它實現了瀏覽器與服務器全雙工(full-duplex)通訊——容許服務器主動發送信息給客戶端。

本文不打算成爲一篇websocket的科普文,有興趣深刻研究的能夠查看博客園的精華博文WebSocket協議:5分鐘從入門到精通

簡單的說,有如下幾點:

  • HTTP自己是無狀態鏈接,不支持實時通訊;
  • websocket基於HTML5,須要瀏覽器支持;
  • 經過在http報頭中添加upgrade屬性,申請通訊協議升級爲websocket;
  • 升級成爲websocket通訊後,能夠實現瀏覽器和遠程服務器之間的全雙工實時通訊。

關於websocket的使用教程,能夠參考阮一峯專家的博文WebSocket 教程

其具體API以下圖所示(圖片來自菜鳥教程):

image.png-74.8kB

image.png-92.7kB

要簡單的建立並使用一個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插件,地址爲:https://github.com/chjj/term.js。其官方文檔比較簡單,有興趣的同窗能夠深刻研讀其源代碼,或者使用xterm做爲替代。

最終效果以下:

無效鏈接.jpg-98.6kB

由於此時後端尚未完成,因此是鏈接不上任何主機的。

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通道之間的一發一收,一收一發的通訊機制。

這一步完成後,重啓服務器,咱們就能夠來展現整個通訊過程了。

首先是,鏈接成功:

4-基於websocket的堡壘機ssh鏈接.jpg-188.4kB

其次是相似Python這種交互式命令:

5-交互式環境命令.jpg-266.9kB

而後是top這種動態命令結果返回:

6-動態內容傳輸命令(好比top).jpg-386.9kB

最後是vim這種編輯環境:

7-vim等編輯環境.jpg-133.5kB

能夠看到,咱們是支持彩色輸出的:

8-支持彩色輸出.jpg-194kB

11. 日誌記錄和行爲審計

關於用戶操做,在數據由websocket往ssh發送過程當中,能夠保存用戶經過前端Linux模擬器終端所敲擊的全部按鍵記錄,而且很規整的以回車鍵進行分隔,很是容易判別。

咱們只須要建立一個日誌模型,編寫一個保存日誌的方法,而後在須要的位置保存日誌便可。

日誌展現頁面很是相似主機帳戶的頁面,一樣使用datatable插件進行處理,最終效果以下圖所示:

3-日誌系統頁面.jpg-270kB

至此,基於Webssh的堡壘機核心功能就開發完畢了。限於篇幅,不可能點點滴滴、枝葉不漏的所有敘述,我這裏也只是一個拋磚引玉的過程。

4、總結

遠程主機的建立、主機帳號的管理、堡壘機用戶和用戶組的管理,這一系列的工做,目前我仍是放在admin後臺中進行。後期,你們能夠將它遷移到堡壘機頁面中一塊兒管理。若是將IronFort用於生產環境,添加批量命令執行、文件分發功能,進行系統部署上線、結合Linux運維等等,必然須要大量的額外工做和安全機制,這些就留給你們本身去研究了。

另外,我在官方主頁劉江的博客和教程中,還有一個關於CMDB主機管理系統的教程,將它結合到堡壘機項目中來,一個運維管理平臺的核心底層功能就基本具有了,在此基礎上進行擴展,大有可爲!

最後,我的技術和能力有限,必然存在不足之處,請你們輕噴,多提寶貴意見,謝謝!

相關文章
相關標籤/搜索