mirror of
https://github.com/outbackdingo/sysadm.git
synced 2026-01-27 10:20:26 +00:00
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:
@@ -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 ==========
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
@@ -74,7 +74,6 @@ public slots:
|
||||
|
||||
signals:
|
||||
void SocketClosed(QString); //ID
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -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**. ";
|
||||
|
||||
Reference in New Issue
Block a user