Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions docs/deploy/services/cloudflare.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ Proxy status

.. attention::

If a service expects the client's IP, reconfigure it to use the `CF-Connecting-IP <https://developers.cloudflare.com/fundamentals/reference/http-headers/#cf-connecting-ip>`__ header: for example, `WordFence <https://www.wordfence.com/help/dashboard/options/>`__.

Inform server operators (like RBC Group and Datanomix) to reconfigure web server logging to use the ``CF-Connecting-IP`` header, as we do for Apache and Nginx.

- Proxy A, AAAA and CNAME records for `URL forwarding <https://developers.cloudflare.com/rules/page-rules/how-to/url-forwarding/>`__.
Expand Down
68 changes: 41 additions & 27 deletions docs/develop/update/wordpress.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,18 @@ WordPress

Salt contains `WordPress states <https://docs.saltproject.io/en/latest/ref/states/all/salt.states.wordpress.html>`__, but they are limited. Also, WordPress is often deployed by copying files, rather than via fresh installs.

#. Configure `WP-CLI <https://wp-cli.org>`__. In the server's Pillar file, add, for example:
#. Configure `WP-CLI <https://wp-cli.org>`__ and the site. In the server's Pillar file, add, for example:

.. code-block:: yaml

wordpress:
cli_version: 2.7.1
sites:
coalition:
database: coalition_wp
cron:
contact:
- sysadmin@open-contracting.org

#. :doc:`Deploy the server<../../deploy/deploy>`.
#. :doc:`Connect to the server<../../use/ssh>` as the WordPress user (e.g. ``coalition``).
Expand All @@ -101,58 +107,71 @@ WordPress

wp core download --locale=en_US

#. Create the ``wp-config.php`` file, and configure the database connection, to correspond to the :ref:`MySQL configuration<wordpress-mysql-php>`. For example:
#. Create the ``wp-config.php`` file, and configure the database connection to correspond to the :ref:`MySQL configuration<wordpress-mysql-php>`. For example:

.. code-block:: bash

wp config create --dbname=DBNAME --dbuser=USERNAME --dbpass=PASSWORD

#. Set `WP_AUTO_UPDATE_CORE <https://developer.wordpress.org/advanced-administration/upgrade/upgrading/#constant-to-configure-core-updates>`__, to enable minor WordPress updates only.
#. Install WordPress, creating a user for yourself. For example:

.. code-block:: bash

wp config set WP_AUTO_UPDATE_CORE minor
wp core install --url=www.open-spending.eu --title="www.open-spending.eu" --admin_user=jmckinney --admin_password=PASSWORD --admin_email=jmckinney@open-contracting.org --skip-email

#. Set `DISABLE_WP_CRON <https://developer.wordpress.org/plugins/cron/hooking-wp-cron-into-the-system-task-scheduler/>`__, since system cron is used instead.
#. Uninstall default plugins:

.. code-block:: bash

wp config set --raw DISABLE_WP_CRON true
wp plugin uninstall hello

#. Install WordPress, with a ``siteadmin`` user associated to ``sysadmin@open-contracting.org``. For example:
#. Install `Two Factor <https://wordpress.org/plugins/two-factor/>`__ (2FA is required for administrators by the ``require-two-factor`` must-use plugin):

.. code-block:: bash

wp core install --url=www.open-spending.eu --title="www.open-spending.eu" --admin_user=siteadmin --admin_password=PASSWORD --admin_email=sysadmin@open-contracting.org --skip-email
wp plugin install two-factor --activate

.. tip::
#. Add or override any constants, setting values to strings of PHP source. (The constants under ``wordpress:constants`` in the ``pillar/cms.sls`` file are configured on every site.) For example:

To list the cron jobs, run:

.. code-block:: bash
.. code-block:: yaml

wp cron event list
wordpress:
sites:
USERNAME:
constants:
WP_AUTO_UPDATE_CORE: 'false'

