Add IP blacklisting to the sysadm server.

Current Settings:
1) 5 auth attempts allowed before failover
2) If no communications for 10 minutes, the failover counter gets reset
3) On failover - the IP is placed on the server blacklist for 1 hour
Note: The blacklist system is connection independant, and uses the host IP for unique tracking/blocking.
This commit is contained in:
Ken Moore
2016-01-21 10:12:13 -05:00
parent 96fa70a1fe
commit 1ae716ebfc
7 changed files with 124 additions and 28 deletions

View File

@@ -4,9 +4,6 @@
// Written by: Ken Moore <ken@pcbsd.org> July 2015
// =================================
#include "AuthorizationManager.h"
#include <QDebug>
#include <QProcess>
#include <QCoreApplication>
// Stuff for PAM to work
#include <sys/types.h>
@@ -19,15 +16,21 @@
#include <login_cap.h>
//Internal defines
// -- token management
#define TIMEOUTSECS 900 // (15 minutes) time before a token becomes invalid
#define AUTHCHARS QString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
#define TOKENLENGTH 20
// -- Connection failure limitations
#define AUTHFAILLIMIT 5 //number of sequential failures before IP is blocked for a time
#define FAILOVERMINS 10 //after this many minutes without a new login attempt the failure count will reset
AuthorizationManager::AuthorizationManager(){
AuthorizationManager::AuthorizationManager() : QObject(){
HASH.clear();
IPFAIL.clear();
//initialize the random number generator (need to generate auth tokens)
qsrand(QDateTime::currentMSecsSinceEpoch());
}
AuthorizationManager::~AuthorizationManager(){
}
@@ -71,8 +74,9 @@ int AuthorizationManager::checkAuthTimeoutSecs(QString token){
// == Token Generation functions
QString AuthorizationManager::LoginUP(bool localhost, QString user, QString pass){
QString AuthorizationManager::LoginUP(QHostAddress host, QString user, QString pass){
//Login w/ username & password
bool localhost = ( (host== QHostAddress::LocalHost) || (host== QHostAddress::LocalHostIPv6) );
bool ok = false;
//First check that the user is valid on the system and part of the operator group
bool isOperator = false;
@@ -92,19 +96,33 @@ QString AuthorizationManager::LoginUP(bool localhost, QString user, QString pass
}
qDebug() << "User Login Attempt:" << user << " Success:" << ok << " Local Login:" << localhost;
if(!ok){ return ""; } //invalid login
else{ return generateNewToken(isOperator); } //valid login - generate a new token for it
if(!ok){
//invalid login
//Bump the fail count for this host
bool overlimit = BumpFailCount(host.toString());
if(overlimit){ emit BlockHost(host); }
return (overlimit ? "REFUSED" : "");
}else{
//valid login - generate a new token for it
ClearHostFail(host.toString());
return generateNewToken(isOperator);
}
}
QString AuthorizationManager::LoginService(bool localhost, QString service){
QString AuthorizationManager::LoginService(QHostAddress host, QString service){
bool localhost = ( (host== QHostAddress::LocalHost) || (host== QHostAddress::LocalHostIPv6) );
//Login a particular automated service
qDebug() << "Service Login Attempt:" << service << " Success:" << localhost;
if(!localhost){ return ""; } //invalid - services must be local for access
//Check that the service is valid on the system
// -- TO-DO
bool isok = false;
if(service!="root" && service!="toor"){
QStringList groups = getUserGroups(service);
isok = (groups.contains(service) && !groups.contains("wheel") && !groups.contains("operator"));
}
//Now generate a new token and send it back
return generateNewToken(false); //services are never given operator privileges
if(!isok){ return ""; }
else{ return generateNewToken(false); }//services are never given operator privileges
}
// =========================
@@ -145,6 +163,28 @@ QStringList AuthorizationManager::getUserGroups(QString user){
return out;
}
bool AuthorizationManager::BumpFailCount(QString host){
//Returns: true if the failure count is over the limit
//key: "<IP>::::<failnum>"
QStringList keys = QStringList(IPFAIL.keys()).filter(host+"::::");
int fails = 0;
if(!keys.isEmpty()){
//Take the existing key/value and put a new one in (this limits the filter to 1 value maximum)
QDateTime last = IPFAIL.take(keys[0]);
if(last.addSecs(FAILOVERMINS*60) > QDateTime::currentDateTime() ){
fails = keys[0].section("::::",1,1).toInt();
}
}
fails++;
IPFAIL.insert(host+"::::"+QString::number(fails), QDateTime::currentDateTime() );
return (fails>=AUTHFAILLIMIT);
}
void AuthorizationManager::ClearHostFail(QString host){
QStringList keys = QStringList(IPFAIL.keys()).filter(host+"::::");
for(int i=0; i<keys.length(); i++){ IPFAIL.remove(keys[i]); }
}
/*
========== PAM FUNCTIONS ==========
*/

View File

@@ -6,12 +6,10 @@
#ifndef _PCBSD_REST_AUTHORIZATION_MANAGER_H
#define _PCBSD_REST_AUTHORIZATION_MANAGER_H
#include <QHash>
#include <QString>
#include <QStringList>
#include <QDateTime>
#include "globals-qt.h"
class AuthorizationManager{
class AuthorizationManager : public QObject{
Q_OBJECT
public:
AuthorizationManager();
~AuthorizationManager();
@@ -24,14 +22,20 @@ public:
int checkAuthTimeoutSecs(QString token); //Return the number of seconds that a token is valid for
// == Token Generation functions
QString LoginUP(bool localhost, QString user, QString pass); //Login w/ username & password
QString LoginService(bool localhost, QString service); //Login a particular automated service
QString LoginUP(QHostAddress host, QString user, QString pass); //Login w/ username & password
QString LoginService(QHostAddress host, QString service); //Login a particular automated service
private:
QHash<QString, QDateTime> HASH;
QHash <QString, QDateTime> IPFAIL;
QString generateNewToken(bool isOperator);
QStringList getUserGroups(QString user);
//Failure count management
bool BumpFailCount(QString host);
void ClearHostFail(QString host);
//token->hashID filter simplification
QString hashID(QString token){
QStringList tmp = QStringList(HASH.keys()).filter(token+"::::");
@@ -42,6 +46,10 @@ private:
//PAM login/check files
bool pam_checkPW(QString user, QString pass);
void pam_logFailure(int ret);
signals:
void BlockHost(QHostAddress); //block a host address temporarily
};
#endif

View File

@@ -15,6 +15,7 @@ WebServer::WebServer(){
WSServer = 0;
TCPServer = 0;
AUTH = new AuthorizationManager();
connect(AUTH, SIGNAL(BlockHost(QHostAddress)), this, SLOT(BlackListConnection(QHostAddress)) );
}
WebServer::~WebServer(){
@@ -97,6 +98,24 @@ bool WebServer::setupTcp(quint16 port){
return TCPServer->listen(QHostAddress::Any, port);
}
//Server Blacklist / DDOS mitigator
bool WebServer::allowConnection(QHostAddress addr){
//Check if this addr is on the blacklist
QString key = "blacklist/"+addr.toString();
if(!CONFIG->contains(key) ){ return true; } //not in the list
//Address on the list - see if the timeout has expired
QDateTime dt = CONFIG->value(key,QDateTime()).toDateTime();
int minblock = CONFIG->value("blacklist/RefuseMinutes",60).toInt();
if(dt.addSecs(minblock*60) < QDateTime::currentDateTime()){
//This entry has timed out - go ahead and allow it
CONFIG->remove(key); //make the next connection check for this IP faster again
return true;
}else{
return false; //blacklist block is still in effect
}
}
QString WebServer::generateID(){
int id = 0;
for(int i=0; i<OpenSockets.length(); i++){
@@ -113,14 +132,23 @@ QString WebServer::generateID(){
void WebServer::NewSocketConnection(){
WebSocket *sock = 0;
if(WSServer!=0){
if(WSServer->hasPendingConnections()){ sock = new WebSocket( WSServer->nextPendingConnection(), generateID(), AUTH); }
if(WSServer->hasPendingConnections()){
QWebSocket *ws = WSServer->nextPendingConnection();
if( !allowConnection(ws->peerAddress()) ){ ws->close(); }
else{ sock = new WebSocket( ws, generateID(), AUTH); }
}
}else if(TCPServer!=0){
if(TCPServer->hasPendingConnections()){ sock = new WebSocket( TCPServer->nextPendingConnection(), generateID(), AUTH); }
if(TCPServer->hasPendingConnections()){
QSslSocket *ss = TCPServer->nextPendingConnection();
if( !allowConnection(ss->peerAddress()) ){ ss->close(); }
else{ sock = new WebSocket( ss, generateID(), AUTH); }
}
}
if(sock==0){ return; } //no new connection
qDebug() << "New Socket Connection";
connect(sock, SIGNAL(SocketClosed(QString)), this, SLOT(SocketClosed(QString)) );
connect(EVENTS, SIGNAL(NewEvent(EventWatcher::EVENT_TYPE, QJsonValue)), sock, SLOT(EventUpdate(EventWatcher::EVENT_TYPE, QJsonValue)) );
connect(sock, SIGNAL(BlackListAddress(QHostAddress)), this, SLOT(BlackListConnection(QHostAddress)) );
OpenSockets << sock;
}
@@ -129,6 +157,15 @@ void WebServer::NewConnectError(QAbstractSocket::SocketError err){
QTimer::singleShot(0,this, SLOT(NewSocketConnection()) ); //check for a new connection
}
//Socket Blacklist function
void WebServer::BlackListConnection(QHostAddress addr){
//Make sure this is not the localhost (never block that)
if(addr!= QHostAddress(QHostAddress::LocalHost) && addr != QHostAddress(QHostAddress::LocalHostIPv6) ){
//Block this remote host
CONFIG->setValue("blacklist/"+addr.toString(), QDateTime::currentDateTime());
}
}
//WEBSOCKET SERVER SIGNALS
// Overall Server signals
void WebServer::ServerClosed(){

View File

@@ -11,6 +11,7 @@
#include "WebSocket.h"
#include "AuthorizationManager.h"
#include "SslServer.h"
class WebServer : public QObject{
Q_OBJECT
public:
@@ -31,6 +32,9 @@ private:
//Server Setup functions
bool setupWebSocket(quint16 port);
bool setupTcp(quint16 port);
//Server Blacklist / DDOS mitigator
bool allowConnection(QHostAddress addr);
//Generic functions for either type of server
QString generateID(); //generate a new ID for a socket
@@ -39,6 +43,8 @@ private slots:
// Generic Server Slots
void NewSocketConnection(); //newConnection() signal
void NewConnectError(QAbstractSocket::SocketError); //acceptError() signal
//Socket Blacklist function
void BlackListConnection(QHostAddress addr);
// (WebSocket-only) Server signals/slots
void ServerClosed(); //closed() signal

View File

@@ -123,6 +123,9 @@ void WebSocket::EvaluateREST(QString msg){
void WebSocket::EvaluateRequest(const RestInputStruct &REQ){
RestOutputStruct out;
out.in_struct = REQ;
QHostAddress host;
if(SOCKET!=0){ host = SOCKET->peerAddress(); }
else if(TSOCKET!=0){ host = TSOCKET->peerAddress(); }
if(!REQ.VERB.isEmpty() && REQ.VERB != "GET" && REQ.VERB!="POST" && REQ.VERB!="PUT"){
//Non-supported request (at the moment) - return an error message
out.CODE = RestOutputStruct::BADREQUEST;
@@ -134,7 +137,7 @@ void WebSocket::EvaluateRequest(const RestInputStruct &REQ){
//First check for a REST authorization (not stand-alone request)
if(!out.in_struct.auth.isEmpty()){
AUTHSYSTEM->clearAuth(SockAuthToken); //new auth requested - clear any old token
SockAuthToken = AUTHSYSTEM->LoginUP(false, out.in_struct.auth.section(":",0,0), out.in_struct.auth.section(":",1,1));
SockAuthToken = AUTHSYSTEM->LoginUP(host, out.in_struct.auth.section(":",0,0), out.in_struct.auth.section(":",1,1));
}
//Now check the body of the message and do what it needs
@@ -144,16 +147,13 @@ void WebSocket::EvaluateRequest(const RestInputStruct &REQ){
//Note: This sets/changes the current SockAuthToken
AUTHSYSTEM->clearAuth(SockAuthToken); //new auth requested - clear any old token
if(DEBUG){ qDebug() << "Authenticate Peer:" << SOCKET->peerAddress().toString(); }
bool localhost = false;
if(SOCKET!=0){ localhost = (SOCKET->peerAddress() == QHostAddress::LocalHost) || (SOCKET->peerAddress() == QHostAddress::LocalHostIPv6); }
else if(TSOCKET!=0){ localhost = (TSOCKET->peerAddress() == QHostAddress::LocalHost) || (TSOCKET->peerAddress() == QHostAddress::LocalHostIPv6); }
//Now do the auth
if(out.in_struct.name=="auth" && out.in_struct.args.isObject() ){
//username/password authentication
QString user, pass;
if(out.in_struct.args.toObject().contains("username")){ user = JsonValueToString(out.in_struct.args.toObject().value("username")); }
if(out.in_struct.args.toObject().contains("password")){ pass = JsonValueToString(out.in_struct.args.toObject().value("password")); }
SockAuthToken = AUTHSYSTEM->LoginUP(localhost, user, pass);
SockAuthToken = AUTHSYSTEM->LoginUP(host, user, pass);
}else if(out.in_struct.name == "auth_token" && out.in_struct.args.isObject()){
SockAuthToken = JsonValueToString(out.in_struct.args.toObject().value("token"));
}else if(out.in_struct.name == "auth_clear"){
@@ -169,9 +169,12 @@ void WebSocket::EvaluateRequest(const RestInputStruct &REQ){
out.out_args = array;
out.CODE = RestOutputStruct::OK;
}else{
if(SockAuthToken=="REFUSED"){
out.CODE = RestOutputStruct::FORBIDDEN;
}
SockAuthToken.clear(); //invalid token
//Bad Authentication - return error
out.CODE = RestOutputStruct::UNAUTHORIZED;
out.CODE = RestOutputStruct::UNAUTHORIZED;
}
}else if( AUTHSYSTEM->checkAuth(SockAuthToken) ){ //validate current Authentication token
@@ -242,6 +245,9 @@ void WebSocket::EvaluateRequest(const RestInputStruct &REQ){
}
//Return any information
this->sendReply(out.assembleMessage());
if(out.CODE == RestOutputStruct::FORBIDDEN && SOCKET!=0){
SOCKET->close(QWebSocketProtocol::CloseCodeNormal, "Too Many Authorization Failures - Try again later");
}
}
// === GENERAL PURPOSE UTILITY FUNCTIONS ===

View File

@@ -74,7 +74,6 @@ public slots:
signals:
void SocketClosed(QString); //ID
};
#endif

View File

@@ -21,7 +21,7 @@ bool DispatcherClient::setupProcAuth(){
QString key = ReadKey();
if(!AUTH->checkAuth(key) ){
//Key now invalid - generate a new one (this ensures that the secure key rotates on a regular basis)
key = AUTH->LoginService(true, "dispatcher");
key = AUTH->LoginService(QHostAddress::LocalHost, "dispatcher");
//Save the auth key to the file and lock it down
if(!WriteKey(key)){
qWarning() << "Could not save dispatcher authorization key: **No dispatcher availability**. ";