{"id":358993,"date":"2026-08-26T18:05:48","date_gmt":"2026-08-26T18:05:48","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/kwistech-secure\/"},"modified":"2026-08-26T18:05:31","modified_gmt":"2026-08-26T18:05:31","slug":"kwistech-secure","status":"publish","type":"plugin","link":"https:\/\/ary.wordpress.org\/plugins\/kwistech-secure\/","author":23555233,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.6.0","stable_tag":"1.6.0","tested":"7.1","requires":"6.2","requires_php":"7.4","requires_plugins":null,"header_name":"Kwistech Secure","header_author":"Kwistech","header_description":"Twelve local hardening controls with an audit trail: two-factor sign-in, login rate limiting, a custom sign-in URL, username-discovery blocking, security response headers, an enforced password policy, XML-RPC off, file editor disabled, and version fingerprints removed. No account, no telemetry, no third-party requests.","assets_banners_color":"161f2d","last_updated":"2026-08-26 18:05:31","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/kwistech.ca\/kwistech-secure","header_author_uri":"https:\/\/kwistech.ca","rating":0,"author_block_rating":0,"active_installs":0,"downloads":42,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.6.0":{"tag":"1.6.0","author":"kwistech","date":"2026-08-26 18:05:31","revision":3667592}},"upgrade_notice":{"1.6.0":"<p>Renames every stored option, table and scheduled job to a longer, directory-compliant prefix.\nYour settings, audit log and two-factor enrolment are migrated automatically on the first\nwp-admin page load after upgrading -- nothing to do by hand. If you query this plugin&#039;s data\nfrom your own code, the names have changed; see the changelog. Update the paid add-on to 1.2.0\nat the same time if you use it.<\/p>","1.5.0":"<p>Adds a dismissible notice on this plugin&#039;s own screens describing the paid evidence-pack\nadd-on. It shows nowhere else, never shows if you already have the add-on, and stays gone once\ndismissed. No change to any hardening control.<\/p>","1.4.0":"<p>Three new controls, all on by default and none able to lock you out: username-discovery\nblocking, security response headers, and an enforced minimum password length. Existing\nsettings are untouched. Review them under Kwistech Secure, Hardening.<\/p>","1.3.0":"<p>Renamed. The folder is now kwistech-secure. If you installed 1.2.0 from a zip, delete the old\nkwistech-wp-secure folder or WordPress runs both copies. Settings and audit log carry across.\nUpdate the Insurance Evidence Pack too: its hook names changed.<\/p>","1.2.0":"<p>Hardening is unchanged and needs no action. If you use the evidence pack, install the separate\nInsurance Evidence Pack plugin from kwistech.ca \u2014 your licence key and existing packs carry\nover untouched.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3667588,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3667588,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3667588,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3667588,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.6.0"],"block_files":[],"assets_screenshots":{"screenshot-1.png":{"filename":"screenshot-1.png","revision":3667588,"resolution":"1","location":"assets","locale":"","width":1425,"height":1449},"screenshot-2.png":{"filename":"screenshot-2.png","revision":3667588,"resolution":"2","location":"assets","locale":"","width":1440,"height":961},"screenshot-3.png":{"filename":"screenshot-3.png","revision":3667588,"resolution":"3","location":"assets","locale":"","width":1440,"height":580}},"screenshots":[]},"plugin_section":[],"plugin_tags":[8534,31093,1229,600,9217],"plugin_category":[54],"plugin_contributors":[277709],"plugin_business_model":[],"class_list":["post-358993","plugin","type-plugin","status-publish","hentry","plugin_tags-audit-log","plugin_tags-hardening","plugin_tags-login-security","plugin_tags-security","plugin_tags-two-factor","plugin_category-security-and-spam-protection","plugin_contributors-kwistech","plugin_committers-kwistech"],"banners":{"banner":"https:\/\/ps.w.org\/kwistech-secure\/assets\/banner-772x250.png?rev=3667588","banner_2x":"https:\/\/ps.w.org\/kwistech-secure\/assets\/banner-1544x500.png?rev=3667588","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/kwistech-secure\/assets\/icon-128x128.png?rev=3667588","icon_2x":"https:\/\/ps.w.org\/kwistech-secure\/assets\/icon-256x256.png?rev=3667588","generated":false},"screenshots":[{"src":"https:\/\/ps.w.org\/kwistech-secure\/assets\/screenshot-1.png?rev=3667588","caption":""},{"src":"https:\/\/ps.w.org\/kwistech-secure\/assets\/screenshot-2.png?rev=3667588","caption":""},{"src":"https:\/\/ps.w.org\/kwistech-secure\/assets\/screenshot-3.png?rev=3667588","caption":""}],"raw_content":"<!--section=description-->\n<p>Kwistech Secure hardens a WordPress site using only what WordPress already gives you. It is\nnot a firewall, not a malware scanner, and not a service \u2014 everything it does happens on your\nown server, and it never contacts anybody.<\/p>\n\n<p><strong>Twelve controls, each independently switchable, each logged when it changes:<\/strong><\/p>\n\n<ul>\n<li><strong>Two-factor sign-in<\/strong> \u2014 RFC 6238 authenticator codes with one-time recovery codes. Detects\nan existing 2FA plugin and stays out of the way rather than fighting it.<\/li>\n<li><strong>Login rate limiting<\/strong> \u2014 counts failures by IP address <em>and<\/em> by username, so a password\nsprayed across many accounts from one host and one account attacked from a botnet are both\ncaught. Usernames are stored only as a salted hash.<\/li>\n<li><strong>Non-enumerating login errors<\/strong> \u2014 one identical message for every failure, on the sign-in\nform and the password-reset form alike, so neither confirms which usernames exist.<\/li>\n<li><strong>Custom sign-in address<\/strong> \u2014 moves <code>\/wp-login.php<\/code>. Tests the new address before saving it\nand provides a break-glass constant, because the obvious way to implement this is also the\nobvious way to lock yourself out.<\/li>\n<li><strong>File editor disabled<\/strong> \u2014 removes the most direct path from a stolen administrator session\nto arbitrary code execution.<\/li>\n<li><strong>XML-RPC off<\/strong> \u2014 closes a long-standing brute-force amplifier that most sites never use.<\/li>\n<li><strong>Application password inventory<\/strong> \u2014 reports how many exist per user, never their values.\nThey bypass two-factor, and most people have forgotten the ones they created.<\/li>\n<li><strong>Version fingerprints removed<\/strong> \u2014 generator tags, feed generators, <code>X-Powered-By<\/code>.<\/li>\n<li><strong>Username discovery blocked<\/strong> \u2014 <code>?author=1<\/code> scans, the REST user list, the user sitemap\nand oEmbed author fields all stop answering anonymous callers. Author archives at their own\naddress keep working, and so does everything a signed-in editor needs. The control also\nreports when an account's display name <em>is<\/em> its username, because when that is true the\nbyline gives the game away regardless.<\/li>\n<li><strong>Security response headers<\/strong> \u2014 <code>X-Content-Type-Options<\/code>, <code>X-Frame-Options<\/code>,\n  Referrer-Policy and <code>X-Permitted-Cross-Domain-Policies<\/code>, and never overwriting one your\nserver or CDN already sends. Deliberately no CSP: any policy that leaves wp-admin usable\nneeds <code>unsafe-inline<\/code>, which does not stop the attack CSP exists to stop.<\/li>\n<li><strong>Enforced password policy<\/strong> \u2014 a minimum length, and a refusal of passwords containing the\nusername or the site name, applied server-side when a password is set or reset. WordPress\nitself only shows a strength meter next to a \"confirm use of weak password\" checkbox.<\/li>\n<li><strong><code>unfiltered_html<\/code> restricted<\/strong> \u2014 via a capability filter rather than the all-or-nothing\nconstant, so administrators keep the workflows they need.<\/li>\n<\/ul>\n\n<p><strong>An append-only activity log<\/strong> records every setting change with the old value, the new\nvalue, who made it, from what address, and when. There is no edit path and no single-row\ndelete; the only removal is the scheduled retention sweep, which logs itself.<\/p>\n\n<h4>What it does not do<\/h4>\n\n<p>Being clear about this matters more than the feature list:<\/p>\n\n<ul>\n<li>It does not scan for malware, and a clean install proves nothing about whether you are\ninfected.<\/li>\n<li>It does not filter traffic. It is not a WAF and cannot stop an attack in progress.<\/li>\n<li>It does not patch anything. It reports on outdated core and plugins; updating is yours.<\/li>\n<li>It cannot protect against someone who already has your administrator password <em>and<\/em> your\nsecond factor.<\/li>\n<\/ul>\n\n<h4>Privacy<\/h4>\n\n<p>This plugin makes <strong>no third-party network request of any kind<\/strong>. There is no account, no\nregistration, no telemetry, no usage reporting, no licence check, and no remote access. It\ndoes not phone home because there is nowhere for it to phone.<\/p>\n\n<p>Two requests are made, both to your own website address: an HTTPS request to your home page\nto check whether a security header is present, and a one-off request to a new sign-in address\nto confirm it works before saving it. Neither carries anything about you.<\/p>\n\n<p>Data stored, all locally: the activity log (400 days), failed sign-in attempts with usernames\nhashed (30 days), and encrypted two-factor secrets in user meta until you turn the feature\noff. Deleting the plugin removes all of it.<\/p>\n\n<h4>Optional paid add-on<\/h4>\n\n<p>A separate plugin, <strong>Kwistech Secure \u2014 Insurance Evidence Pack<\/strong>, generates a dated PDF and\nJSON report of this site's security posture for an insurance broker. It is sold at\n<a href=\"https:\/\/kwistech.ca\/kwistech-secure\">kwistech.ca<\/a> and is not required for anything here.<\/p>\n\n<p>Everything described on this page is free, works without it, and always will. This plugin\ncontains no locked functionality.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Upload the plugin through <strong>Plugins \u2192 Add New \u2192 Upload Plugin<\/strong>, or install it from the\ndirectory.<\/li>\n<li>Activate it. Ten of the twelve controls switch on immediately at safe defaults.<\/li>\n<li>Open <strong>Kwistech Secure \u2192 Hardening<\/strong> and decide on the two that need a decision from you: the\ncustom sign-in address and two-factor enforcement. Both default to off, deliberately \u2014\nenabling either without preparation can lock you out of your own site.<\/li>\n<\/ol>\n\n<p>No configuration file to edit, no keys to obtain, no account to create.<\/p>\n\n<!--section=faq-->\n<dl>\n<dt id=\"will%20this%20lock%20me%20out%3F\"><h3>Will this lock me out?<\/h3><\/dt>\n<dd><p>Two controls can, and both are built to make that hard. The custom sign-in address makes a\nreal request to the new address before saving it, and refuses to save if it does not answer;\nif the check cannot run, you must tick an acknowledgement, and that choice is recorded.\nTwo-factor enforcement cannot be switched on until at least one administrator has enrolled,\nbecause requiring codes before anyone has an authenticator locks out everybody at once.<\/p>\n\n<p>If you are locked out anyway, add <code>define( 'KWISTECH_SECURE_DISABLE_LOGIN_SLUG', true );<\/code> to\n    wp-config.php and <code>\/wp-login.php<\/code> works again immediately.<\/p><\/dd>\n<dt id=\"does%20it%20conflict%20with%20my%20existing%20security%20plugin%3F\"><h3>Does it conflict with my existing security plugin?<\/h3><\/dt>\n<dd><p>It detects other two-factor plugins and steps aside rather than running a second\nimplementation. For firewall and scanning plugins there is no overlap \u2014 this one does neither.\nSwitch off any individual control that duplicates something you already have.<\/p><\/dd>\n<dt id=\"does%20it%20slow%20the%20site%20down%3F\"><h3>Does it slow the site down?<\/h3><\/dt>\n<dd><p>The hardening controls are filters and capability checks with no database work on a normal\npage load. Nothing runs on the front end except the header changes.<\/p><\/dd>\n<dt id=\"does%20it%20work%20on%20multisite%3F\"><h3>Does it work on multisite?<\/h3><\/dt>\n<dd><p>The code paths exist and have never been run on a real network. Treat multisite as\nunsupported rather than broken.<\/p><\/dd>\n<dt id=\"what%20happens%20to%20my%20data%20if%20i%20uninstall%20it%3F\"><h3>What happens to my data if I uninstall it?<\/h3><\/dt>\n<dd><p>Deleting the plugin removes its options, both database tables, the scheduled job, and every\nuser meta key it set, including two-factor secrets and recovery codes. Deactivating changes\nnothing. Constants you added to <code>wp-config.php<\/code> by hand are left alone, because this plugin\nnever edits that file.<\/p><\/dd>\n<dt id=\"is%20the%20source%20readable%3F\"><h3>Is the source readable?<\/h3><\/dt>\n<dd><p>Yes, deliberately. No minified files, no bundled dependencies, no build step, no obfuscation.\nA security plugin you cannot read is a security plugin you cannot check.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.6.0<\/h4>\n\n<ul>\n<li>Every option, user meta key, database table and scheduled job this plugin stores has moved\nfrom the <code>kws_<\/code> prefix to <code>kwistech_secure_<\/code>. Three characters is below the four-character\nminimum the plugin directory asks of a prefix, and it was the one part of the plugin still\nusing the short form. <strong>Your data is migrated automatically<\/strong> the first time an administrator\nloads a wp-admin page after upgrading: settings, audit log, login history, two-factor\nenrolment and recovery codes all carry across. Tables are renamed rather than copied, so no\naudit entry is lost.<\/li>\n<li>If you read this plugin's data directly -- a WP-CLI script, a dashboard widget, a database\nquery -- update it to the new names. <code>kws_settings<\/code> is now <code>kwistech_secure_settings<\/code>, the\naudit table is now <code>{prefix}kwistech_secure_audit_log<\/code>, and the daily job is now\n  kwistech_secure_daily_maintenance.<\/li>\n<li>The key used to encrypt two-factor secrets at rest is rotated as part of the same change.\nThis is transparent: existing secrets are still read under the previous key, so your\nauthenticator app keeps working and there is nothing to re-enrol.<\/li>\n<li>Removed the cleanup that deleted two retired database tables and a retired encrypted\ncredential left over from a payment feature that was never released. No installation has\never held that data, so the code deleted something that could not exist.<\/li>\n<li>The add-on notice introduced in 1.5.0 is now four lines instead of twelve, and appears only\non this plugin's dashboard rather than on all four of its screens.<\/li>\n<li>Reading a submitted form field now requires that the capability and nonce check has already\npassed, enforced in code rather than by convention. No behaviour changes for anyone using\nthe plugin normally; it closes the gap where a handler added in future could forget the\ncheck.<\/li>\n<li>Requires the Insurance Evidence Pack add-on 1.2.0 or newer, if you use it. Older add-on\nversions keep working but will not have their own data migrated.<\/li>\n<\/ul>\n\n<h4>1.5.0<\/h4>\n\n<ul>\n<li>Added a one-off notice on this plugin's own screens introducing the Insurance Evidence Pack\nadd-on. It appears nowhere else in wp-admin, never appears if the add-on is installed, is\ndismissed permanently and per-user, is styled as information rather than as a warning, and\nloads nothing from anywhere. Developers and agencies can switch it off with the\n  kwistech_secure_show_addon_notice filter.<\/li>\n<li>Fixed a test-isolation bug where the active-plugin cache leaked between unit tests.<\/li>\n<\/ul>\n\n<h4>1.4.0<\/h4>\n\n<ul>\n<li><strong>Three new hardening controls, taking the total to twelve.<\/strong> All three default to on; none\nof them can lock you out.\n\n<ul>\n<li><em>Block username discovery.<\/em> Closes <code>?author=N<\/code> scans, <code>\/wp-json\/wp\/v2\/users<\/code>, the user\nsitemap and oEmbed author fields to anonymous visitors. This was a known gap, recorded in\nthe threat model as accepted residual R-5. Refusing to confirm usernames at the sign-in\nform while handing the list out over REST was never a coherent position.<\/li>\n<li><em>Security response headers.<\/em> Four of them, and it will not overwrite a header your server\nor CDN already sends.<\/li>\n<li><em>Enforced password policy.<\/em> Minimum length, and no passwords containing the username or\nthe site name. Separators and case are ignored on both sides of that comparison, so\n\"Test-Site-2026\" is caught for a site called \"Test Site\". Existing passwords are stored as\nhashes and cannot be assessed; the policy applies from the next time each one is set.<\/li>\n<\/ul><\/li>\n<li>Added a regression test that the hardening form and its save handler agree on the field\nlist. An unchecked checkbox submits nothing, so a field rendered but not read back is\nsilently switched off on every save \u2014 a bug that only surfaces as \"this setting will not\nstay on\", and precisely the mistake a three-control release invites.<\/li>\n<\/ul>\n\n<h4>1.3.0<\/h4>\n\n<ul>\n<li>Renamed from \"Kwistech WP Secure\" to \"Kwistech Secure\". \"WP\" is a restricted term that\ncannot appear in a plugin name or slug, so the old name could never have been published.\nThe plugin folder is now <code>kwistech-secure<\/code>; if you installed 1.2.0 by hand, delete the old\n  kwistech-wp-secure folder after upgrading or WordPress will run both copies at once.\nSettings, audit log and login history are untouched -- they are keyed independently of the\nfolder name and carry across.<\/li>\n<li>Extension hooks renamed to match the new slug: <code>kws_dashboard_cards<\/code>,\n  kws_settings_panels, <code>kws_admin_notices<\/code> and <code>kws_admin_notice_messages<\/code> become\n  kwistech_secure_*. The opt-out constant <code>KWS_DISABLE_LOGIN_SLUG<\/code> becomes\n  KWISTECH_SECURE_DISABLE_LOGIN_SLUG. Update the add-on to 1.1.0 at the same time.<\/li>\n<li>Now requires WordPress 6.2, up from 6.0. Table names in the audit log and login-attempt\nqueries are passed through <code>$wpdb-&gt;prepare()<\/code> as <code>%i<\/code> identifier placeholders rather than\ninterpolated into the SQL string. The old form was safe -- the names were built from\n  $wpdb-&gt;prefix plus a literal -- but \"safe because you can read the surrounding code\"\nis weaker than \"cannot be otherwise\", and it needed a static-analysis suppression on every\nquery to say so. <code>%i<\/code> arrived in WordPress 6.2.<\/li>\n<\/ul>\n\n<h4>1.2.0<\/h4>\n\n<ul>\n<li>Split into two plugins. The evidence pack is now a separate paid add-on distributed from\nkwistech.ca; this plugin contains no locked functionality and never will.<\/li>\n<li>Added extension points so add-ons attach through documented hooks rather than patching.<\/li>\n<li>Settings screen now explains what is stored and for how long, instead of offering a\nretention control that governed data this plugin no longer owns.<\/li>\n<li>Renamed for clarity. The plugin folder and all settings are unchanged; nothing to migrate.<\/li>\n<\/ul>\n\n<h4>1.1.0<\/h4>\n\n<ul>\n<li>Removed the payment layer entirely. No webhook, no outbound third-party request, no stored\ncredential.<\/li>\n<li>Every entry point is now a capability-checked, nonce-protected wp-admin request.<\/li>\n<\/ul>\n\n<h4>1.0.1<\/h4>\n\n<ul>\n<li>Fixed admin styles being served from cache after an update.<\/li>\n<\/ul>\n\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>Initial release.<\/li>\n<\/ul>","raw_excerpt":"Twelve local hardening controls with an audit trail. No account, no telemetry, no third-party requests, nothing to configure elsewhere.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/ary.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/358993","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ary.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/ary.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/ary.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=358993"}],"author":[{"embeddable":true,"href":"https:\/\/ary.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/kwistech"}],"wp:attachment":[{"href":"https:\/\/ary.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=358993"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/ary.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=358993"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/ary.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=358993"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/ary.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=358993"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/ary.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=358993"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/ary.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=358993"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}