#. Uninstall default plugins:

.. code-block:: bash
#. Add any `must-use plugins <https://developer.wordpress.org/advanced-administration/plugins/mu-plugins/>`__, with any context they need. (The must-use plugins under ``wordpress:mu_plugins`` in the ``pillar/cms.sls`` file are installed on every site.) For example:

wp plugin uninstall hello

#. Add a `must-use plugin <https://developer.wordpress.org/advanced-administration/plugins/mu-plugins/>`__ to auto-update plugins for non-major versions only. For example, replacing ``USERNAME``:
.. code-block:: yaml

.. note::
wordpress:
sites:
USERNAME:
mu_plugins:
- fathom-analytics
context:
FATHOM_ANALYTICS_ID: ABCDEFGH

`WP Rocket can't auto-update. <https://docs.wp-rocket.me/article/1446-why-are-wp-rocket-auto-updates-disabled>`__ See the `changelog <https://wp-rocket.me/changelog/>`__.
#. Unless the site uses a security plugin's file scanner, schedule checks:

.. code-block:: yaml

wordpress:
sites:
USERNAME:
plugins:
- auto-update-plugin
checks:
enabled: True
premium_plugins:
- advanced-custom-fields-pro

.. admonition:: Interpreting the emails it sends

- The integrity check lists core and plugin files that differ from wordpress.org's copy: modified, missing or added. A plugin that writes to its own directory can cause a false positive. Confirm changes before restoring files from a backup.
- The updates check lists the updates that won't install automatically, like major versions. It also lists the plugins whose update API gave no answer, which can be due to an HTTP timeout, an expired license, or the plugin being closed on wordpress.org.

#. :doc:`Deploy the server<../../deploy/deploy>`.
#. If you have a custom theme, download and activate it. For example:

.. code-block:: bash
Expand Down Expand Up @@ -185,8 +204,3 @@ Strings to replace might include:
- Domain names
- Theme names
- File paths

If the site uses these plugins, perform these operations to remove old items in the database:

- `Rank Math <https://rankmath.com>`__: *Status & Tools* menu item > *Database Tools* tab > Click the *Delete Internal Links* and *Clear 404 Log* buttons.
- `WordFence <https://www.wordfence.com>`__: *Scan* menu item -> Click the *START NEW SCAN* button. You can also manually delete rows from the ``wp_wfhits`` and ``wp_wflogins`` tables.
7 changes: 0 additions & 7 deletions docs/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,6 @@ Cybersecurity issues are monitored by `SecurityScorecard <https://platform.secur
#. Add a comment like: *Fathom Analytics, Google Analytics, and the Google Fonts API do not support SRI, by design: https://github.com/google/fonts/issues/473#issuecomment-331329601. As a compensating control, we subscribe to Fathom Analytics' and Google's incident alerts.*
#. Click the *Submit* button

WordFence
~~~~~~~~~

WordPress issues are monitored by `WordFence <https://www.wordfence.com/central>`__.

WordFence is managed in each WordPress installation, rather than by visiting its website.

.. _hosting:

Hosting
Expand Down
10 changes: 10 additions & 0 deletions pillar/cms.sls
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,13 @@ redis:

wordpress:
cli_version: 2.12.0
constants:
# https://developer.wordpress.org/plugins/cron/hooking-wp-cron-into-the-system-task-scheduler/
DISABLE_WP_CRON: 'true'
# https://developer.wordpress.org/advanced-administration/security/hardening/#disable-file-editing
DISALLOW_FILE_EDIT: 'true'
mu_plugins:
- disable-admin-view-transitions
- disable-comments-pingbacks
- mail-from
- require-two-factor
27 changes: 16 additions & 11 deletions pillar/coalition.sls
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,28 @@ phpfpm:
pm: dynamic
pm_max_children: 6
pm_max_requests: 500
cron:
contact:
- sysadmin@open-contracting.org
# Reproduce with: wp cron event run --quiet --all
# https://developer.wordpress.org/reference/classes/wp_site_health/detect_plugin_theme_auto_update_issues/
ignore:
- a-fake-plugin.php

