{"id":356632,"date":"2026-08-26T12:34:18","date_gmt":"2026-08-26T12:34:18","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/sentinel-security-center\/"},"modified":"2026-08-28T09:26:18","modified_gmt":"2026-08-28T09:26:18","slug":"vokull-security-center","status":"publish","type":"plugin","link":"https:\/\/hsb.wordpress.org\/plugins\/vokull-security-center\/","author":15002685,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.9.0","stable_tag":"1.9.0","tested":"7.1","requires":"6.5","requires_php":"8.1","requires_plugins":null,"header_name":"Vokull Security Center","header_author":"Steven Glogger","header_description":"Security monitoring and alerting for WordPress (\"v\u00f6kull\" is Icelandic for \"vigilant\/watchful\"). Logs and alerts on plugin\/theme changes, administrator and role changes, configuration changes, filesystem integrity and logins from countries outside your allow list \u2014 with optional login blocking and two-factor authentication. Administrator-only, with immediate e-mail alerts. There is no PRO version. All free.","assets_banners_color":"","last_updated":"2026-08-28 09:26:18","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/github.com\/sglogger\/vokull-security-center","header_author_uri":"https:\/\/www.glogger.ch","rating":0,"author_block_rating":0,"active_installs":0,"downloads":101,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.7.0":{"tag":"1.7.0","author":"glogger","date":"2026-08-26 12:33:56","revision":3667003},"1.8.0":{"tag":"1.8.0","author":"glogger","date":"2026-08-27 17:40:12","revision":3669321},"1.9.0":{"tag":"1.9.0","author":"glogger","date":"2026-08-28 09:26:18","revision":3670079}},"upgrade_notice":{"1.9.0":"<p>Adds failed-login rate limiting, on after the update: three wrong passwords lock an address out for 15 minutes, five lockouts hold it a day. Configurable under Settings, never applies to the local network or allow-listed addresses. Also stops reporting liesmich.html as a missing core file.<\/p>","1.8.0":"<p>Adds passkeys as a second factor alongside authenticator apps, and optional passwordless sign-in. Nothing to do after updating: existing two-factor setups are untouched and passkeys are opt-in per account. On an HTTPS site, users will find &quot;Add a passkey&quot; on their Two-factor screen.<\/p>","1.7.0":"<p>Sentinel Security Center is now Vokull Security Center. Settings, log and baselines are preserved. One manual step: the main plugin file was renamed, so WordPress leaves the plugin switched off after updating \u2014 activate it again on the Plugins screen.<\/p>","1.6.6":"<p>Documentation and housekeeping only, with no functional change and nothing to do after updating. The readme now spells out the two external services the plugin can contact, what each request sends, and when.<\/p>","1.6.5":"<p>Adds an alert for plugins that have an update waiting. Nothing is e-mailed until you set that event to e-mail under Settings; until then it is written to the log like any other event.<\/p>","1.6.0":"<p>The plugin no longer updates itself from GitHub; updates come through WordPress.org from this version on. Install this one through the update offer as it stands, or by uploading the ZIP. WPSEC_GITHUB_TOKEN, if you set it, can be removed from wp-config.php.<\/p>","1.5.2":"<p>Fixes &quot;Check again&quot; reporting no update for hours after one was published. The fix only takes effect once this version is installed: to see it now, use the plugin&#039;s update offer as it stands, or clear the wpsec_gh_release transient.<\/p>","1.5.1":"<p>A build-tooling fix with no functional change from 1.5.0. Nothing to do after updating.<\/p>","1.5.0":"<p>Packaging and metadata only: no functional change, and nothing to do after updating.<\/p>","1.4.0":"<p>A rename and nothing else: WP Security Center is now Sentinel Security Center. Settings, log and baselines are preserved. One manual step: the main plugin file was renamed, so WordPress leaves the plugin switched off after the update. Nothing is monitored until you activate it again.<\/p>","1.3.0":"<p>Adds an IP deny list on the Login &amp; Location tab; nothing changes until you put an address in it. Also fixes the plugins screen offering an update that is already installed, and lets the log search box find rows by event type, IP address and time rather than only by description.<\/p>","1.2.0":"<p>Start at the Hardening screen: it grades this installation against the official WordPress hardening guide and says what to change. Adds optional two-factor authentication \u2014 nothing changes for anyone until a user enrols, or until you require it in Settings. Also fixes three false alarms.<\/p>","1.1.1":"<p>Required if your copy of this plugin is hosted in a private GitHub repository: without it, automatic updates are detected but cannot be downloaded. Install this version once by hand, and every later update will work on its own.<\/p>","1.1.0":"<p>The first release that actually does anything. Review Settings after upgrading: alerts are off until recipients are set, and login blocking stays in monitor mode until you arm it.<\/p>","1.0.0":"<p>Initial release.<\/p>"},"ratings":[],"assets_icons":{"icon-256x256.png":{"filename":"icon-256x256.png","revision":3667003,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256},"icon.svg":{"filename":"icon.svg","revision":3667003,"resolution":false,"location":"assets","locale":false}},"assets_banners":[],"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.7.0","1.8.0","1.9.0"],"block_files":[],"assets_screenshots":[],"screenshots":[]},"plugin_section":[],"plugin_tags":[8531,8534,222353,600,9217],"plugin_category":[54],"plugin_contributors":[277624],"plugin_business_model":[],"class_list":["post-356632","plugin","type-plugin","status-publish","hentry","plugin_tags-activity-log","plugin_tags-audit-log","plugin_tags-passkeys","plugin_tags-security","plugin_tags-two-factor","plugin_category-security-and-spam-protection","plugin_contributors-glogger","plugin_committers-glogger"],"banners":[],"icons":{"svg":"https:\/\/ps.w.org\/vokull-security-center\/assets\/icon.svg?rev=3667003","icon":"https:\/\/ps.w.org\/vokull-security-center\/assets\/icon.svg?rev=3667003","icon_2x":false,"generated":false},"screenshots":[],"raw_content":"<!--section=description-->\n<p>Vokull Security Center (\"v\u00f6kull\" is Icelandic for \"vigilant\/watchful\") watches the things an attacker actually has to touch in order to keep a foothold in a WordPress site, records them in a searchable log, and e-mails you immediately when something matters.<\/p>\n\n<p>It is built around two goals that pull against each other: miss as little as possible, and produce as few false alarms as possible. Every event type can be set individually to immediate e-mail, log only, or off. Login blocking always starts in monitor mode so you can see what a rule would have done before you arm it.<\/p>\n\n<p><strong>What is monitored<\/strong><\/p>\n\n<ul>\n<li>Plugins: installed, activated, deactivated, updated, deleted, plugins with an update waiting, and plugins that appear without a matching install (an SFTP drop).<\/li>\n<li>Themes: installed, activated, updated, deleted.<\/li>\n<li>Users and administrators: created, deleted, role changed, promoted to administrator, demoted, e-mail changed, password changed or reset \u2014 including changes an administrator makes to their own account.<\/li>\n<li>User records altered directly in the database, outside WordPress, detected by a periodic reconciliation scan.<\/li>\n<li>Configuration: critical options such as siteurl, home, admin_email, users_can_register and default_role; wp-config.php and .htaccess changes; WordPress core files verified against the official checksums; cron jobs; newly appearing must-use plugins; XML-RPC and file-editor state; application passwords.<\/li>\n<li>Filesystem: new or changed files in wp-content\/mu-plugins\/, and any PHP file under wp-content\/uploads\/ \u2014 where one never belongs. New PHP files are additionally checked against common backdoor signatures.<\/li>\n<li>Logins: failed attempts, successful logins, a login from a country outside your allow list, and logins refused by the IP deny list \u2014 with optional blocking. Repeated wrong passwords from one address are rate-limited: a configurable number of retries, then a lockout, then a much longer one for an address that keeps coming back.<\/li>\n<li>Two-factor authentication: who switched it on or off, passkeys registered and removed, wrong codes submitted after a correct password, and every use of a recovery code or the e-mail fallback.<\/li>\n<\/ul>\n\n<p>A separate Hardening screen reports the current posture \u2014 file editor, permissions, salts, updates, HTTPS, two-factor coverage and more \u2014 against the official WordPress hardening guide, linking to it at each point.<\/p>\n\n<p><strong>The plugin never modifies, quarantines or deletes a scanned file.<\/strong> It reports, and leaves recovery to you.<\/p>\n\n<p><strong>Hardening report<\/strong><\/p>\n\n<p>A read-only screen grading this installation against the official WordPress hardening guide, with a link to the relevant section of that guide on every check. Twenty-two checks covering the dashboard file editor and DISALLOW_FILE_MODS, file permissions, wp-config.php location and permissions, authentication salts, error output, core and extension updates, unused plugins and themes, administrator count, open registration, HTTPS, two-factor coverage, XML-RPC, alerting, file monitoring and backups.<\/p>\n\n<p>Checks are graded Good, Fix this, Worth fixing \u2014 or \"Your call\", for the ones that genuinely depend on how the site is run rather than having a right answer. Nothing on the page changes anything.<\/p>\n\n<p><strong>Two-factor authentication: passkeys or an authenticator app<\/strong><\/p>\n\n<p>Two independent second factors, and an account may hold either or both. Whichever is used, the session is issued only after the factor is proven \u2014 never before. Enrolment is per account and voluntary by default; a site setting can require a second factor for administrators, with a grace period whose clock starts when you switch the requirement on. Either factor satisfies it.<\/p>\n\n<pre><code>Username + password\n       \u2502\n       \u25bc\nWordPress accepts the password\n       \u2502\n       \u25bc\nDoes the account have a second factor?\n       \u2502\n       \u251c\u2500\u2500 Passkey \u2500\u2500\u2500\u2500\u25ba Face ID \/ Touch ID \/ Hello \u2500\u2500\u2510\n       \u2502                                              \u2502\n       \u251c\u2500\u2500 TOTP \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba six digits from the app \u2500\u2500\u2500\u2500\u2500\u2524\n       \u2502                                              \u2502\n       \u2514\u2500\u2500 Recovery \u2500\u2500\u2500\u25ba one of ten single-use codes \u2500\u2524\n                                                      \u2502\n                                                      \u25bc\n                                                    Login\n<\/code><\/pre>\n\n<p><em>Passkeys.<\/em> A WebAuthn credential held by the phone, laptop, hardware key or password manager that created it. There is nothing to type, nothing to read out over the phone to someone claiming to be support, and the browser will only ever offer the passkey to your exact domain \u2014 so a convincing copy of your login page gets nothing. Only a public key is stored on the site; the private half never leaves the device. Users can register several and see when each was last used. If an authenticator that keeps a signature counter ever repeats a value \u2014 what a cloned key looks like \u2014 that is logged and mailed to you.<\/p>\n\n<p><em>Passwordless sign-in.<\/em> On an HTTPS site you can additionally allow a passkey to sign in on its own, with no password at all. It is off by default, because it is a second way into the site and that is a decision worth taking deliberately. The authenticator must verify the user (fingerprint, face or PIN), and country rules, the IP deny list and the kill switch all still apply.<\/p>\n\n<p><em>Authenticator apps.<\/em> The familiar six digits from any TOTP app. Shared secrets are encrypted with AES-256-GCM under a key derived from the site salts, so a database dump without wp-config.php is useless. Each code is accepted once, so a code read over your shoulder cannot be replayed. The QR code is drawn on your own server \u2014 the secret is never sent to an external QR service.<\/p>\n\n<p>Recovery, in order: ten single-use recovery codes, issued the first time any factor is switched on and shown once; the other factor, if the account has both; optionally a one-time code mailed to the account address; and failing everything, a reset by another administrator.<\/p>\n\n<p>No part of this contacts anything outside your own site. Passkeys are a conversation between the browser and this server; the WebAuthn library is bundled with the plugin.<\/p>\n\n<p><strong>Geo-aware login control<\/strong><\/p>\n\n<p>Country is resolved from your CDN or reverse proxy's country header when the request demonstrably came through it, otherwise from a local MaxMind GeoLite2 database. No external API is called during login. X-Forwarded-For is only trusted when the connecting address is in your configured trusted-proxy list, so the client IP cannot be spoofed.<\/p>\n\n<p>Because locking yourself out is the real risk, there are four independent ways back in: monitor mode is the default, an IP\/CIDR allow list is exempt from blocking, a wp-config.php constant disables blocking outright, and every blocked login e-mails you a single-use, time-limited link that unblocks your current IP.<\/p>\n\n<p><strong>Administrator-only<\/strong><\/p>\n\n<p>The plugin adds no front-end output, no REST routes and no shortcodes. Its menu, notices, assets and actions all require the manage_options capability, and a blocked login is indistinguishable from an ordinary wrong password. The one exception is two-factor enrolment: that belongs to the account holder, so every signed-in user finds a Two-factor entry in their own profile menu and can set up a passkey or an authenticator app there. Nothing else about the plugin becomes visible to them.<\/p>\n\n<p><strong>About the name<\/strong><\/p>\n\n<p>\"V\u00f6kull\" is Icelandic for \"vigilant\", \"watchful\". Which is fairly close to the entire job description: watch, and say something the moment it matters.<\/p>\n\n<h3>External services<\/h3>\n\n<p>This plugin contacts two external services. Both are optional, neither is contacted from the front end or during a login, and no information about your site, your users or your visitors is sent to either.<\/p>\n\n<p><strong>MaxMind GeoLite2<\/strong><\/p>\n\n<p>Used to resolve the country a login came from. The lookup itself happens locally against a downloaded database file, which is why no API is called while anyone signs in \u2014 but the database has to be fetched in the first place, and refreshed as it is reissued.<\/p>\n\n<p>What is sent: a download request to <code>https:\/\/download.maxmind.com\/app\/geoip_download<\/code> carrying the MaxMind licence key you configured and the edition name (<code>GeoLite2-Country<\/code>). MaxMind requires both to authorise the download. Nothing else is transmitted.<\/p>\n\n<p>When: only after you enter a MaxMind licence key under Security Center \u2192 Settings \u2192 Login &amp; Location. Until you do, the service is never contacted. After that, when you press \"Download the GeoIP database now\", and weekly via a scheduled task.<\/p>\n\n<p>Service provided by MaxMind, Inc. \u2014 <a href=\"https:\/\/www.maxmind.com\/en\/geolite2\/eula\">GeoLite2 End User Licence Agreement<\/a>, <a href=\"https:\/\/www.maxmind.com\/en\/privacy-policy\">privacy policy<\/a>.<\/p>\n\n<p><strong>Cloudflare IP ranges<\/strong><\/p>\n\n<p>Used to offer Cloudflare's own address ranges as a ready-made option for the trusted-proxy list, so you do not have to find and paste them yourself.<\/p>\n\n<p>What is sent: nothing beyond the HTTP request. The plugin performs a plain read of the public text files at <code>https:\/\/www.cloudflare.com\/ips-v4<\/code> and <code>https:\/\/www.cloudflare.com\/ips-v6<\/code>.<\/p>\n\n<p>When: only when an administrator presses \"Fetch Cloudflare's address ranges\" under Security Center \u2192 Settings \u2192 Login &amp; Location. Nothing is requested by opening that screen, or by any other part of the plugin, and there is no scheduled task for it; the stored list is re-read only when you press the button again. Fetching alone changes nothing \u2014 the ranges are offered as a suggestion, every line is validated as CIDR notation, and nothing reaches your trusted-proxy list until you separately click to merge them.<\/p>\n\n<p>Service provided by Cloudflare, Inc. \u2014 <a href=\"https:\/\/www.cloudflare.com\/website-terms\/\">website terms of use<\/a>, <a href=\"https:\/\/www.cloudflare.com\/privacypolicy\/\">privacy policy<\/a>.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Install it from the Plugins screen, or upload the release ZIP under Plugins &gt; Add New &gt; Upload Plugin. If you take the ZIP from GitHub, use the <code>vokull-security-center.zip<\/code> release asset and not the \"Download ZIP\" source archive: the source archive carries no <code>vendor\/<\/code> directory and unpacks under a branch-suffixed directory name, which breaks country lookups and updates.<\/li>\n<li>The plugin directory must be named <code>vokull-security-center<\/code>. It is the plugin slug, and updates are matched against it.<\/li>\n<li>Activate it. WordPress Multisite is not supported and activation will stop with an explanation.<\/li>\n<li>Open Security Center \u2192 Settings and set your alert recipients.<\/li>\n<li>For country-based rules, add a MaxMind GeoLite2 licence key (free) and download the database, or configure your CDN's country header.<\/li>\n<li>Leave blocking in monitor mode for a few days, review the log, then arm it.<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"do%20passkeys%20need%20anything%20special%3F\"><h3>Do passkeys need anything special?<\/h3><\/dt>\n<dd><p>An HTTPS site and a reasonably current browser. Nothing else: no service to sign up for, no key to configure, no traffic leaving your server. If the site is not on HTTPS the feature does not offer itself, because browsers refuse to create a passkey over a plain connection.<\/p>\n\n<p>A passkey is bound to your domain. On a subdomain multisite, one registered on a.example.com will not work on b.example.com.<\/p><\/dd>\n<dt id=\"should%20users%20have%20a%20passkey%20or%20an%20authenticator%20app%3F\"><h3>Should users have a passkey or an authenticator app?<\/h3><\/dt>\n<dd><p>A passkey, if the device allows it \u2014 it is the only second factor that cannot be typed into a fake login page. But there is no need to choose: an account can hold both, and either one gets you in. Whichever comes first also issues the recovery codes.<\/p><\/dd>\n<dt id=\"what%20happens%20if%20i%20lose%20my%20authenticator%20app%3F\"><h3>What happens if I lose my authenticator app?<\/h3><\/dt>\n<dd><p>Use one of the ten recovery codes issued when you switched two-factor on. If those are gone too and the site has the e-mail fallback enabled, the sign-in screen can mail a one-time code to the address on your account. If everything is lost, any other administrator can reset your second factor from your profile screen \u2014 you then set it up again.<\/p>\n\n<p>The e-mail fallback is off by default on purpose. It means whoever can read that mailbox can finish the sign-in, which on many sites is the same person who controls the hosting account. Turn it on when losing a phone would otherwise mean losing the site; leave it off otherwise. Every code sent and every code used is written to the log.<\/p><\/dd>\n<dt id=\"does%20two-factor%20cover%20the%20rest%20api%20and%20application%20passwords%3F\"><h3>Does two-factor cover the REST API and application passwords?<\/h3><\/dt>\n<dd><p>No. They are non-interactive \u2014 there is nobody there to type a code \u2014 and an application password is already a separate credential you can revoke on its own. If an account has to be locked down completely, revoke its application passwords as well.<\/p><\/dd>\n<dt id=\"does%20it%20block%20brute-force%20login%20attempts%3F\"><h3>Does it block brute-force login attempts?<\/h3><\/dt>\n<dd><p>Yes, since 1.9.0, under Settings \u2192 Login &amp; Location. An address gets three wrong passwords before it is locked out for fifteen minutes; after five lockouts it is held for twenty-four hours; an address that goes quiet for twenty-four hours is forgotten entirely. Every one of those numbers is yours to change.<\/p>\n\n<p>The lockout is checked before the password is verified, so a locked address does not even get a password hash computed for it. It applies to the login form, to XML-RPC and to application passwords alike \u2014 those are where most password guessing actually happens.<\/p>\n\n<p>This does not replace a firewall, a CDN rule or fail2ban, all of which act before the request reaches PHP and cost you nothing to run. It is what you have when none of those are available to you, and it is aimed at the volume rather than at a determined attacker.<\/p>\n\n<p>Failed attempts are still logged individually as <code>login.failed<\/code>, at Info and log-only. Set that event to \"E-mail\" only if you know the site is quiet \u2014 on a public site bots guess passwords around the clock, and an inbox that learns to ignore this plugin is worse than no alert at all. The lockouts themselves are separate events: <code>login.lockout<\/code> is logged, and <code>login.lockout_extended<\/code> is e-mailed, because an address that is still going after five lockouts is somebody trying rather than a bot passing through. An attempt made while an address is locked out is recorded as <code>login.blocked_lockout<\/code> instead of <code>login.failed<\/code>, so nothing is written twice.<\/p><\/dd>\n<dt id=\"will%20country%20blocking%20stop%20a%20determined%20attacker%3F\"><h3>Will country blocking stop a determined attacker?<\/h3><\/dt>\n<dd><p>No. An attacker using a VPN endpoint inside an allowed country resolves to that country and passes. There is no VPN or Tor detection. Treat this control as something that removes opportunistic foreign traffic, not as a boundary.<\/p><\/dd>\n<dt id=\"what%20happens%20if%20the%20geoip%20database%20is%20missing%20or%20broken%3F\"><h3>What happens if the GeoIP database is missing or broken?<\/h3><\/dt>\n<dd><p>An individual IP that cannot be resolved is treated as not allowed and is blocked. But if the lookup subsystem as a whole is unavailable, blocking automatically falls back to monitor mode and raises a critical alert, so a deleted database file can never lock you out.<\/p><\/dd>\n<dt id=\"the%20status%20screen%20says%20the%20geoip%20self%20test%20failed%2C%20but%20the%20database%20is%20installed.%20why%3F\"><h3>The Status screen says the GeoIP self test failed, but the database is installed. Why?<\/h3><\/dt>\n<dd><p>Almost always because the plugin was installed from a GitHub source archive rather than the release ZIP, so the bundled MaxMind reader library in <code>vendor\/<\/code> is missing. Downloading the database needs no library and succeeds; reading it does. Two-factor enrolment showing no QR code is the same cause. Reinstall from the release ZIP.<\/p><\/dd>\n<dt id=\"can%20i%20get%20locked%20out%3F\"><h3>Can I get locked out?<\/h3><\/dt>\n<dd><p>Country blocking is off until you arm it, and the settings screen refuses to arm it without a working database. If it does happen: the <code>WPSEC_DISABLE_BLOCKING<\/code> constant in wp-config.php disables blocking immediately, and the alert e-mail for every blocked login contains a single-use bypass link.<\/p>\n\n<p>The failed-login rate limit is on out of the box, so it is worth knowing what it will not do. It never applies to the local network, to an address on your always-allowed list, or to one holding a live bypass grant; the same <code>WPSEC_DISABLE_BLOCKING<\/code> constant stands it down; and the Status screen lists every address being held with a button to release them all. At worst an ordinary lockout is a fifteen-minute wait.<\/p><\/dd>\n<dt id=\"are%20logins%20over%20the%20rest%20api%20or%20xml-rpc%20blocked%20too%3F\"><h3>Are logins over the REST API or XML-RPC blocked too?<\/h3><\/dt>\n<dd><p>Not by default. Application passwords and XML-RPC authenticate through the same WordPress hook as an interactive login, so blocking them would silently break integrations hosted abroad. There is a setting to include them.<\/p><\/dd>\n<dt id=\"does%20it%20support%20multisite%3F\"><h3>Does it support Multisite?<\/h3><\/dt>\n<dd><p>No. Activation on a network stops with a message rather than misbehaving quietly.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.9.0<\/h4>\n\n<ul>\n<li>Added: failed-login rate limiting, under Settings \u2192 Login &amp; Location. Five settings, with the defaults in brackets: how many wrong passwords an address may submit (3), how long it is then locked out for (15 minutes), how many lockouts it may collect before the long sentence (5), how long that one lasts (24 hours), and how long an address must be quiet before it is forgotten altogether (24 hours). On by default \u2014 unlike the country rule, this one acts on the site's own record of repeated failures rather than on a database's opinion about where an address sits.<\/li>\n<li>Added: the lockout is enforced before the password is checked, so a locked address never gets a password hash computed for it. It covers the login form, XML-RPC and application passwords alike.<\/li>\n<li>Added: three log events \u2014 <code>login.lockout<\/code> (logged), <code>login.lockout_extended<\/code> (e-mailed, because an address still going after five lockouts is somebody trying rather than a bot passing through), and <code>login.blocked_lockout<\/code> for each attempt made during a lockout. The last one takes the place of the <code>login.failed<\/code> line that attempt would otherwise have produced rather than being written next to it, so the log does not grow. All three are configurable on the Alerts tab like every other event.<\/li>\n<li>Added: the Status screen lists every address currently being held, with how long is left, how many lockouts it has collected and the last user name it tried \u2014 and a button to release them all at once.<\/li>\n<li>Added: the login screen now says how many attempts are left before a lockout, and says plainly when the lockout has just started. Withholding that would only surprise the person who mistyped; whoever is guessing already knows how many times they have tried.<\/li>\n<li>Added: Settings \u2192 File Integrity gained a \"Core files to ignore\" box, for core files your particular install is known to be missing.<\/li>\n<li>Fixed: the translated readme that a localised WordPress ships in place of readme.html \u2014 liesmich.html in German, lisezmoi.html in French, and so on \u2014 is no longer reported as a missing core file. Hosts strip it as routinely as they strip readme.html, which was already ignored.<\/li>\n<li>Note: the rate limit never applies to the local network, to an address on your always-allowed list, or to one holding a live bypass grant, and <code>WPSEC_DISABLE_BLOCKING<\/code> stands it down along with the country rule. It cannot be the thing that shuts you out of your own site.<\/li>\n<\/ul>\n\n<h4>1.8.0<\/h4>\n\n<ul>\n<li>Added: passkeys. A second factor that is not a code \u2014 a WebAuthn credential held by the phone, laptop, hardware key or password manager that created it. There is nothing to type and nothing to read out over the phone, and the browser will only ever offer the passkey to your exact domain, so a convincing copy of your login page gets nothing. Only a public key is stored on the site; the private half never leaves the device.<\/li>\n<li>Added: passkeys and authenticator apps sit side by side. An account can hold either or both, either one satisfies the \"administrators must use two-factor\" requirement, and the setup screen lists registered passkeys with when each was added and last used, with renaming and removal. Up to ten per account.<\/li>\n<li>Added: passwordless sign-in, off by default. Where you switch it on, the login screen gains a \"Sign in with a passkey\" button, and browsers that support it offer the passkey from the username field's own autofill list. The authenticator must verify the user \u2014 fingerprint, face or PIN \u2014 so the device and the person holding it are both proven. Country rules, the IP deny list and the kill switch all still apply, and every such sign-in is logged as one.<\/li>\n<li>Added: registering your first passkey now also issues your recovery codes, if you do not have a set. A passkey lives on one device, and losing that device with nothing written down would otherwise mean losing the account.<\/li>\n<li>Added: new log events for passkeys registered, removed, used and used for a passwordless sign-in, for refused verifications, and for a signature-counter anomaly. That last one is what a cloned authenticator looks like \u2014 an authenticator that keeps a counter should never repeat a value \u2014 and it is reported at critical severity by e-mail. The sign-in itself is not refused, because a mis-implemented authenticator would otherwise lock a legitimate user out for good.<\/li>\n<li>Added: two settings under Settings \u2192 Two-factor \u2014 whether users may register passkeys (on by default; the feature simply never offers itself on a site without HTTPS), and whether a passkey may sign in without the password (off by default).<\/li>\n<li>Changed: the authenticator app can now be removed on its own, leaving your passkeys and recovery codes in place. \"Turn off two-factor authentication\" still removes everything, as does an administrator reset \u2014 an account you believe is unprotected must not still hold a credential that can sign in.<\/li>\n<li>Changed: recovery codes are no longer reissued when a second factor is added to an account that already has a set. Reissuing would silently invalidate the codes you filed away.<\/li>\n<li>Note: passkeys need HTTPS, because browsers refuse to create one over a plain connection, and they are bound to your domain \u2014 on a subdomain multisite, one registered on a.example.com will not work on b.example.com. Nothing about this contacts anything outside your own site: the WebAuthn library is bundled with the plugin and reaches no network.<\/li>\n<\/ul>\n\n<h4>1.7.0<\/h4>\n\n<ul>\n<li>Changed: the plugin is now called Vokull Security Center, with the permalink and text domain vokull-security-center. The WordPress.org review flagged \"Sentinel\" as a name already carried by well-known security products in this same field. V\u00f6kull is Icelandic for \"vigilant\", \"watchful\" \u2014 the name the project has been developed under all along.<\/li>\n<li>Changed: what the plugin calls itself on screen is now simply \"Security Center\" everywhere \u2014 the activation and deactivation log entries, the alert e-mail footer, the multisite refusal and the platform guards. The full name is what appears on the Plugins screen and in the directory.<\/li>\n<li>Changed: Cloudflare's published address ranges are now fetched only when you press a button on the Login &amp; Location tab. Opening that tab used to read them as a side effect of rendering the page \u2014 nothing about the site was ever sent, but a settings screen should not contact a third party on your behalf. There is no scheduled refresh either. Ranges already stored keep working, and merging them into the trusted-proxy list remains a separate click.<\/li>\n<li>Fixed: the error message shown when a login is refused by a country rule, or when API authentication is refused for an account with two-factor, was not translatable \u2014 it was passed through WordPress' translation function without this plugin's text domain, so it stayed English in every language. The wording is unchanged; it is simply translated now like everything else.<\/li>\n<li>Removed: the bundled German translation and the translation template. Translations for a plugin hosted on WordPress.org come from translate.wordpress.org, which delivers them per locale through the ordinary update system; a bundled copy only duplicates that and goes stale against it. The strings themselves are unchanged.<\/li>\n<li>Unchanged: your settings, the log and the file and user baselines are all preserved. The option names, database tables and the GeoIP directory under uploads keep their existing prefix and are untouched.<\/li>\n<li>Note: as with the previous rename, this one leaves the plugin deactivated, because WordPress reactivates a plugin by the file path it recorded and the main plugin file has been renamed. Activate Vokull Security Center on the Plugins screen and monitoring resumes as before. Until you do, nothing is being monitored.<\/li>\n<\/ul>\n\n<h4>1.6.6<\/h4>\n\n<ul>\n<li>Added: an \"External services\" section to this readme, documenting exactly what the MaxMind and Cloudflare requests send, and when. Neither is contacted until you configure it, and neither ever sees anything about your site or your visitors.<\/li>\n<li>Changed: the GeoIP downloader now deletes its temporary files through WordPress rather than calling unlink() directly.<\/li>\n<li>Changed: the plugin no longer calls load_plugin_textdomain(). WordPress has loaded translations on demand since 4.6 and does it for us.<\/li>\n<li>Fixed: the German translation was missing the two strings added in 1.6.5, and the translation template still offered three strings from features that have been removed. Both are up to date again.<\/li>\n<li>Housekeeping: no functional change otherwise. The code-standards exemptions moved from the project ruleset onto the statements they apply to, so the reasoning is visible where it matters and automated checks can see it.<\/li>\n<\/ul>\n\n<h4>1.6.5<\/h4>\n\n<ul>\n<li>Added: a daily check for plugins with an update waiting, logged as its own event with the installed and available versions. It starts at log only \u2014 switch it to e-mail under Settings if you want to be told. An unpatched plugin is the most common way a site is taken over.<\/li>\n<li>Removed: the log event for changes to the automatic-update options. The Hardening screen still reports whether automatic updates are switched on, and disabling them through wp-config.php is still logged.<\/li>\n<\/ul>\n\n<h4>1.6.0<\/h4>\n\n<ul>\n<li>Removed: the built-in updater that installed updates from GitHub Releases, along with the Update URI header. Plugins hosted on WordPress.org may not install or serve updates from an external source; updates now reach your site the ordinary way, through WordPress.<\/li>\n<li>Note: the WPSEC_GITHUB_TOKEN constant no longer does anything and can be deleted from wp-config.php.<\/li>\n<\/ul>\n\n<h4>1.5.2<\/h4>\n\n<ul>\n<li>Fixed: \"Check again\" on the Updates screen could report no update for up to six hours after one had been released. The plugin caches its release lookup to stay under the GitHub rate limit, and was reading that cache back even when you had explicitly asked WordPress to check again. A forced check now re-queries GitHub.<\/li>\n<li>Added: the plugin's own icon, wherever WordPress previously drew the generic puzzle piece \u2014 the Updates screen and the plugin details modal. The admin menu keeps its shield.<\/li>\n<\/ul>\n\n<h4>1.5.1<\/h4>\n\n<ul>\n<li>Fixed: a stale composer.lock left over from the 1.4.0 rename made the automated test run fail. Build tooling only; the plugin itself is unchanged from 1.5.0.<\/li>\n<\/ul>\n\n<h4>1.5.0<\/h4>\n\n<ul>\n<li>Fixed: readme.txt declared \"Tested up to: 7.0.4\". That field takes a WordPress major version only, and a patch number in it is an error at review time, so it now reads 7.0.<\/li>\n<li>Fixed: the release ZIP shipped a vendor\/ directory built by Composer without the composer.json that describes it, which the plugin review tooling flags. composer.json is now packaged alongside it.<\/li>\n<\/ul>\n\n<h4>1.4.0<\/h4>\n\n<ul>\n<li>Changed: the plugin is now called Sentinel Security Center. WordPress.org does not allow a plugin name or permalink to begin with \"wp\", so the name, the slug and the text domain changed from wp-security-center to sentinel-security-center, and the main plugin file was renamed to match.<\/li>\n<li>Changed: the GitHub repository moved to sglogger\/sentinel-security-center and the updater now queries it. The old URLs redirect.<\/li>\n<li>Unchanged: your settings, the log and the file and user baselines are all preserved. The option names, database tables and the GeoIP directory under uploads keep their existing prefix and are untouched.<\/li>\n<li>Note: this upgrade leaves the plugin deactivated, because WordPress reactivates a plugin by the file path it recorded and the main plugin file has been renamed. Activate Sentinel Security Center on the Plugins screen and monitoring resumes as before. Until you do, nothing is being monitored.<\/li>\n<\/ul>\n\n<h4>1.3.0<\/h4>\n\n<ul>\n<li>Security: two-factor authentication could be bypassed by authenticating through xmlrpc.php with the account password, because XML-RPC never fires the hook the challenge hangs on. Primary-password API authentication is now refused for accounts with a second factor; application passwords are unaffected.<\/li>\n<li>Security: the GitHub updater token could be sent to a foreign host if any WordPress HTTP request contained the asset URL as a substring, e.g. in a query string. The URL is now matched structurally by scheme, host and path.<\/li>\n<li>Security: CSV export now neutralises spreadsheet formula injection \u2014 cells starting with =, +, - or @ are prefixed with a quote, since the log deliberately records attacker-typed strings.<\/li>\n<li>Security: two-factor attempts are now capped per user across all addresses, so rotating IPs does not multiply the guess budget.<\/li>\n<li>Added: an IP deny list for IPv4 and IPv6, single addresses or CIDR blocks, on the Login &amp; Location tab. Denied addresses can never sign in: the list overrides the allow list, an allowed country and the private-network exemption, and applies even when country checking is off. No bypass link is issued for a denied address, and the settings screen refuses to store an entry matching the address you are saving from.<\/li>\n<li>Added: the login.blocked_denylist event, defaulting to log only. Because the check runs after the password is verified, an entry means someone at that address had working credentials.<\/li>\n<li>Changed: the log search box now searches the event type, the IP address and the timestamp as well as the description, the object and the user \u2014 everything a row puts on screen.<\/li>\n<li>Changed: tested up to WordPress 7.0.4.<\/li>\n<li>Fixed: the plugins screen kept offering an update to a version that was already installed, when the files had been updated by any means other than the updater itself. The cached check is now corrected on read, and is discarded outright when the version on disk changes.<\/li>\n<li>Fixed: the plugin details modal showed the changelog of the installed version rather than of the version being offered, and reported the last released version even on a copy that was newer.<\/li>\n<\/ul>\n\n<h4>1.2.0<\/h4>\n\n<ul>\n<li>Added: a Hardening screen. Twenty-two read-only checks graded against the official WordPress hardening guide, each linking to the section it comes from. Verdicts include \"Your call\" for the decisions that depend on how the site is run \u2014 DISALLOW_FILE_MODS being the clearest, since it blocks plugin installation and every security update alike.<\/li>\n<li>Added: two-factor authentication (TOTP). A one-time code from any authenticator app, asked for after the password is accepted; the session is only issued once that code is right. Enrolment is per account and voluntary by default, with a site setting to require it for administrators after a grace period.<\/li>\n<li>Added: recovery for a lost authenticator \u2014 ten single-use recovery codes shown once at enrolment, an optional one-time code by e-mail (off by default, because it reduces the second factor to whoever reads the mailbox), and a reset by another administrator as the last resort.<\/li>\n<li>Added: failed login attempts are recorded as login.failed, at Info and log only. Nothing is enforced on a failure; rate limiting still belongs in your firewall or CDN.<\/li>\n<li>Fixed: the file scanner reported the plugin's own GeoIP guard files under uploads as a critical find. The GeoIP refresh was overwriting the recorded path of its own directory, which is what the scanner used to recognise them.<\/li>\n<li>Fixed: a .htaccess in the uploads directory was reported as \"an executable file \u2026 should never contain PHP\", which is wrong on both counts. It is now its own finding, with the event the registry already defined for it.<\/li>\n<li>Fixed: on a localised WordPress, wp-includes\/version.php was reported as modified on every scan. The checksum manifest is now chosen by the package the core was built from rather than by the site's current language.<\/li>\n<li>Fixed: the \"View details\" link vanished from the plugins list whenever GitHub could not be reached. The plugin now always registers itself in the update transient, and the details modal no longer offers a WordPress.org page that does not exist.<\/li>\n<\/ul>\n\n<h4>1.1.1<\/h4>\n\n<ul>\n<li>Fixed: updating from a private GitHub repository failed after the update had already been offered. The release asset was fetched from its browser URL, which cannot carry a token; it is now fetched from the API asset URL with the token and the correct Accept header. Public repositories were unaffected.<\/li>\n<\/ul>\n\n<h4>1.1.0<\/h4>\n\n<ul>\n<li>First functional release. Everything below is new.<\/li>\n<li>Event log with a filterable admin viewer, search, sorting, per-page control and CSV export of exactly the filtered view.<\/li>\n<li>Monitoring of plugins and themes: install, activate, deactivate, update, delete, auto-update, and plugins that appear on disk without an install.<\/li>\n<li>Monitoring of users and administrators: creation, deletion, role change, promotion and demotion, e-mail and password changes, application passwords, and changes an administrator makes to their own account.<\/li>\n<li>Detection of user records altered directly in the database, by hourly reconciliation against a stored baseline. This is the only way to see a changed login name, which WordPress itself provides no path for.<\/li>\n<li>Configuration monitoring: critical options, wp-config.php and .htaccess hashes, WordPress core files against the official checksums, cron jobs, new must-use plugins, XML-RPC and file-editor state.<\/li>\n<li>File integrity for wp-content\/mu-plugins and any PHP file under uploads, with weighted backdoor-signature heuristics. Files are only ever read, never modified, quarantined or deleted.<\/li>\n<li>Geo-aware login control: country from a trusted CDN header or a local MaxMind GeoLite2 database, monitor mode by default, optional blocking, and four independent ways back in if you lock yourself out.<\/li>\n<li>Immediate e-mail alerts, configurable per event type as e-mail, log only, or off, with an hourly circuit breaker so a mass finding cannot flood a mail server.<\/li>\n<li>Diagnostics screen showing how the site sees your address, and a what-if test for any other address.<\/li>\n<li>Complete German translation.<\/li>\n<\/ul>\n\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>Initial scaffolding release.<\/li>\n<\/ul>","raw_excerpt":"Security monitoring and alerting: plugin, user, role and config changes, file integrity, passkeys, two-factor authentication and geo-aware logins.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/hsb.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/356632","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hsb.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/hsb.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/hsb.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=356632"}],"author":[{"embeddable":true,"href":"https:\/\/hsb.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/glogger"}],"wp:attachment":[{"href":"https:\/\/hsb.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=356632"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/hsb.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=356632"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/hsb.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=356632"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/hsb.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=356632"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/hsb.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=356632"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/hsb.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=356632"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}