ASP.NET WebAPIで Port Knockingを作ってみた。


Port Knocking?

システム管理すると、遠隔地のサーバーに管理者としてアクセスする必要があるが往々にある。sshやhttpsで暗号化された通路を使用するが、外部に常時さらされている管理ポートは、brute-force攻撃を受け常だ。

特にsshの場合は、ネット上にtcp/22万うろついボットが存在程度のリスクだが、fail2banやsshguardのような一定回数以上失敗した場合、自動的にシステムのファイアウォール(iptables、pf、firewalld...)を利用して、一定時間ブロックする手法が必要である。

もし常時サービスが必要としていない、すなわち、管理目的のサービスに見合った手法が「ポート - ノッキング( Port Knoking)」である。クライアントは、デーモンの事前約束した文字列を渡して、予め約束されたポートを少しの間だけ開いておく式である。こうするとポートが常に開放される必要はない。

Basic API

提供するコマンドは以下の通り。

request:
POST -d '{action:"allow", ip:"127.0.0.1", port:8080}' /api/portkock
POST -d '{action:"deny", ip:"127.0.0.1", port:8080}' /api/portkock

response:
when OKay, {"result":"OK"}
when NG, {"result":"NG", "message":"bla bla ..."}

具現

referencesに "C:\Windows\System32\FirewallAPI.dll" を追加する必要がある。以下のようなシンプルなControllerを作ってみた。


using PortKnockService;
using System;
using System.Runtime.Serialization;
using System.Web.Http;

namespace PortKockWebApi.Controllers
{
    public class PortKnockController : ApiController
    {

        [HttpGet]
        public PortKnockResponse DoGet(PortKnockRequest req)
        {
            return new PortKnockResponse()
            {
                Result = "NG",
                Message = "unknown method"
            };
        }


        [HttpPost]
        public PortKnockResponse DoPost(PortKnockRequest req)
        {

            // need to log here.

            if (req.Action.ToLower() == "allow") {
                try
                {
                    FirewallUtils.AllowAddressPort(req.Ip, req.Port);
                    return new PortKnockResponse()
                    {
                        Result = "OK",
                        Message = null
                    };

                }
                catch (Exception e)
                {
                    return new PortKnockResponse()
                    {
                        Result = "NG",
                        Message = e.Message
                    };
                }
            }
            else if (req.Action.ToLower() == "deny")
            {
                try
                {
                    FirewallUtils.CloseAddressPort(req.Ip, req.Port);
                    return new PortKnockResponse()
                    {
                        Result = "OK",
                        Message = null
                    };

                }
                catch (Exception e)
                {
                    return new PortKnockResponse()
                    {
                        Result = "NG",
                        Message = e.Message
                    };
                }
            }

            return new PortKnockResponse()
            {
                Result = "NG",
                Message = "unknown error"
            };
        }
    }

    [Serializable]
    [DataContract(Name = "")]
    public class PortKnockRequest
    {
        [DataMember(Name = "action", IsRequired = true)]
        public string Action { get; set; }

        [DataMember(Name = "ip", IsRequired = true)]
        public string Ip { get; set; }

        [DataMember(Name = "port", IsRequired = true)]
        public int Port { get; set; }
    }

    [Serializable]
    [DataContract(Name = "")]
    public class PortKnockResponse
    {
        [DataMember(Name = "result")]
        public string Result { get; set; }

        [DataMember(Name = "message", IsRequired =false)]
        public string Message { get; set; }

    }
}

具現したもの

動作確認

実行する時にはFirewallを操作するため、管理者権限が必要だ。

curlで実行して

Powershellで結果を確認した。

改善点

  • proper listing feature current opened destinations

    list opened destinations
    request:
        GET /api/portkock[?items=50&page=1]
    response:
        {items:[
            {id="", requested_at="", expired_at="", destination={action:"allow", ip:"127.0.0.1", port:8080}, result:{}},
            ...
        ]}
    
  • proper getting request specific item details feature

    request:
        GET /api/portkock/<id>
    response:
        ...
    
  • proper logging feature
  • proper persistent to records
  • proper scheduling to automated expiration
  • not to show IIS error page