wordpress:
sites:
coalition:
database: coalition_wp
plugins:
cron:
contact:
- sysadmin@open-contracting.org
ignore:
# Reproduce with: wp cron event run --quiet --all
# https://developer.wordpress.org/reference/classes/wp_site_health/detect_plugin_theme_auto_update_issues/
- a-fake-plugin.php
constants:
# https://developer.wordpress.org/advanced-administration/upgrade/upgrading/#constant-to-configure-core-updates
WP_AUTO_UPDATE_CORE: "'minor'"
mu_plugins:
- auto-update-plugin
- disable-admin-view-transitions
- disable-comments-pingbacks
- disable-user-enumeration
- fathom-analytics
- mail-from
checks:
enabled: True
premium_plugins:
- advanced-custom-fields-pro
context:
FATHOM_ANALYTICS_ID: LNRZMMVR
13 changes: 5 additions & 8 deletions pillar/corporate.sls
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,17 @@ phpfpm:
php_value[memory_limit]: 256M
php_value[upload_max_filesize]: 8M
php_value[post_max_size]: 10M
cron:
contact:
- sysadmin@open-contracting.org
- support+ocp@theideabureau.co

wordpress:
sites:
corporate:
database: corporate_wp
plugins:
- disable-admin-view-transitions
- disable-comments-pingbacks
cron:
contact:
- sysadmin@open-contracting.org
- support+ocp@theideabureau.co
mu_plugins:
- google-tag-manager
- mail-from
context:
GTM_CONTAINER_ID: GTM-WMV9C5F

Expand Down
32 changes: 32 additions & 0 deletions salt/apache/files/sites/wordpress.conf.include
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,38 @@ Header set X-Content-Type-Options nosniff
Require valid-user
</FilesMatch>

# Paths inside dot-directories (.git/HEAD), except ACME's.
<DirectoryMatch "/\.(?!well-known/)">
Require all denied
</DirectoryMatch>
# Apache's covers .ht* only.
<FilesMatch "^\.">
Require all denied
</FilesMatch>

<Files "wp-config.php">
Require all denied
</Files>

# Core's PHP entry points are all at the root (index.php, wp-login.php, wp-cron.php, ...).
<DirectoryMatch "^/home/{{ user }}/public_html/wp-includes/">
<FilesMatch "\.php$">
Require all denied
</FilesMatch>
# The one file under wp-includes that browsers request.
<Files "wp-tinymce.php">
Require all granted
</Files>
</DirectoryMatch>
<DirectoryMatch "^/home/{{ user }}/public_html/wp-admin/includes/">
Require all denied
</DirectoryMatch>
<DirectoryMatch "^/home/{{ user }}/public_html/wp-admin/">
<Files "install.php">
Require all denied
</Files>
</DirectoryMatch>

<FilesMatch "^xmlrpc\.php$">
Require all denied
</FilesMatch>
Expand Down
128 changes: 128 additions & 0 deletions salt/cms/files/check-updates.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php
/**
* Lists the updates that WordPress's automatic updater won't install, and the plugins it can't update.
*
* Run with `wp eval-file`.
*
* @package OpenContracting
* @link https://developer.wordpress.org/reference/classes/wp_automatic_updater/should_update/
*/

/**
* Requires the update API and refreshes the update transients.
*/
function opencontracting_updates_refresh() {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/update.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';

// Refresh the update transients, unless they were refreshed recently.
wp_version_check();
wp_update_plugins();
wp_update_themes();
}

