Move <oper:autologin> from m_sslinfo to core_oper and rework.

- Promote autologin to a core concept with visibility in events.

- Replace the binary yes/no value with strict/relaxed/never. This
  intentionally breaks v3 oper block autologin as admins will need
  to review them for the security implications of the new behaviour.
This commit is contained in:
Sadie Powell 2022-12-11 09:58:27 +00:00
parent cd6329d4b0
commit 3c6e24665f
16 changed files with 126 additions and 54 deletions

View File

@ -142,10 +142,18 @@
# If the sslinfo module isn't loaded, this option will be ignored.
#fingerprint="67cb9dc013248a829bb2171ed11becd4"
# autologin: If a TLS client certificate fingerprint for this oper is specified,
# you can have the oper block automatically log in if they match the fingerprint
# and host fields. Requires the sslinfo module.
#autologin="yes"
# autologin: Whether to automatically log this server operator in on connect if all
# of their details match the ones in this <oper> block. Can be set to "strict" to
# automatically log in if the user's nickname matches the oper account name and the
# account/host/sslonly/etc fields match, "relaxed" to automatically log in if the
# account/host/sslonly/etc fields match, and "never" to not allow automatically
# logging in to this oper account. Defaults to "never".
#
# IMPORTANT: As this option overrides the password field it should **NOT** be used
# unless you are certain that nobody other than the intended user will match the
# restrictions of this <oper> block. Failure to do this may result in your server
# being compromised.
#autologin="strict"
# sslonly: If enabled, this oper can only oper up if they're using a TLS connection.
# Setting this option adds a decent bit of security. Highly recommended

View File

@ -925,21 +925,24 @@ public:
/** Called when a local user is attempting to log in to an server operator account.
* @param user The user who is attempting to log in.
* @param oper The server operator account they are attempting to log in to.
* @param automatic Whether the login attempt is being performed automatically.
* @return MOD_RES_ALLOW to explicitly allow the login, MOD_RES_DENY to explicitly deny the
* login, or MOD_RES_PASSTHRU to let another module handle the event.
*/
virtual ModResult OnPreOperLogin(LocalUser* user, const std::shared_ptr<OperAccount>& oper);
virtual ModResult OnPreOperLogin(LocalUser* user, const std::shared_ptr<OperAccount>& oper, bool automatic);
/** Called when a user is about to be logged in to an server operator account.
* @param user The user who is about to be logged in.
* @param oper The server operator account they are logging in to.
* @param automatic Whether the login was performed automatically.
*/
virtual void OnOperLogin(User* user, const std::shared_ptr<OperAccount>& oper);
virtual void OnOperLogin(User* user, const std::shared_ptr<OperAccount>& oper, bool automatic);
/** Called after a user has been logged in to an server operator account.
* @param user The user who has been logged in.
* @param automatic Whether the login was performed automatically.
*/
virtual void OnPostOperLogin(User* user);
virtual void OnPostOperLogin(User* user, bool automatic);
/** Called when a user is about to be logged out of an server operator account.
* @param user The user who is about to be logged out.

View File

@ -236,7 +236,24 @@ public:
class CoreExport OperAccount
: public OperType
{
private:
protected:
/** Possible states for whether an oper account can be automatically logged into. */
enum class AutoLogin
: uint8_t
{
/** Users can automatically log in to this account if they match all fields and their nick matches the account name. */
STRICT,
/** Users can automatically log in to this account if they match all fields. */
RELAXED,
/** Users can not automatically log in to this account. */
NEVER,
};
/** Whether this oper account can be automatically logged into. */
AutoLogin autologin;
/** The password to used to log into this oper account. */
std::string password;
@ -254,6 +271,9 @@ public:
*/
OperAccount(const std::string& n, const std::shared_ptr<OperType>& o, const std::shared_ptr<ConfigTag>& t);
/** Check whether this user can attempt to automatically log in to this account. */
bool CanAutoLogin(LocalUser* user) const;
/** Check the specified password against the one from this oper account's password.
* @param pw The password to check.
*/
@ -581,10 +601,11 @@ public:
/** Logs this user into the specified server operator account.
* @param account The account to log this user in to.
* @param automatic Whether this is an automatic login attempt.
* @param force Whether to ignore any checks from OnPreOperLogin.
* @return True if the user is logged into successfully; otherwise, false.
*/
bool OperLogin(const std::shared_ptr<OperAccount>& account, bool force = false);
bool OperLogin(const std::shared_ptr<OperAccount>& account, bool automatic = false, bool force = false);
/** Logs this user out of their server operator account. Does nothing to non-operators. */
void OperLogout();

View File

@ -63,18 +63,38 @@ public:
cmdkill.hideservicekills = security->getBool("hideservicekills", security->getBool("hideulinekills"));
}
ModResult OnPreOperLogin(LocalUser* user, const std::shared_ptr<OperAccount>& oper) override
void OnPostConnect(User* user) override
{
LocalUser* luser = IS_LOCAL(user);
if (!luser)
return;
// Find an auto-oper block for this user.
for (const auto& [_, account] : ServerInstance->Config->OperAccounts)
{
if (!account->CanAutoLogin(luser))
continue; // No autologin for this account.
if (user->OperLogin(account, true))
break; // Successfully logged in to the account.
}
}
ModResult OnPreOperLogin(LocalUser* user, const std::shared_ptr<OperAccount>& oper, bool automatic) override
{
const std::string hosts = oper->GetConfig()->getString("host");
if (InspIRCd::MatchMask(hosts, user->MakeHost(), user->MakeHostIP()))
return MOD_RES_PASSTHRU; // Host matches.
ServerInstance->SNO.WriteGlobalSno('o', "%s (%s) [%s] failed to log into the \x02%s\x02 oper account because they are connecting from the wrong user@host.",
user->nick.c_str(), user->MakeHost().c_str(), user->GetIPString().c_str(), oper->GetName().c_str());
if (!automatic)
{
ServerInstance->SNO.WriteGlobalSno('o', "%s (%s) [%s] failed to log into the \x02%s\x02 oper account because they are connecting from the wrong user@host.",
user->nick.c_str(), user->MakeHost().c_str(), user->GetIPString().c_str(), oper->GetName().c_str());
}
return MOD_RES_DENY; // Host does not match.
}
void OnPostOperLogin(User* user) override
void OnPostOperLogin(User* user, bool automatic) override
{
LocalUser* luser = IS_LOCAL(user);
if (!luser)
@ -84,9 +104,9 @@ public:
strchr("AEIOUaeiou", user->oper->GetType()[0]) ? "an" : "a",
user->oper->GetType().c_str()));
ServerInstance->SNO.WriteToSnoMask('o', "%s (%s) is now a server operator of type %s (using account %s)",
user->nick.c_str(), user->MakeHost().c_str(), user->oper->GetType().c_str(),
user->oper->GetName().c_str());
ServerInstance->SNO.WriteToSnoMask('o', "%s (%s) [%s] is now a server operator of type \x02%s\x02 (%susing account \x02%s\x02).",
user->nick.c_str(), user->MakeHost().c_str(), user->GetIPString().c_str(), user->oper->GetType().c_str(),
automatic ? "automatically " : "", user->oper->GetName().c_str());
const std::string vhost = luser->oper->GetConfig()->getString("vhost");
if (!vhost.empty())

View File

@ -162,9 +162,9 @@ void Module::OnServiceAdd(ServiceProvider&) { DetachEvent(I_OnServiceAdd); }
void Module::OnServiceDel(ServiceProvider&) { DetachEvent(I_OnServiceDel); }
ModResult Module::OnUserWrite(LocalUser*, ClientProtocol::Message&) { DetachEvent(I_OnUserWrite); return MOD_RES_PASSTHRU; }
void Module::OnShutdown(const std::string& reason) { DetachEvent(I_OnShutdown); }
ModResult Module::OnPreOperLogin(LocalUser*, const std::shared_ptr<OperAccount>&) { DetachEvent(I_OnPreOperLogin); return MOD_RES_PASSTHRU; }
void Module::OnOperLogin(User*, const std::shared_ptr<OperAccount>&) { DetachEvent(I_OnOperLogin); }
void Module::OnPostOperLogin(User*) { DetachEvent(I_OnPostOperLogin); }
ModResult Module::OnPreOperLogin(LocalUser*, const std::shared_ptr<OperAccount>&, bool) { DetachEvent(I_OnPreOperLogin); return MOD_RES_PASSTHRU; }
void Module::OnOperLogin(User*, const std::shared_ptr<OperAccount>&, bool) { DetachEvent(I_OnOperLogin); }
void Module::OnPostOperLogin(User*, bool) { DetachEvent(I_OnPostOperLogin); }
void Module::OnOperLogout(User*) { DetachEvent(I_OnOperLogout); }
void Module::OnPostOperLogout(User*, const std::shared_ptr<OperAccount>&) { DetachEvent(I_OnPostOperLogout); }

View File