/**
* Returns the updates that WP_Automatic_Updater won't install.
*
* @return array[] Rows of type, name, installed version and available version.
*/
function opencontracting_updates_check() {
$updater = new WP_Automatic_Updater();
$pending = array();

// Unlike `wp core check-update`, this follows WP_AUTO_UPDATE_CORE.
//
// wp_version_check() stores the offers from api.wordpress.org: the newest version, with response "upgrade", and the
// newest version of each branch down to the newest version of the site's branch, with response "autoupdate".
// get_core_updates() omits "autoupdate" offers. find_core_auto_update() returns the "autoupdate" offer with the
// highest version that WP_AUTO_UPDATE_CORE allows, which can be the same version as the "upgrade" offer. As such,
// the loop reports the "upgrade" offer, unless it will be auto-updated.
$auto_update = find_core_auto_update();
// Include an offer hidden with "Hide this update" in Dashboard > Updates, which get_core_updates() omits by default.
foreach ( (array) get_core_updates( array( 'dismissed' => true ) ) as $offer ) {
// Ignore "latest" and "development" offers.
if ( 'upgrade' !== $offer->response ) {
continue;
}
if ( $auto_update && $auto_update->current === $offer->current ) {
continue;
}
$pending[] = array( 'core', 'wordpress', get_bloginfo( 'version' ), $offer->current );
}

$plugins = get_plugins();
// Not `wp plugin list --update=available`, which also lists the updates that WP_Automatic_Updater is about to install.
foreach ( get_plugin_updates() as $file => $data ) {
if ( $updater->should_update( 'plugin', $data->update, WP_PLUGIN_DIR ) ) {
continue;
}
$pending[] = array( 'plugin', '.' === dirname( $file ) ? $file : dirname( $file ), $plugins[ $file ]['Version'], $data->update->new_version );
}

// Not `wp theme list --update=available`, which also lists the updates that WP_Automatic_Updater is about to install.
foreach ( get_theme_updates() as $stylesheet => $theme ) {
// The transient stores theme offers as arrays.
$item = (object) $theme->update;
if ( $updater->should_update( 'theme', $item, get_theme_root( $stylesheet ) ) ) {
continue;
}
$pending[] = array( 'theme', $stylesheet, $theme->get( 'Version' ), $item->new_version );
}

return $pending;
}

/**
* Returns the installed plugins that received no answer from their update API.
*
* WordPress lists every plugin it asked about in `checked`, and every answer in `response` or `no_update`. A plugin
* in neither was not answered for: the request timed out, its license is expired, or it's closed on wordpress.org.
* (Not `wp plugin list`, which shows such a plugin as up to date.)
*
* @return array[] Rows of name and installed version.
*/
function opencontracting_silent_plugins() {
$transient = get_site_transient( 'update_plugins' );

if ( empty( $transient->checked ) ) {
return array();
}

$silent = array_diff_key(
(array) $transient->checked,
(array) ( $transient->response ?? array() ),
(array) ( $transient->no_update ?? array() )
);

$rows = array();
foreach ( $silent as $file => $version ) {
$rows[] = array( '.' === dirname( $file ) ? $file : dirname( $file ), $version );
}

return $rows;
}

opencontracting_updates_refresh();

$opencontracting_pending = opencontracting_updates_check();
$opencontracting_silent = opencontracting_silent_plugins();

if ( $opencontracting_pending ) {
WP_CLI::line( "Updates that won't install automatically:" );
foreach ( $opencontracting_pending as $row ) {
WP_CLI::line( sprintf( '%s %s: %s -> %s', $row[0], $row[1], $row[2], $row[3] ) );
}
}

if ( $opencontracting_silent ) {
WP_CLI::line( "Plugins that can't update, because their update API gave no answer:" );
foreach ( $opencontracting_silent as $row ) {
WP_CLI::line( sprintf( 'plugin %s: %s', $row[0], $row[1] ) );
}
}

if ( $opencontracting_pending || $opencontracting_silent ) {
WP_CLI::halt( 1 );
}
Loading
Loading