@ -338,7 +338,7 @@ public:
return MOD_RES_PASSTHRU;
}
ModResult OnPreOperLogin(LocalUser* user, const std::shared_ptr<OperAccount>& oper) override
ModResult OnPreOperLogin(LocalUser* user, const std::shared_ptr<OperAccount>& oper, bool automatic) override
{
const std::string accountstr = oper->GetConfig()->getString("account");
if (accountstr.empty())
@ -357,8 +357,11 @@ public:
return MOD_RES_PASSTHRU; // Matches on account name.
}
ServerInstance->SNO.WriteGlobalSno('o', "%s (%s) [%s] failed to log into the \x02%s\x02 oper account because they are not logged into the correct user account.",
user->nick.c_str(), user->MakeHost().c_str(), user->GetIPString().c_str(), oper->GetName().c_str());
if (!automatic)
{
ServerInstance->SNO.WriteGlobalSno('o', "%s (%s) [%s] failed to log into the \x02%s\x02 oper account because they are not logged into the correct user account.",
user->nick.c_str(), user->MakeHost().c_str(), user->GetIPString().c_str(), oper->GetName().c_str());
}
return MOD_RES_DENY; // Account required but it does not match.
}

View File

@ -52,7 +52,7 @@ public:
operChans.push_back(channame);
}
void OnPostOperLogin(User* user) override
void OnPostOperLogin(User* user, bool automatic) override
{
LocalUser* localuser = IS_LOCAL(user);
if (!localuser)

View File

@ -35,7 +35,7 @@ public:
{
}
void OnPostOperLogin(User* user) override
void OnPostOperLogin(User* user, bool automatic) override
{
if (!IS_LOCAL(user))
return; // We don't handle remote users.

View File

@ -136,7 +136,7 @@ public:
{
}
void OnPostOperLogin(User* user) override
void OnPostOperLogin(User* user, bool automatic) override
{
if (IS_LOCAL(user) && user->oper->GetConfig()->getBool("automotd", onoper))
cmd.ShowOperMOTD(user, false);

View File

@ -104,7 +104,7 @@ public:
ServerInstance->Modes.Process(ServerInstance->FakeClient, memb->chan, nullptr, changelist);
}
void OnPostOperLogin(User* user) override
void OnPostOperLogin(User* user, bool automatic) override
{
if (IS_LOCAL(user) && (!user->IsModeSet(hideopermode)))
SetOperPrefix(user, true);

View File

@ -772,7 +772,7 @@ restart:
}
}
void ModuleSpanningTree::OnOperLogin(User* user, const std::shared_ptr<OperAccount>& oper)
void ModuleSpanningTree::OnOperLogin(User* user, const std::shared_ptr<OperAccount>& oper, bool automatic)
{
if (!user->IsFullyConnected() || !IS_LOCAL(user))
return;

View File

@ -196,7 +196,7 @@ public:
void OnUserKick(User* source, Membership* memb, const std::string& reason, CUList& excepts) override;
void OnPreRehash(User* user, const std::string& parameter) override;
void ReadConfig(ConfigStatus& status) override;
void OnOperLogin(User* user, const std::shared_ptr<OperAccount>& oper) override;
void OnOperLogin(User* user, const std::shared_ptr<OperAccount>& oper, bool automatic) override;
void OnAddLine(User* u, XLine* x) override;
void OnDelLine(User* u, XLine* x) override;
ModResult OnStats(Stats::Context& stats) override;

View File

@ -49,8 +49,9 @@ CmdResult CommandOpertype::HandleRemote(RemoteUser* u, CommandBase::Params& para
return CmdResult::SUCCESS;
}
ServerInstance->SNO.WriteToSnoMask('O', "From %s: %s (%s) is now a server operator of type %s",
u->server->GetName().c_str(), u->nick.c_str(), u->MakeHost().c_str(), u->oper->GetType().c_str());
ServerInstance->SNO.WriteToSnoMask('O', "From %s: %s (%s) [%s] is now a server operator of type \x02%s\x02.",
u->server->GetName().c_str(), u->nick.c_str(), u->MakeHost().c_str(), u->GetIPString().c_str(),
u->oper->GetType().c_str());
return CmdResult::SUCCESS;
}

View File

@ -323,21 +323,27 @@ public:
return MOD_RES_PASSTHRU;
}
ModResult OnPreOperLogin(LocalUser* user, const std::shared_ptr<OperAccount>& oper) override
ModResult OnPreOperLogin(LocalUser* user, const std::shared_ptr<OperAccount>& oper, bool automatic) override
{
auto cert = cmd.sslapi.GetCertificate(user);
if (oper->GetConfig()->getBool("sslonly") && !cert)
{
ServerInstance->SNO.WriteGlobalSno('o', "%s (%s) [%s] failed to log into the \x02%s\x02 oper account because they are not connected using TLS.",
user->nick.c_str(), user->MakeHost().c_str(), user->GetIPString().c_str(), oper->GetName().c_str());
if (!automatic)
{
ServerInstance->SNO.WriteGlobalSno('o', "%s (%s) [%s] failed to log into the \x02%s\x02 oper account because they are not connected using TLS.",
user->nick.c_str(), user->MakeHost().c_str(), user->GetIPString().c_str(), oper->GetName().c_str());
}
return MOD_RES_DENY;
}
const std::string fingerprint = oper->GetConfig()->getString("fingerprint");
if (!fingerprint.empty() && (!cert || !MatchFP(cert, fingerprint)))
{
ServerInstance->SNO.WriteGlobalSno('o', "%s (%s) [%s] failed to log into the \x02%s\x02 oper account because they are not using the correct TLS client certificate.",
user->nick.c_str(), user->MakeHost().c_str(), user->GetIPString().c_str(), oper->GetName().c_str());
if (!automatic)
{
ServerInstance->SNO.WriteGlobalSno('o', "%s (%s) [%s] failed to log into the \x02%s\x02 oper account because they are not using the correct TLS client certificate.",
user->nick.c_str(), user->MakeHost().c_str(), user->GetIPString().c_str(), oper->GetName().c_str());
}
return MOD_RES_DENY;
}
@ -365,20 +371,6 @@ public:
if (cert && !cert->GetFingerprint().empty())
text.append(" and your TLS client certificate fingerprint is ").append(cert->GetFingerprint());
user->WriteNotice(text);
if (!cert)
return;
// Find an auto-oper block for this user
for (const auto& [_, info] : ServerInstance->Config->OperAccounts)
{
const auto& oper = info->GetConfig();
if (!oper->getBool("autologin"))
continue; // No autologin for this block.
if (!user->OperLogin(info))
continue; // Some other field does not match.
}
}
ModResult OnSetConnectClass(LocalUser* user, const ConnectClass::Ptr& myclass) override

View File

@ -116,7 +116,7 @@ public:
return MOD_RES_PASSTHRU;
}
void OnPostOperLogin(User* user) override
void OnPostOperLogin(User* user, bool automatic) override
{
if (!IS_LOCAL(user))
return;

View File

@ -349,13 +349,13 @@ Cullable::Result FakeUser::Cull()
return User::Cull();
}
bool User::OperLogin(const std::shared_ptr<OperAccount>& account, bool force)
bool User::OperLogin(const std::shared_ptr<OperAccount>& account, bool automatic, bool force)
{
LocalUser* luser = IS_LOCAL(this);
if (luser && !quitting && !force)
{
ModResult modres;
FIRST_MOD_RESULT(OnPreOperLogin, modres, (luser, account));
FIRST_MOD_RESULT(OnPreOperLogin, modres, (luser, account, automatic));
if (modres == MOD_RES_DENY)
return false; // Module rejected the oper attempt.
}
@ -364,7 +364,7 @@ bool User::OperLogin(const std::shared_ptr<OperAccount>& account, bool force)
if (IsOper())
OperLogout();
FOREACH_MOD(OnOperLogin, (this, account));
FOREACH_MOD(OnOperLogin, (this, account, automatic));
// When a user logs in we need to:
// 1. Set the operator account (this is what IsOper checks).
@ -385,7 +385,7 @@ bool User::OperLogin(const std::shared_ptr<OperAccount>& account, bool force)
}
ServerInstance->Users.all_opers.push_back(this);
FOREACH_MOD(OnPostOperLogin, (this));
FOREACH_MOD(OnPostOperLogin, (this, automatic));
return true;
}
@ -1379,6 +1379,12 @@ OperAccount::OperAccount(const std::string& n, const std::shared_ptr<OperType>&
, passwordhash(t->getString("hash", "plaintext", 1))
, type(o ? o->GetName() : n)
{
autologin = t->getEnum("autologin", AutoLogin::NEVER, {
{ "strict", AutoLogin::STRICT },
{ "relaxed", AutoLogin::RELAXED },
{ "never", AutoLogin::NEVER },
});
if (o)
{
chanmodes = o->chanmodes;
@ -1391,6 +1397,24 @@ OperAccount::OperAccount(const std::string& n, const std::shared_ptr<OperType>&
Configure(t, true);
}
bool OperAccount::CanAutoLogin(LocalUser* user) const
{
switch (autologin)
{
case AutoLogin::STRICT:
return user->nick == GetName();
case AutoLogin::RELAXED:
return true;
case AutoLogin::NEVER:
return false;
}
// Should never be reached.
return false;
}
bool OperAccount::CheckPassword(const std::string& pw) const
{
return ServerInstance->PassCompare(password, pw, passwordhash);