Skip to content

Changelog ​

Unreleased ​

  • The chart switches STARTTLS on for SMTP credentials itself. With smtpProtocol: "" and smtpUser, smtpPassword or smtpExistingSecret set, the Deployment now carries SMTP_TLS=on. Since 7.1.4-6 that came from the sendmail wrapper of the base image, which has taken the default back (shinsenter/php, "restore 6.0 defaults"): on the current base the wrapper passes SMTP_TLS on and decides nothing itself. Nothing changes for a site: "" stays unencrypted without credentials and STARTTLS with them, and tls, ssl and none render what they did. The released versions up to 7.1.412 are built on a base that still had the default.
    • The chart looks at its own SMTP values only. Where SMTP_USER or SMTP_PASSWORD reach the container through extraEnvVars, extraEnvVarsCM or extraEnvVarsSecret, set smtpProtocol: tls or ssl next to them from this version on; the wrapper no longer switches TLS on for them.
    • An entry named SMTP_TLS in extraEnvVars takes precedence: the chart then renders none of its own, so the Deployment never has two variables of that name, which a server-side apply refuses. With smtpProtocol set, the chart used to render its own next to such an entry.
    • A release with credentials in the SMTP values that hands SMTP_TLS=off to the container through extraEnvVarsCM or extraEnvVarsSecret sets smtpProtocol: none before the upgrade. The chart cannot see into those sources, and its own SMTP_TLS=on wins over a variable from them.
    • Whoever runs the image without the chart sets SMTP_TLS=on next to SMTP_USER and SMTP_PASSWORD from this version on.
  • Image hardening: PHP keeps accepting only session ids it has issued itself (session.use_strict_mode). The base image no longer sets it, so the image sets PHP_SESSION_USE_STRICT_MODE=1.

Project and CI ​

  • The template test checks SMTP_TLS for an empty smtpProtocol: on with a user, a password or an existing Secret, absent without credentials. The smoke test no longer expects a TLS default from the wrapper; it checks that SMTP_TLS reaches msmtp next to credentials, and that a PHP request runs with session.use_strict_mode on.

7.1.412 ​

  • The PHP-FPM pool is free again between requests. Since 7.1.4-6 the sites answered slowly or not at all once a few requests per minute came in, with [pool www] server reached max_children setting (5) in the log, readiness probes that timed out and 502 responses: every PHP request left its worker bound to NGINX for a minute after the response, so five requests exhausted the pool of five on-demand workers. The base image had switched fastcgi_keep_conn on in its PHP location (shinsenter/php 6.1.0, which 7.1.4-6 was the first build on) and pairs it with an upstream keepalive cache in its own nginx.conf, which this image replaces. A PHP-FPM worker that serves a kept connection waits on it for the next request without any timeout; NGINX, with no cache, held the connection until its read timeout, and with the base image's cache every NGINX worker process would hold its own idle connections instead. The image sets the directive to off, as it was up to 7.1.4-5: a connection over the Unix socket costs microseconds, and a worker is free as soon as its response is out. The build fails if a later base image switches the directive on elsewhere or adds an upstream keepalive cache. No chart change; upgrading is recommended for every installation on 7.1.4-6 to 7.1.411.
    • The sites on 7.1.4-6 to 7.1.411 can be helped before the upgrade with a script in customPostInitScripts, which the image runs before NGINX starts: sed -i 's/fastcgi_keep_conn on;/fastcgi_keep_conn off;/' /etc/nginx/custom.d/01-common.conf.
    • PHP_PM_MAX_CHILDREN stays at 5. A bigger pool only moves the limit: with the directive on, each worker serves one request per minute, so ten workers carry ten requests per minute, which is a stopgap for a site and not a fix.

Project and CI ​

  • The smoke test checks that the NGINX configuration has fastcgi_keep_conn off and no upstream keepalive cache, and that no connection to PHP-FPM stays open after a request.

7.1.411 ​

  • Hardening: the image listens on port 80 only, and a container has no private key in its filesystem. TLS ends at the Ingress, and the pod serves the site over HTTP. The listeners on port 443 that the base image brings (TLS and HTTP/3), the placeholder key pair they needed and the TLS settings in the NGINX configuration are gone; the image build fails if a later base image brings a listener or a key back. The base image's own layer keeps its copy of the placeholder pair, which a scanner that reads layers can still report; "TLS ends at the Ingress" in the repository's SECURITY.md says what it is. Upgrading is recommended.
    • The chart renders no HTTPS port: not on the container, not on the Service, not in the NetworkPolicy. containerPorts.https, service.ports.https, service.httpsTargetPort and service.nodePorts.https are gone from values.yaml, and with them the container's variables NGINX_HTTPS_PORT_NUMBER and WORDPRESS_MULTISITE_EXTERNAL_HTTPS_PORT_NUMBER, which nothing read. A values file that still sets one of the four values renders as before, apart from the missing port, and the notes after an install or upgrade name the ones that are set.
    • The readiness and the startup probe ask over HTTP on the port http, whatever wordpressScheme is. Their defaults followed wordpressScheme and asked the port https for a site with wordpressScheme: https. A probe of the release's own that still names the port https or 443, or asks the HTTP port with the scheme HTTPS, fails the render with a message that says what to change, instead of leaving the pods of the upgrade unready.
    • Responses no longer carry the Alt-Svc header that announced HTTP/3 on port 443.
    • The log of a starting container has one new line from the base image, which says that it disables ssl_dhparam. It needs no attention: the image has no TLS settings left.
    • "Upgrading" in the chart's README says what an installation that used the Service's port https has to change.
  • "Rebuild index" in wp-admin works right after the installation. With idx.enabled, the search sidecar always has a key for POST /api/reindex, and WordPress' container hands the same key to the search plugin at every start. Until now the endpoint was switched off unless idx.reindexApiKey or idx.existingReindexApiKeySecret was set, and the button reported that no key was configured until an administrator had entered the same key under Settings > IDX Search.
    • Without a setting the chart generates the key, at the installation or at the upgrade to this version, and keeps it across upgrades in the Secret <fullname>-idx under idx-reindex-api-key. An installation that left the endpoint switched off has it switched on from this version, behind a key of 32 random characters. idx.reindexApiKey and idx.existingReindexApiKeySecret work as before and take the place of the generated key.
    • The plugin keeps the key as the option idx_search_api_key in the site's database, as it does with one entered in wp-admin. A key entered there is replaced by the chart's at the next start.
    • A new idx.reindexApiKey replaces the pod, so that the plugin and the sidecar change over together; until now the sidecar kept the old key until the pod was replaced for another reason. After a change to a Secret named in idx.existingReindexApiKeySecret, replace the pod yourself.
    • A tool that renders the chart without access to the cluster (helm template, Argo CD) would generate a new key with every render: set the key or name a Secret there.
  • The search sidecar is wordpress-idx 1.3.9 (1.3.8 before). Its image keeps the chart version as its tag.
    • The first start rebuilds the index once. The index file has a new name and form, wp-index.bin, so the sidecar builds its index anew from the database at its first start with this version and needs the database for that start. The pod is not ready before the index is there. It then removes the file of the earlier version. Nothing has to be done by hand.
    • The build needs room on the volume. The index is on the WordPress volume, under idx/. While a build runs it needs room there for three to four times the index file, and at the upgrade the file of the earlier version is still there until the new index is written. On a site of 15,600 posts and pages that is a new index file of 143 MB next to an earlier one of 415 MB.
    • The index of a large site is built and served with a fraction of the memory. On a site of 15,600 posts and pages the build needed 4.7 GiB and failed with Node's default heap, and the sidecar held 1.1 GiB at rest. With this version the build and a rebuild of that site peak at 0.46 GiB under the chart's default limit for the sidecar (768 MiB), and the sidecar rests at 0.24 GiB. No NODE_OPTIONS is needed.
    • The sidecar answers while it builds the index. The build runs in a process of its own, so the liveness check keeps answering, and during a rebuild the search answers from the index that is there. A build that took longer than the liveness probe allows had the container restarted in the middle of it. At a first build there is no index to answer from yet, and the sidecar is not ready until it is written.
    • A build that fails leaves the index in place.
    • What the search returns has not changed. Posts published in the same second, and comments written in the same second, now come in a fixed order, by their ID.
  • A script in customPostInitScripts under the name of a script the chart renders itself with the same values (00-preinstall-plugins.sh, 05-idx-plugin.sh, 06-idx-reindex-key.sh, 10-configure-w3-total-cache.sh) fails the render with a message that names it. Kubernetes refused such a Deployment before, with an error about a mount path.

Project and CI ​

  • The smoke test checks that the container listens on port 80 and the base image's health port only, that the NGINX configuration has no TLS directive and that the container's filesystem has no private key under /etc/ssl and /etc/nginx.
  • The template test checks that no rendered object names an HTTPS port, that a values file with the removed values renders the same objects as one without them, what the notes say about them, and which probes the chart refuses.
  • The template test checks that the sidecar and WordPress' container read the key from the same Secret, that the chart generates one when none is set, and that its value is in no other object.
  • The run of the "Try it locally" page signs in and uses "Rebuild index" in wp-admin, against a chart that hands the key over.

7.1.410 ​

  • The bundled MariaDB moves to 12.3.3, the current long-term series; the 12.2 image is no longer updated. An installation with mariadb.enabled starts the new series on its existing data directory at the upgrade. Back the database up first: MariaDB does not support going back to the earlier series.
    • The image updates its system tables itself. The chart sets MARIADB_AUTO_UPGRADE, so the first start on data of an earlier series runs mariadb-upgrade before the server takes connections, and keeps a copy of the old system tables in the data directory. Until now a change of series left the server on the old system tables until someone ran mariadb-upgrade by hand.
    • An installation with an external database (mariadb.enabled: false) is not affected. One that sets mariadb.image itself keeps its image and gets the setting: its database pod is replaced once at the upgrade, and a server that still runs on the system tables of an earlier series updates them at that start.
  • The search sidecar is wordpress-idx 1.3.8 (1.3.7 before). Its image keeps the chart version as its tag.
    • The first start rebuilds the index once. The index file on the volume has a new format, so the sidecar builds its index anew from the database at its first start with this version and needs the database for that start; the search is ready a little later than usual. No manual "Rebuild index" is needed.
    • A query returns every match, where it returned at most 100 entries per searched field, and it matches titles as well as content and comments.
    • Published posts and pages whose author is not a WordPress user are in the index, with an empty author.
    • Hardening: the index holds only posts and pages that are public without a password.
    • The sidecar tries its index build again every 5 seconds until it succeeds. A database that answered late, or a WordPress that had not created its tables yet, left it without an index until it was restarted.
    • The plugin's search form follows idx.basePath; it called the sidecar under /idx whatever the path was.

Project and CI ​

  • The template test checks MARIADB_AUTO_UPGRADE on the rendered Deployment.

7.1.409 ​

  • A new installation can store uploads. The image installed WordPress as root, and the installer left wp-content/uploads with root as its owner, so the first upload in wp-admin failed with "The uploaded file could not be moved". The installer now runs as the web user. An installation made by an earlier version is repaired on its next start: wp-content/uploads and the year and month directories below it are handed to the web user where root owns them. Files, other directories and symbolic links are left alone. Sites that brought their uploads along were not affected.
  • The first start waits for the database. WordPress installs itself when its container first starts, once; a database that was not up at that moment left a running pod with a site that was never installed, until the pod was restarted. The image's start hook now tells "not installed" from "no database" (a server that does not answer, or one that does not have the database yet) and asks again until the database answers, for up to WORDPRESS_DB_WAIT_TIMEOUT seconds (300; 0 does not wait; set it through extraEnvVars). It asks WordPress's own configuration, so the wait holds for the bundled MariaDB, an external database, a wp-config.php of the user's own and a database proxy in a sidecar. No chart template and no value changes.
    • An installed site loses nothing by waiting: a pod that cannot reach its database is not ready either way, its readiness probe needs the database. With the default liveness probe the kubelet starts a waiting container again before the limit, so a database that stays away shows in the pod's restarts and in its log.
    • Past the limit the start goes on as before, and the log says so; a new installation is then made at the container's next start.
  • Releases are signed. A release builds both images for each platform, pushes them and the chart to the registry under their digests, signs all three with Sigstore cosign (keyless), and sets the version tags and latest only once the signatures are in place. Every platform of an image carries an SPDX SBOM and SLSA provenance. "Verify a release" in the repository's README has the commands.
  • The search sidecar's image carries the chart version as its tag. ghcr.io/ioanalytica/wordpress-idx is now built by this project's release, from the wordpress-idx source pinned as a git submodule (wordpress-idx 1.3.7 at this point), signed with it, and tagged like the chart. With idx.enabled the chart runs wordpress-idx:<chart version>.
    • An installation that takes the image from its default repository needs nothing: the upgrade pulls the new tag.
    • An installation that points idx.image.registry or idx.image.repository at a mirror has to have the image there under the chart version, where the chart asked for a 1.x.y tag before. idx.image.digest still wins over the tag.
    • The image's labels org.opencontainers.image.version and org.opencontainers.image.revision name the wordpress-idx version and commit. The tags 1.x.y up to 1.3.7 stay as they are; a later wordpress-idx version gets its 1.x.y tag from the first release that builds it.
  • The chart carries its metadata for Artifact Hub. artifacthub.io/* annotations replace category, licenses, images, imageTag and idxImageTag in Chart.yaml; the templates take the image tags from the chart version itself. The images list names every image the chart can run, and home is the documentation site.
  • containerSecurityContext.enabled: true gives a pod that starts. The image has to start as root, with a writable root filesystem and the container runtime's default capabilities, and the init container sets the owner of the files it creates. The defaults of containerSecurityContext were those of the Bitnami chart (user 1001, runAsNonRoot, a read-only root filesystem, all capabilities dropped), with which neither container starts. They are now runAsUser: 0, runAsGroup: 0, runAsNonRoot: false, readOnlyRootFilesystem: false and capabilities.drop: [], so that enabling the context adds what the image can run with: allowPrivilegeEscalation: false, privileged: false and the RuntimeDefault seccomp profile.
    • The context stays off by default. A release that enables it and sets these five values itself renders as before.
    • Tried on a cluster with the context enabled alone and together with podSecurityContext.enabled; the template test pins the defaults.
  • wordpressExtraConfigContent takes effect. The value was passed to the container under a name nothing reads. It now reaches the generated wp-config.php as WORDPRESS_CONFIG_EXTRA, which the file evaluates before it loads WordPress: PHP statements, without an opening tag.
    • A release that has set the value without effect so far gets it evaluated with this version. Check the content first: a statement PHP cannot parse stops every request.
    • An extraEnvVars entry named WORDPRESS_CONFIG_EXTRA keeps precedence; the value is then not rendered. A variable of that name that comes from extraEnvVarsCM or extraEnvVarsSecret is overridden by a non-empty wordpressExtraConfigContent, since the chart cannot see into those; keep the content in one place.
  • metrics.serviceMonitor.metricRelabelings and metrics.serviceMonitor.selector take effect. The ServiceMonitor template read a key of another spelling for the first, so the documented value did nothing; a release that set that other spelling to get its rules applied keeps them. selector was not read at all; its labels are now labels of the ServiceMonitor, next to labels, which wins where both name a key.
  • The NetworkPolicy admits the metrics port and extra container ports. With metrics.enabled, the exporter's port (metrics.containerPorts.metrics, 9113 by default) is listed with the pod's other ports. Until now a scrape was refused wherever network policies are enforced, unless a rule in networkPolicy.extraIngress admitted it; such a rule is no longer needed. The port is admitted the way the HTTP port is: from every source with networkPolicy.allowExternal: true, the default. A release whose extraIngress rule admitted the scrape from selected sources only has the port open to every source with this version; networkPolicy.allowExternal: false narrows all of the pod's ports to labelled clients and the namespaces of networkPolicy.ingressNSMatchLabels. For extraContainerPorts the policy now lists each entry's containerPort, with its protocol where one is set; it rendered the whole entry, which the API server refuses, so such a release could not be installed with the policy on.
  • Removed: customHTAccessCM and metrics.prometheusRule. No template read either. NGINX reads no .htaccess; alert rules for the exporter can be deployed through extraDeploy. A values file that still sets them renders as before, since they had no effect.
  • Values without effect say so. wordpressFirstName, wordpressLastName, wordpressPlugins, allowEmptyPassword, overrideDatabaseSettings, multisite.*, image.debug, ingress.apiVersion, secondaryIngress.apiVersion and secondaryIngress.secrets have no effect: the chart passes some of them to the container as environment variables the image does not read, and no template reads the others. They stay so that values files written for the Bitnami chart carry over, and values.yaml now names each as such and what to use instead. nginxConfiguration and existingNginxConfigurationConfigMap do not reach NGINX: the chart renders the first into a ConfigMap, the second names one that has to exist for the pod to start, and either becomes a pod volume that no container mounts. wordpressSkipInstall does not skip the installation: the image installs WordPress where the database holds none and leaves an existing site alone. The key stays, because wordpressConfiguration requires it; the environment variable the chart set for it, which nothing read, is gone.
  • The values schema covers every value. values.schema.json described 18 of the chart's top-level keys. It now lists every value of values.yaml with its type, description and default, which is what Artifact Hub shows as the chart's values reference.
    • Helm checks every install, upgrade and helm template against the schema, so it is permissive on purpose: no key is required, a key it does not list is left to the chart, text may come as a number and a number as text (as --set and --set-string deliver them), a switch as 0/1 or text, a list as a map of entries or as text, and a map as an empty list or, where the chart renders it as a template, as text. Text and numbers the earlier schema limited to one type, such as externalDatabase.port or wordpressPassword, accept these forms as well. The six switches it listed (mariadb.enabled, metrics.enabled, and enabled and tls of both Ingresses) stay limited to true and false.
    • It refuses what the chart cannot use: a list or map where the chart reads a single value, a single number or switch where it reads a list or map, a fraction where it reads a whole number, a mode other than the listed ones, and, as before, a malformed entry of nginx.deniedPaths or wordpressPreinstallPlugins. Such a value used to render into a manifest that was wrong in some other way.
    • chart/tests/schema-test.sh fails when a documented parameter is missing from the schema, a schema entry is not in values.yaml, or a default differs, and it checks the forms above. A changed image pin needs its default in the schema as well: the Dragonfly tracker sets it with its bump, and Renovate's pull requests for values.yaml say to run scripts/schema-default.sh.
  • The notes Helm prints after an install with ingress.enabled give the site's URL and the command that shows the Ingress's address. They told the reader to run minikube ip and to add the hostname to /etc/hosts.
  • The init container no longer prints "Checking environment for required settings". It has no environment to check, and the block printed a check mark for every name whatever was set.
  • image.pullSecrets is a key of values.yaml again, with the default [] its documentation gave; it was commented out there. The exporter's probes (metrics.livenessProbe, metrics.readinessProbe, metrics.startupProbe and their custom* counterparts), metrics.containerSecurityContext.seLinuxOptions, externalDatabase.databaseCharset and externalDatabase.databaseCollation are documented now. No change in behaviour.
  • Documentation: the nginx.deniedPaths example uses neutral paths; the "Metrics & Monitoring" section of the README shows the exporter the chart ships; smtpPort says that msmtp's default port is 465 with smtpProtocol: ssl; wp-config.php is described as written once per pod, by the init container, not on every container start; and the README no longer lists OpenShift compatibility, which an image that starts as root does not have without further permissions.
  • Image: built on the current base image and Alpine 3.24 packages, as with every release. Still PHP 8.4.27, the current 8.4 release.

Project and CI ​

  • The k3d run of "Try it locally" uploads a file through wp-admin and installs a second release whose database is away at the first start; the smoke test checks the wait and its limit, for a database that refuses the connection and for one that takes it and says nothing, and the hand-over of upload directories that belong to root.
  • A release candidate x.y.P-rc.N can be tagged on the commit of its release: it runs the whole release, publishes chart and images at the candidate's version, and leaves latest where it is. A version that is in the registry already is refused before anything is built.
  • The GitHub Release takes its notes from this changelog's entry for the version.
  • "Try it locally" builds the sidecar's image from the submodule when the registry does not have it yet, as it does for the WordPress image, and runs after every release against what the release published. On a pull request it builds an image whose source has changed since the release the chart names, also when an earlier pull request made the change, so the page is always tested with the images the next release would publish.
  • Renovate keeps the entries of the third-party images in the chart's images list in step with their pins; the wordpress-idx submodule moves by hand.
  • Changes between releases are collected under ## Unreleased, the first section of this file. A pull request that changes the chart or the image adds its lines there and leaves the version alone; the release pull request sets the version and turns the section into the version's entry (scripts/changelog-release.sh). The pull request of the WordPress release tracker does the same, and the template test fails when the version of Chart.yaml is not the newest one in this file.

7.1.408 ​

  • Chart versions are stable SemVer versions now. The build number moved from a -N suffix into the patch component: for WordPress x.y.z and build N the version is x.y.P with P = z * 100 + N. 7.1.408 follows 7.1.4-7, and the first version for WordPress 7.1.5 will be 7.1.500. SemVer reads a -N suffix as a pre-release, so Helm needed --devel, and a version range matched a release only if it named a pre-release itself. appVersion remains the plain WordPress version, and the chart version remains the image tag. "Chart versions" in the README has the scheme with examples, "To 7.1.408" the upgrade notes.
    • Earlier versions and their image tags stay published unchanged, and every version of the new scheme sorts above them. A deployment that pins an exact version changes the pin to the new form with its next upgrade.
    • --devel is no longer needed; the install commands in both READMEs dropped it.
  • No changes to templates, values or the Dockerfile. As with every release, the image is built on the base image and Alpine packages current at release time.
  • scripts/chart-version.sh holds the scheme, and the template test fails on a version outside it: a suffix, a build number past 99, or a patch component that does not belong to appVersion. The WordPress release tracker proposes x.y.(z * 100) for a new WordPress release.

7.1.4-7 ​

  • Image hardening: rebuilt on current Alpine 3.24 packages, picking up the latest security update for perl (5.42.3, pulled in by the base image's build tooling, not used by WordPress) and apk-tools 3.0.9. Still PHP 8.4.27, the current 8.4 release. No chart or configuration changes; upgrading is recommended as routine hardening.

7.1.4-6 ​

  • SMTP credentials are no longer sent unencrypted by default. The base image's sendmail wrapper now switches TLS (STARTTLS) on as soon as a user or password is set and SMTP_TLS is not. With smtpProtocol: "" the chart sets no SMTP_TLS, so a site that sets smtpUser or a password now negotiates STARTTLS where it used to send in the clear. Without credentials, "" stays unencrypted. Sites with smtpProtocol: tls or ssl are unaffected.
    • New smtpProtocol: none keeps a connection unencrypted even with credentials (SMTP_TLS=off), for internal relays that authenticate but offer no TLS. Set it only where the relay cannot do STARTTLS; otherwise sending fails once this version is deployed.
    • The template test checks the SMTP_TLS value per protocol; the smoke test pins the wrapper's TLS default with and without credentials.
  • Image hardening: rebuilt on the current base image and Alpine 3.24 packages (ImageMagick 7.1.2.35, libexpat 2.10.0). Still PHP 8.4.27, the current 8.4 release. The base reshaped its sendmail wrapper; the SMTP_STARTTLS option the image adds for smtpProtocol: ssl is now inserted before the wrapper's final exec line, so implicit TLS keeps working.
  • The install commands in both READMEs work again: install straight from the OCI reference (Helm cannot repo add an OCI registry) with --devel or an explicit --version, since chart versions are SemVer pre-releases. A new "Chart versions" section explains the scheme.
  • The repository moved to a single main branch; the chart icon URL now points at it.

7.1.4-5 ​

  • Every release now gets its own W3 Total Cache key space. W3 Total Cache starts every cache key with common.instance_id, an id it draws once at activation and stores in wp-content/w3tc-config/master.php. A site set up as a copy of another one carries the original's id along, and on a shared cache server the two then share keys: WP-CLI calls without --url, loopback and pod-IP requests, and every flush, whose version counters carry no host. With wordpressConfigureCache the configuration script now sets the id on every start, before its first flush, derived from <namespace>/<release name> (the first 28 bits of its SHA-256, plus one).
    • The first start on this version switches each site to its new id once: the caches start cold, and entries under the old id expire on their own.
    • New wordpressCacheInstanceId sets the id explicitly, for example to keep an id across a release rename. A value that is not an integer from 1 to 2147483647 fails the render.
  • The template test checks that the id is stable for a release, differs between releases and namespaces, is set before the first flush, and that invalid overrides fail the render.

7.1.4-4 ​

  • File-integrity monitoring no longer needs baseline maintenance after plugin updates. Plugins update themselves automatically, and W3 Total Cache rewrites files the watcher checks. Those files are now judged by content, never by a digest that every update would break:
    • Drop-ins are checked against the copy their plugin ships. advanced-cache.php, db.php and object-cache.php in wp-content/ must equal W3 Total Cache's own copies under plugins/w3-total-cache/wp-content/. A drop-in that differs, or whose plugin is gone, is a dropin-modified finding with its digest. This replaces the name-only allowance that came with wordpressConfigureCache and applies on every site, so a replaced drop-in no longer passes unnoticed. fim.dropins points a drop-in at another plugin's copy or removes it from the check.
    • nginx.conf is checked for its directives. The file is included into the server block, and W3 Total Cache writes its rules there. A directive that hands requests to a backend, maps other files, loads further configuration or writes a log file (*_pass, fastcgi_*, proxy_*, alias, root, include, access_log with a path, Lua/Perl/njs, ...) is an nginx-conf-directive finding naming the directives; a file nginx could not parse is one too. W3 Total Cache's rules and hand-written rules such as a deny pass. A digest baseline for nginx.conf remains possible where no plugin writes into it and replaces the directive check there.
    • A placeholder index.php is accepted wherever it is one: empty, or an opening tag with a single comment such as "Silence is golden". Plugins drop these into their upload directories on updates and exports. Elsewhere index.php was accepted by name alone; it is now held to the same content check, so code in wp-content/index.php or plugins/index.php is a finding.
  • fim.php --scan-once runs one scan in the foreground and prints its metrics, for a check by hand: kubectl exec deploy/<release>-wordpress-nginx-fim -- /usr/local/bin/php /fim/fim.php --scan-once.
  • New chart/tests/fim-test.sh runs the watcher against real volume trees - clean W3 Total Cache site, replaced and orphaned drop-ins, nginx.conf directives, placeholders, and every earlier rule. CI now runs the chart tests on chart changes too; until now only changes under docker/ triggered them.
  • Validated before release against four live sites with the new rules: no new findings on any of them.

7.1.4-3 ​

  • The init container now creates the site's nginx.conf on the volume only when it is missing. A misspelled file name in the check made it run on every start, touching and re-owning an existing nginx.conf and logging "Creating site nginx.conf" each time. Existing files are unaffected: on the sites checked before the release, every nginx.conf was already owned by www-data with mode 644, and the content was never changed.
  • The template test now checks that every create-if-missing guard in the init script tests the path its block creates.

7.1.4-2 ​

  • New: file-integrity monitoring as a chart feature (fim.enabled, off by default). A watcher mounts the site's volume read-only, polls it every fim.interval seconds and exports its findings as Prometheus metrics. The metric names and rule names match the per-site watchers that ran beside the chart until now, so existing alert rules keep working; the Service carries app: <release>-fim, which an optional ServiceMonitor copies onto the samples.
    • Structural rules instead of file-name lists: a mu-plugins directory, executable files directly in wp-content/, loose files directly in plugins/ or themes/, executable files in uploads/, .user.ini files, encoded payload carriers such as .b64, unreadable directories, and changes to baselined files. Site-specific file-name markers (fim.nameMarkers), allowances and extra writable directories are values.
    • wp-config.php is baselined by the chart itself. Its content now comes from one named template (wordpress.wpConfigPhp), and the watcher's digest is computed from it at render time, so a chart upgrade no longer needs a manual re-baseline. The file the init container writes is byte-identical to 7.1.4-1.
    • Web-root mounts are found automatically: every extraVolumeMounts entry that mounts a subPath of the chart's volume below /var/www/html is watched as a static path.
    • Re-baselining from the alert: a baseline-modified finding carries the digest it found in a sha256 label. The configuration is re-read on every scan, so a baseline change applies without a restart.
    • Own pod by default (fim.mode: deployment), sidecar as an option. The own pod shares neither network namespace nor lifecycle with WordPress: it keeps reporting while WordPress is scaled down, and it has a NetworkPolicy that allows no egress at all. With a volume that is not ReadWriteMany it is scheduled next to WordPress. mode: sidecar covers ReadWriteOncePod and setups without persistence, and has no readiness probe, so it can never take WordPress out of its Service.
    • The watcher runs PHP from the chart's own image; there is no new image to pin or track. Every scan runs in a forked child, so /metrics stays responsive during long scans, and a hung scan surfaces as a stale wp_fim_last_scan_timestamp_seconds.
    • Optional fim.prometheusRule with WpFimViolation, WpFimWatcherStalled and WpFimWatcherAbsent, scoped to the release.
  • A baseline digest that YAML has read as a number, or one that is not 64 hex characters, fails the render instead of being silently compared.
  • The template test checks that the pinned wp-config.php digest matches the bytes the init container writes (the rendered heredoc is replayed), that no Service selecting WordPress also selects the watcher, and that a sidecar watcher has no readiness probe.

7.1.4-1 ​

7.1.3-8 ​

  • The .htaccess report now names what is actually open, and stops proposing denies that would break a site. A deny-class file is narrowed by what it denies:
    • File extensions (<Files *.php> in uploads/; Wordfence's <FilesMatch "\.php$"> in lib/, views/, waf/ and siblings): judged per extension. php is covered with phpExecutionAllowlist.mode: enforce, because NGINX already answers 403 for that PHP - unless extraAllowedPaths lets PHP in that directory run - a file that exists, which the report names, or one added later - or holds an entry the init container cannot read as NGINX does, in which case the allowlist is not credited at all. Every other extension (phtml, phps, phar, ...) NGINX serves as a plain file, so the proposal is a regex closing exactly those extensions, never the directory. The previous report proposed denying /wp-content/uploads for an uploads/.htaccess of this kind, which would have blocked every image on the site.
    • Named files (<Files debug.log>): one deniedPaths regex per name instead of the directory, covering the subdirectories <Files> governs too. A <FilesMatch> is a regex even when it looks like a file name, and keeps the directory proposal.
    • Dotfiles (<Files .ht*>, <FilesMatch "^\.">): covered, since NGINX denies dotfiles everywhere.
    • Anything else keeps the whole-directory proposal, and a plugin-managed directory stays plugin-managed whatever its .htaccess denies.
  • Regex deniedPaths entries now count as coverage. The report tests paths against every entry in the form NGINX gets it - plain paths included - so a directory closed by a regex such as ~* ^/wp-content/[^/]*backup[^/]*(/|$) is no longer reported as open. A directory, extension or name counts as closed only when a new file - at the top and in a subdirectory - and every file it holds now are denied, so an entry matching only the directory URL, only the top level or only some names does not count. Only entries that read the same in busybox ERE and NGINX PCRE count; any other entry covers nothing in the report, so its directory still shows as uncovered rather than being assumed closed.
  • htaccessPolicy: fail blocks the start for any uncovered deny of the kinds above, and for nothing that is already covered.
  • Validated against live sites before release: every entry the new report no longer lists was confirmed closed.
  • docker/tests/htaccess-test.sh now renders the chart per case with real values instead of patching the script, and pins every case above, including a regression for an extraAllowedPaths lookup that must never stop the init container.

7.1.3-7 ​

  • With idx.enabled, the wordpress-idx search plugin now follows the sidecar on every container start. A new application-hook script (zz-05-idx-plugin.sh) fetches the plugin package from the sidecar over loopback - never from wordpress.org, so plugin and sidecar stay one release - and installs and activates the plugin when it is missing, or updates it in place when the sidecar ships a newer build. Until now the plugin had to be installed by hand, and an update only arrived through WordPress' own update check, which could lag a sidecar upgrade by hours. Rolling out a new idxImageTag now brings the matching plugin with the pod restart.
  • Updates on start honour idx.pluginAutoUpdate: with false, an installed plugin is left as it is and updates stay a choice in wp-admin; a missing plugin is still installed. A plugin deactivated in wp-admin stays deactivated - only a fresh install is activated. A plugin deleted in wp-admin comes back on the next start; disable idx to remove it for good.
  • The script waits up to 60 seconds for the sidecar (it may still be pulling its image) and logs every failure as [idx-plugin] without failing the boot. Verified end to end against a real WordPress and the 1.3.7 sidecar: fresh install and activation, in-place update from 1.3.5 with the activation kept, and no action when current.
  • The post-init ConfigMap is now also rendered when idx.enabled is the only feature using it; the template test pins that, the mount, the loopback source and both pluginAutoUpdate modes.

7.1.3-6 ​

  • Bump idxImageTag to 1.3.7 (wordpress-idx sidecar). The bundled WordPress plugin now asks the sidecar on every update check instead of reading a manifest cached for up to six hours. Before, after a sidecar upgrade the plugin kept reporting itself as current until that cache expired, although the new sidecar already offered a newer build; only "Check again" on the Updates screen got past it. The fix takes effect once a site has installed plugin 1.3.7: that update itself may still need "Check again", every later one shows up with the next regular check. No change to the environment variables the sidecar reads or the endpoints it serves.
  • The wordpress-nginx image has no changes of its own; the release build refreshes its Alpine packages as usual.

7.1.3-5 ​

  • Image hardening: the image is rebuilt on current Alpine 3.24 packages, picking up the latest security updates for curl, libraw and nginx (nginx 1.30.6). Still PHP 8.4.27, the current 8.4 release. No chart or configuration changes; upgrading is recommended as routine hardening.
  • Bump idxImageTag to 1.3.6 (wordpress-idx sidecar). Hardening release: the sidecar is rebuilt on Node 26.12.1 (a security release) and current Alpine 3.24 packages, and its HTTP request-body parser is updated (body-parser 2.4.0). Its dependencies move on as well (dotenv 18.0.9, mysql2 3.24.6). No change to the environment variables it reads or the endpoints it serves.

7.1.3-4 ​

  • Security hardening: the image is rebuilt on PHP 8.4.27 (was 8.4.26), a security release from the PHP project. Upgrading is strongly recommended; see the PHP 8.4.27 release announcement.
  • Refreshed Alpine packages, including ImageMagick 7.1.2.34 and current CA certificates.
  • Dragonfly is bumped to v2.0.2 (was v2.0.0) for the internal cache (memcached.enabled: true). Patch release; no change to the flags the chart passes.

7.1.3-3 ​

  • Image hardening: the image is rebuilt on current Alpine 3.24 packages, picking up the latest security updates for expat, libX11 and libXrender. The release build now refreshes the system packages on every release instead of reusing a cached package layer, so each image ships the package set current at its release. No chart or configuration changes.

7.1.3-2 ​

  • Bump idxImageTag to 1.3.5 (wordpress-idx sidecar). Maintenance only: the sidecar's own dependencies move on (dotenv 18, Express 5.3, mysql2 3.24.5), with no change to the environment variables it reads or the endpoints it serves.

7.1.3-1 ​

7.1.2-1 ​

7.1.1-1 ​

7.1.0-17 ​

  • Dragonfly is bumped to v2.0.0 (was v1.40.1) for the internal cache (memcached.enabled: true). The major version marks upstream's maturity milestone rather than a feature break: it adds Valkey 9 RDB loading and GEOSEARCHSTORE, reduces connection-accounting and buffering overhead, and carries a long list of correctness fixes for streams, RESP3, tiered storage and cluster migration. Nothing the chart depends on changed — the deprecations in this release concern --huffman_table and the fixed-length replication backlog, neither of which the chart passes. Verified before the bump, against the actual image: --requirepass and --memcached_port are both still accepted, and the container comes up listening on 0.0.0.0:6379 and 0.0.0.0:11211 with exactly the flags the chart passes. Password authentication for the cache is unaffected.
  • The memcached.image example in the chart README still showed dragonflydb/dragonfly:v1.26.0 — the unprefixed reference that resolves to Docker Hub, where the project does not publish, and which ImagePullBackOffs. It now matches the shipped default.
  • Fix: the weekly Dragonfly tracker could never propose a bump. Its flag check piped dragonfly --help into grep under set -o pipefail; gflags' help exits 1 by design, and that status wins over the matching grep, so every flag was reported as missing and the workflow failed on the first one (--requirepass) instead of opening a PR. The help output is now captured once and matched afterwards, which also drops one container start per flag.

7.1.0-16 ​

  • The generated wp-config.php now defines WORDPRESS_IDX_BASE whenever idx.enabled is set, pointing the wordpress-idx plugin at the sidecar over loopback (http://127.0.0.1:<idx.port><idx.basePath>). The plugin makes two server-side calls — its own update check and the reindex action — and without the constant both resolve through site_url() to the site's public URL, so they only work if the pod can reach the site from the inside. Loopback is the shortest path to a sidecar sharing the pod, needs no DNS and stays available under restrictive network policies, which makes it the better default regardless. Where the public URL is not reachable from the pod, the difference is not cosmetic: the calls fail silently and the symptom misleads, because the search keeps working — it runs as fetch() in the visitor's browser and never touches this path — while the plugin simply stops offering updates. The chart is the only place that knows the sidecar shares the pod and on which port and base path it listens. The define sits after the WORDPRESS_CONFIG_EXTRA eval and is guarded by ! defined(), so an operator can still override it there for setups where the sidecar does not share the pod.
  • Note for file-integrity monitoring: the content of the generated wp-config.php changes with this release (first pod start on 7.1.0-16). Refresh content-hash baselines accordingly; the file remains identical across sites with identical values, so the new hash can be computed from the rendered template ahead of the rollout.
  • Bump idxImageTag to 1.3.4 (wordpress-idx sidecar). Besides the constant above, that build routes the plugin's reindex call through the same base URL — it called site_url() directly and so ignored WORDPRESS_IDX_BASE entirely — and lets a forced update check bypass the plugin's own manifest cache, which otherwise pinned a stale or failed lookup for up to six hours.

7.1.0-15 ​

  • Bump idxImageTag to 1.3.3 (wordpress-idx sidecar).

7.1.0-14 ​

  • Maintenance rebuild for package freshness: the image now enables Alpine's community repository and runs a full apk update && apk upgrade directly after FROM, so every build starts from current packages instead of the base image's snapshot. No chart behavior changes; upgrade is recommended as routine hardening.

7.1.0-13 ​

  • The SMTP values now configure mail delivery completely - and only mail delivery. PHP mail() and therefore wp_mail() relay through the image's msmtp sendmail wrapper, configured entirely from chart values via environment variables: no mail plugin, nothing written to the database. smtpHost, smtpPort, smtpUser and smtpPassword/smtpExistingSecret worked before; the remaining values rendered environment variables that nothing consumed (SMTP_PROTOCOL, WORDPRESS_SMTP_FROM_EMAIL, WORDPRESS_SMTP_FROM_NAME - inherited names, read by neither the wrapper nor the image) and are now mapped onto the names the wrapper actually reads. Verified end to end: STARTTLS handshake and delivery through the wrapper against a TLS-enabled mail sink, implicit TLS against a public submission endpoint.
  • smtpProtocol has real semantics now: tls = STARTTLS (usually port 587), ssl = implicit TLS (smtps, port 465), empty = unencrypted (internal relays only). Any other value fails the render instead of silently sending unencrypted. Sites that had smtpProtocol set gain encryption on the first rollout of this release - until now the value did nothing and msmtp spoke plaintext. If your relay's certificate does not validate against the system CA bundle, sending fails loudly from this release on; fix the certificate rather than the setting.
  • Implicit TLS needed image support: the sendmail wrapper only exposed msmtp's --tls switch, which upgrades via STARTTLS. The wrapper now understands SMTP_STARTTLS (the chart sets it to off for smtpProtocol: ssl), passed to msmtp as --tls-starttls. A build-time guard fails the image build if the base image rewrites the wrapper and the patch no longer lands.
  • smtpFromEmail is the sender, envelope AND header. It becomes msmtp's envelope sender (--from), and the message's From header is rewritten to match on the way into msmtp - transport-side canonical-sender rewriting (the job Postfix does with sender_canonical/header_checks), performed by the new rewrite-mail-from step in the image's sendmail wrapper. This matters beyond cosmetics: submission services tie the sender to the authenticated identity - Office 365 rejects a mismatched header From with SendAsDenied, and WordPress writes wordpress@<site host> there unless site code filters it. No code is injected into WordPress; the whole mail configuration stays in the environment. Unset, envelope and header leave the pod exactly as WordPress wrote them, as before. The rewrite replaces only the first From header of the header block (folded continuations included), inserts one if the message has none, and never touches the body.
  • smtpFromName sets the human-readable display name of the rewritten From header ("Name" <smtpFromEmail>); non-ASCII names are RFC 2047 encoded. It requires smtpFromEmail and fails the render when set alone - a display name without an address configures nothing, and a value that silently does nothing is exactly how this block was broken before.
  • The smoke test pins the wrapper's option composition and the header rewrite (hermetically, against a stub msmtp): TLS switches, envelope sender, auth toggle, the password traveling via --passwordeval and never via argv, the loud refusal without SMTP_HOST, From replacement including folded headers and insert-when-missing, RFC 2047 encoding, the body staying untouched, and byte-identical passthrough when smtpFromEmail is unset. The template test pins the value-to-variable mapping, that unknown smtpProtocol values and a standalone smtpFromName fail the render, and that the SMTP password reaches the pod only through a Secret.

7.1.0-12 ​

  • The image no longer defaults SMTP_HOST to mailpit. The base image ships SMTP_HOST="mailpit" / SMTP_PORT="1025" (its development mail-catcher) in /usr/local/etc/.env. In production that default made PHP mail() resolve a nonexistent host and fail silently on every wp_mail() call — password resets and form notifications never left the pod, with nothing in the logs but a periodic DNS lookup for mailpit. Both defaults are now emptied at build time, so the sendmail wrapper refuses loudly ([mail error] SMTP_HOST is not defined) until a deployment provides SMTP_HOST; with the port unset, msmtp uses its native default (25). Build-time is the only place this can be fixed: the .env uses := expansion, which applies the default even when the variable is set but empty, so a runtime SMTP_HOST="" cannot disable it. Sites sending through an SMTP plugin (e.g. Post SMTP) are unaffected — they never used the wrapper. Sites relying on PHP mail() gain nothing until SMTP_HOST is set, but their failure is now visible instead of silent.

7.1.0-11 ​

  • WP_HOME/WP_SITEURL now resolve under wp-cli. The generated wp-config.php derived both from $_SERVER['HTTP_HOST'] unconditionally; under wp-cli there is no request, so every CLI invocation logged two PHP warnings (Undefined array key "HTTP_HOST") and the site URL degenerated to scheme:/// — visible at every container start since 7.1.0-9's post-init scripts run wp-cli. The fallback chain is now: HTTP_HOST (requests, unchanged) → the new WORDPRESS_HOSTNAME environment variable, fed from ingress.hostname (the canonical name reaches the site through the ingress, so the public scheme stays valid from inside the pod — a https://localhost would not, TLS terminates at the ingress) → http://localhost:<container port> as the last resort without an ingress hostname.
  • Note for file-integrity monitoring: the content of the generated wp-config.php changes with this release (first pod start on 7.1.0-11). Refresh content-hash baselines accordingly; the file remains identical across sites with identical cache values, so the new hash can be computed from the rendered template ahead of the rollout.
  • The W3 Total Cache configuration script now declares SERVER_SOFTWARE=nginx. Under wp-cli there is no server signature, so W3TC fell back to managing Apache .htaccess rules; the WordPress root is immutable here, and every boot from the second one on logged W3TC's misleading FTP credentials don't allow to write to file /var/www/html/.htaccess error (harmless — NGINX never reads .htaccess, and the cache options were all applied regardless). With the server declared, fix_environment maintains its rules in the plugin-managed nginx.conf instead.

7.1.0-10 ​

  • Fix: 7.1.0-9 broke every HelmRelease upgrade — do not use 7.1.0-9. The post-init volume carried two defaultMode keys (the 7.1.0-9 edit added 0750 without removing the pre-existing 0755 on the following line). helm template, helm lint and PyYAML all accept duplicate mapping keys silently (last wins), but the strict YAML parser in Flux's post-renderer refuses the manifest: mapping key "defaultMode" already defined. Flux keeps serving the last successful release, so affected sites silently stayed on their previous chart/image. The template test's manifest checker now parses with a duplicate-key-rejecting loader, so this class of edit fails CI instead of the fleet.
  • Bump idxImageTag to 1.3.2 (wordpress-idx sidecar). Image availability on GHCR verified before the bump.

7.1.0-9 ​

  • Fix: post-init scripts never executed. The Deployment mounted the post-init ConfigMap as a directory at /etc/hook/application/…, while the image's hook runner executes /etc/hooks/<hook> (plural) — and its run-parts invocation neither descends into subdirectories nor runs non-executable files, so even the correct path would not have executed a directory mount. customPostInitScripts and the wordpressConfigureCache W3 Total Cache configuration (introduced in 7.1.0-5) have therefore never run. Scripts are now mounted as single files (/etc/hooks/application/zz-<name>, mode 0750 — the hook runs as root, nothing else needs the bits) with a checksum/postinit rollout annotation; the smoke test pins the image-side execution contract and the template test pins the mount shape. After upgrading, the first rollout executes these scripts for the first time — the next two items define what that means.
  • Behavior change: wordpressConfigureCache configures W3 Total Cache only when it is the active caching plugin. It no longer activates it (and never installs it): sites that set the value but run a different caching plugin would otherwise get a second page cache activated on the first rollout that actually executes the script, with both fighting over the advanced-cache.php/object-cache.php drop-ins. On such sites the script now logs [configure-cache] w3-total-cache is not active and exits. To install and activate W3TC declaratively, list it in wordpressPreinstallPlugins. Where W3TC is active, the chart's engine/server/password settings are (re)applied on every container start — verify after the first rollout that they match expectations, since they have never been applied by the chart before.
  • New wordpressPreinstallPlugins: plugins the pod installs, activates and sets to auto-update, idempotently on every container start. Default: two-factor — given the constant credential-stuffing against WordPress accounts, two-factor authentication should at least be available everywhere. Activation makes 2FA available per user; it does not enforce it (enforcement via the plugin's two_factor_* filters remains a deliberate per-site step). The install needs egress to wordpress.org; without it the step logs and skips, never blocking the pod. Set wordpressPreinstallPlugins: [] to opt out.
  • customPostInitScripts accepts .sh scripts only, and templating fails loudly on other extensions — the previously documented .sql/.php support was never real in this image. The scripts run on every container start (the old "1st boot" wording was misleading), so they must be idempotent.

7.1.0-8 ​

  • New redirect.goneHosts: retired hostnames whose content no longer exists anywhere. Crawlers (matched by User-Agent keywords, including an empty User-Agent) are answered 410 Gone on every path of such a host — the signal to drop the URLs and stop recrawling — while browsers get a courtesy 301 to targetUrl's root (deliberately not path-preserving: the old paths do not exist at the target). This complements redirect.hosts, which stays the right choice for aliases whose content lives on at targetUrl — there, everyone including crawlers should follow the redirect so search engines consolidate onto the canonical name.
  • Gone hosts are routed to the backend through the primary Ingress (with an automatic ingress-shim TLS entry when the Ingress carries a cert-manager issuer annotation) instead of through the controller-level redirect resources, and NGINX answers in the rewrite phase — ahead of the PHP execution allowlist. This matters: a host-based return 410 in nginxCustomServerBlockAddition never fires for .php paths, because the allowlist answers 403 first — exactly the URLs a crawler replaying a dead site keeps requesting, and a 403 reads as "temporarily forbidden", so it never stops. deniedPaths still ranks first, so a honeypot path keeps answering 418 on a retired host.
  • redirect.enabled with an empty redirect.hosts list is now valid as long as redirect.goneHosts has an entry; no controller-level redirect resources are emitted in that case.
  • Extended the image smoke test (crawler/browser/empty-UA matrix, allowlist and honeypot precedence, hostnames suffix entries) and the chart template test (map rendering, checksum, primary-Ingress routing, no leakage into the redirect resources).
  • Bump idxImageTag to 1.3.1 (wordpress-idx sidecar). Image availability on GHCR verified before the bump.

7.1.0-7 ​

  • Fix: an empty or comments-only .htaccess under wp-content stopped the pod from starting. The init container runs with set -e, and the filter that strips comments from a .htaccess exits non-zero when nothing is left — aborting the init script whatever nginx.htaccessPolicy is set to. Any plugin leaving such a file behind would have taken the site down on the next roll. Such files are now reported as "empty or only comments — no access control at all". Affects 7.1.0-6 only; upgrade if you are on it.
  • Fix: changes to NGINX configuration values never reached running pods. The four NGINX ConfigMaps (nginxConfiguration, nginxCustomServerBlockAddition, phpExecutionAllowlist, restApiHardening, nginx.deniedPaths) are mounted with subPath, and a subPath mount never receives ConfigMap updates — the kubelet projects the file once at container start. With the Deployment spec unchanged, no rollout happened, and pods kept serving the previous configuration with nothing to indicate it. The pod template now carries a checksum annotation per ConfigMap, so any value change rolls the pods; identical values keep the checksum stable, so no pod is restarted needlessly.

7.1.0-6 ​

  • The init container's .htaccess report now classifies files by content instead of proposing deny for every one. The first version did that, and on a real site three of seven proposals were wrong: W3 Total Cache's pub/.htaccess guards PHP execution and wants its admin assets served (a directory deny broke the W3TC admin UI), Contact Form 7's captcha directory denies everything except its images (a directory deny breaks the captcha), and an .htaccess that says allow from all for .php in an upload directory is not a protection at all — it is the usual companion of a webshell, and is now flagged SUSPICIOUS rather than covered up by a deny. Directories that already have a location or rewrite in the site's /wordpress/nginx.conf — W3 Total Cache writes its own NGINX rules there — are reported as plugin-managed; a deniedPaths entry would override those rules. Under htaccessPolicy: fail only an uncovered blanket deny blocks the start. Listing a deny-with-exceptions directory in deniedPaths now produces a warning.
  • Added docker/tests/htaccess-test.sh, wired into CI: it runs the check in busybox against a wp-content tree rebuilt from the real files found on that site.

7.1.0-5 ​

  • Fix: the chart now deploys its own image. 7.1.0-4 bumped the chart version but not the imageTag annotation in Chart.yaml, which is the single source for the image tag — so the 7.1.0-4 chart deployed the 7.1.0-3 image. In the default configuration nothing was visibly wrong; with restApiHardening.mode set to enforce or report, the mounted configuration referenced an NGINX variable that only the 7.1.0-4 image defines, and NGINX would have refused to start. If you are on 7.1.0-4 and enabled REST hardening, upgrade. The chart template test now fails when version, imageTag and the images annotation disagree, or when the rendered image tag differs from the chart version.
  • New nginx.deniedPaths: directories or URL patterns NGINX refuses for every caller, declared in values, rendered into a ConfigMap and enforced in the rewrite phase — ahead of location matching and ahead of the PHP execution allowlist. action: deny answers 403, action: honeypot answers 418 (useful with a CrowdSec teapot-ban scenario: probers of a known-removed backdoor ban themselves). A plain location /dir { deny all; } in the per-site nginx.conf does not achieve this: it loses to the static-file regex location for zip|doc|xls|…, and can never produce a honeypot status on a .php path. Default is an empty list; nothing changes unless you add entries.
  • New nginx.htaccessPolicy (warn [default] | fail): the init container now reports .htaccess files under wp-content on every start — NGINX never reads them, so a plugin-written deny from all protects nothing and says nothing — together with the deniedPaths entry that would close the directory. fail refuses to start while any such directory is not covered.
  • The Prometheus exporter moved from bitnami/nginx-exporter to NGINX's own nginx/nginx-prometheus-exporter:1.5.3. Bitnami withdrew the versioned tags from bitnami/* — only latest remains there — so the pinned 1.4.1-debian-12-r5 could no longer be pulled and metrics.enabled: true ended in ImagePullBackOff. The new image is distroless, its entrypoint is the exporter binary (the chart's command: [exporter] override is gone) and it runs as 1001:1001; --nginx.scrape-uri and --web.listen-address are the same upstream flags as before, so metrics.extraArgs carries over. If you pin a Bitnami image via metrics.image, note that those images need the command override this chart no longer sets.
  • Added a weekly workflow tracking the pinned nginx-prometheus-exporter and wordpress-idx versions, opening a bump PR when a newer one appears. It pulls the candidate image before opening the PR, because a release tag existing does not mean the image was published.
  • Dragonfly is bumped to v1.40.1 and now pulled from the project's own registry, docker.dragonflydb.io/dragonflydb/dragonfly. The previous reference dragonflydb/dragonfly:v1.26.0 had no registry prefix and therefore resolved to Docker Hub, where that tag does not exist — memcached.enabled: true ended in ImagePullBackOff. The Docker Hub namespace of that name is not where the project publishes; the vanity domain fronts ghcr.io, and both serve the same digest. Verified that v1.40.1 still accepts --requirepass and --memcached_port and comes up on both ports with the flags the chart passes.
  • Added a weekly workflow that tracks upstream Dragonfly releases and opens a bump PR, mirroring the existing WordPress tracker. It only proposes a bump after checking that the new image still accepts those two flags and starts with them, so a release that drops either fails the workflow instead of producing a PR that breaks the cache.
  • The cache password now reaches W3 Total Cache. The bundled Dragonfly starts with --requirepass and its password is managed by the chart, but the post-init script configured only the server addresses — so with externalCache.type: redis the plugin connected to an authenticated cache without credentials. All three caches now receive it.
  • New values externalCache.password, externalCache.existingSecret and externalCache.existingSecretPasswordKey: an external Redis with requirepass could not be used at all before. The password is passed through an environment variable sourced from a Secret and is never rendered into the post-init ConfigMap; the chart template test enforces that with a sentinel value.
  • WP_CACHE is now defined in the generated wp-config.php, controlled by the new wordpressWpCache value (null [default] follows wordpressConfigureCache, true/false set it explicitly). WordPress only loads wp-content/advanced-cache.php when this constant is true, so without it every page cache plugin degrades to no page caching. Those plugins normally write the constant themselves, which does not survive here: the chart regenerates wp-config.php on every container start, so a plugin-written constant was lost on the next restart or rollout.
  • The bundled W3 Total Cache integration (wordpressConfigureCache) now configures the page cache as well. It previously set up only the database and object caches, both of which work through object-cache.php and need no constant — which is why the gap went unnoticed.

7.1.0-4 ​

  • New opt-in hardening, restApiHardening.mode (off [default] | enforce | report): NGINX answers 403 for anonymous requests that reveal the site's login names — the wp/v2/users REST route in both of its spellings (/wp-json/wp/v2/users and the /?rest_route=/wp/v2/users fallback), and the author archive addressed by numeric id (/?author=1). Matching runs on the normalized path, so percent-encoding, duplicate slashes and . segments do not get around it. restApiHardening.extraDeniedPaths adds further patterns; report mode logs would-be blocks as [rest-hardening] lines on container stderr without blocking.
  • No default changes in this release. The feature is off unless you enable it, and it is off on purpose: the session-cookie exemption it needs in order not to break the block editor is satisfied by any forged cookie, and the same login names stay reachable through the pretty author archive and through oEmbed. It stops bulk automated enumeration and nothing more — worth having as an option, not worth a breaking default. The chart README documents the root-cause fix instead (decoupling user_nicename from user_login), which closes every channel and breaks nothing.
  • When enabled, requests carrying a WordPress session cookie are exempt, so the block editor's author selector and the wp-admin posts-list author filter keep working. The pretty /author/<slug>/ archive and ordinary REST author filters such as /wp-json/wp/v2/posts?author=1 stay reachable.
  • Added a chart template test (chart/tests/template-test.sh, wired into CI alongside helm lint) that renders the chart across a matrix of value combinations and checks structural invariants lint does not: that every ConfigMap a Deployment mounts is actually rendered, that every volumeMount refers to a declared volume, and that every subPath exists as a key in the mounted ConfigMap. This is the failure class behind the 7.1.0-2 server block regression — a mount for a resource the templates never create passes lint and leaves the pod stuck.
  • Extended the smoke test with normalization variants for the PHP execution allowlist (percent-encoded extension and dot, duplicate slashes, . and .. segments, uppercase extension, trailing dot). These assert that PHP is never executed rather than pinning a status code, so the test states the security property instead of today's behavior.
  • Added a smoke test covering every route spelling, the encoding variants, numeric and non-numeric author arguments, REST author filters, the session-cookie exemption, the extra-deny patterns and all three modes.
  • Documented how to set security response headers (CSP, Referrer-Policy, HSTS) on the ingress rather than on the pod, with recipes for Traefik and ingress-nginx. Documentation only — the chart adds no values for it. On ingress-nginx the snippet route needs allow-snippet-annotations: true in the controller ConfigMap, a cluster-wide setting the chart does not own.

7.1.0-3 ​

  • The /healthz.php readiness endpoint is now executed by PHP-FPM, so the probe genuinely verifies PHP-FPM, WordPress core and database connectivity. Previously NGINX served the file statically, and the readiness probe effectively only verified that NGINX itself was up.
  • Behavior change to be aware of: a pod whose PHP stack or database is unavailable now fails its readiness probe and is taken out of the Service endpoints until the dependency recovers. Liveness is unaffected (it remains a plain TCP check), so pods are not restarted during a database outage.
  • Extended the smoke test to assert that /healthz.php responses are rendered by PHP.
  • The published container images now carry an org.opencontainers.image.source label, linking the GHCR packages to this repository.
  • Added docker/wpscan/, a small Alpine image bundling WPScan for scanning self-hosted instances from inside the cluster (as an ephemeral debug container, so scans are not seen by an edge WAF). Both docker-build.sh scripts now take --push/--no-push, -y and --cache, lint the Dockerfile before building, and build multi-arch only when pushing — local builds produce a host-arch image loaded into the Docker daemon.

7.1.0-2 ​

  • Security hardening release. Upgrading promptly is recommended for all installations.
  • NGINX now enforces a PHP execution allowlist: PHP is only executed for known WordPress entry points (index.php, wp-login.php, wp-cron.php, wp-comments-post.php, wp-signup.php, wp-activate.php, wp-admin/*.php and the healthz.php probe); every other .php request is answered with 403. The check runs as an $uri map plus a rewrite-phase guard, independent of location matching order. Plugins and themes are unaffected — their code is include()d by WordPress via the entry points above; only legacy plugins with direct-access .php endpoints need an explicit allow entry.
  • New values: phpExecutionAllowlist.mode (enforce [default] | report | off) and phpExecutionAllowlist.extraAllowedPaths (additional NGINX map regex patterns). report mode logs would-be blocks as [php-allowlist] lines on container stderr without blocking — use it to validate existing installations before enforcing. See the chart README for details.
  • Added a smoke test (docker/tests/smoke-test.sh, wired into CI) that asserts the allowlist blocks and allows the right URLs in all three modes.
  • When upgrading, auditing the uploads directory for stray PHP files is good practice: find wp-content/uploads -name '*.php'.
  • Fix: setting only nginxCustomServerBlockAddition did not render the server block addition ConfigMap (the wordpress.nginx.createServerblockConfigmap helper checked nginxConfiguration instead), while the Deployment still mounted it — leaving the pod stuck on a missing ConfigMap. The helper now checks nginxCustomServerBlockAddition and existingCustomServerBlockAdditionConfigMap.

7.1.0-1 ​

7.0.4-1 ​

7.0.3-2 ​

  • Add idx.pluginAutoUpdate (default true): when the wordpress-idx sidecar serves the bundled WordPress plugin, WordPress installs plugin updates automatically. Set to false to only surface updates in wp-admin. Passed to the sidecar as the PLUGIN_AUTO_UPDATE environment variable.
  • Bump idxImageTag to 1.3.0, the wordpress-idx image that serves the bundled plugin and its update manifest.

7.0.3-1 ​

7.0.2-1 ​

  • Update to WordPress 7.0.2 (includes the 7.0.1 and 7.0.2 maintenance releases). Review the WordPress release notes before upgrading.

7.0.0-11 ​

  • Maintenance release: rebuild image to address CVEs (incl. ImageMagick 7.1.2.27-r0).
  • Fix: bump imageTag annotation so the rebuilt image is actually deployed (it was still pinned to 7.0.0-9).

7.0.0-10 ​

  • Maintenance release: rebuild image to address CVEs.

7.0.0-9 ​

  • Maintenance release: rebuild image to address CVEs.

7.0.0-8 ​

  • Maintenance release: rebuild image to address CVEs.

7.0.0-7 ​

  • Maintenance release: rebuild image to address CVEs.

7.0.0-6 ​

  • Maintenance release: update wordpress-idx image to 0.1.11 to address CVEs.

7.0.0-5 ​

  • Maintenance release: rebuild image to address CVEs in base images.

7.0.0-4 ​

  • Maintenance release: rebuild image to address CVEs.

7.0.0-3 ​

  • Maintenance release: rebuild image to address CVEs.

7.0.0-2 ​

  • Maintenance: imageTag / idxImageTag annotations bumped in lockstep with the chart version (chart convention is that the application image tag and the chart version always match). For this release the image content is unchanged from 7.0.0-1 — a rebuild + retag is required as part of the release pipeline.

  • First-class Traefik support. The chart's primary Ingress now renders correctly under all three supported values of ingress.ingressClassName:

    • nginx: served by rke2-ingress-nginx (or upstream ingress-nginx) — the legacy path; nginx-style annotations are interpreted directly by the controller.
    • nginx-traefik: served by Traefik's kubernetesIngressNGINX bridge provider, which reads the same kind:Ingress and translates most nginx-style annotations into Traefik's internal middleware chain on the fly (proxy-body-size, whitelist-source-range, auth-url, backend-protocol, cors-*, etc.). Useful as a transition class — same Ingress manifest works on both controllers.
    • traefik: served by Traefik's native kubernetesIngress provider. nginx-style annotations are silently ignored; the user is expected to manage behaviour via Traefik annotations (traefik.ingress.kubernetes.io/router.middlewares: …, service.serversscheme: …) and Middleware CRDs. This is the target steady state after the nginx→Traefik migration.
  • New feature: chart-rendered redirect support via the redirect: block. Emits the right resource shape per ingress.ingressClassName:

    • nginx: one kind:Ingress with nginx.ingress.kubernetes.io/permanent-redirect (cert-manager ingress-shim manages the cert).
    • nginx-traefik / traefik: one kind:Middleware (RedirectRegex with capture group → path-preserving 308) + kind:IngressRoute matching all redirect.hosts, plus a free-standing kind:Certificate if redirect.annotations["cert-manager.io/cluster-issuer"] is set.

    Unlike the retired from-to-www-redirect annotation (which only toggled nginx-ingress's built-in www-magic), the new block supports any set of source hostnames in one declaration — vanity domains, backup TLDs, multiple www variants. See the README "Redirects" section for an example covering apex + www across multiple TLDs.

    All chart-emitted redirect resources (Ingress on nginx; Middleware

    • IngressRoute + Certificate on nginx-traefik/traefik) inherit the top-level commonAnnotations and the redirect.annotations overrides — same convention as the primary Ingress.

    Path preservation matrix (different by design):

    • nginx: redirect drops the request path. Limitation of nginx-ingress's permanent-redirect annotation — it issues a static-target 308 with no path interpolation. https://www.example.org/foo?x=1 → https://example.com/
    • nginx-traefik / traefik: redirect preserves path AND query string. The chart emits a Middleware with redirectRegex (^https?://[^/]+(/.*)?$ → <target>${1}, permanent: true), which produces a 308 with the captured path appended. https://www.example.org/foo?x=1 → https://example.com/foo?x=1
  • BREAKING / retired: the bitnami-compatible nginx.ingress.kubernetes.io/from-to-www-redirect: "true" annotation is no longer honoured by the chart. It previously had two effects: (1) triggered nginx-ingress's built-in www-magic redirect — a feature that doesn't translate to Traefik anyway; (2) expanded the primary TLS host list to also cover www.<hostname>.

    Migration path:

    • For the cert-expansion side effect: set ingress.tlsWwwPrefix: true (already existed as a first-class flag, just the annotation fallback was removed).
    • For the actual www→apex (or apex→www) redirect: use the new redirect: block. It works across all three IngressClasses and supports arbitrary source hostnames in one declaration.

7.0.0-1 ​

  • Update to WordPress 7.0.0.

6.9.4-15 ​

  • Maintenance release: rebuild image to address CVEs.

6.9.4-14 ​

  • Maintenance release: rebuild image to address CVEs.

6.9.4-13 ​

  • Remove overly restrictive nginx method allow-list (if ($request_method !~ ^(GET|HEAD|POST)$) { return 444; }) that broke the WordPress REST API for OPTIONS, PUT, DELETE, PATCH — these are required by the Gutenberg block editor and other REST API clients.
  • Fix nginxCustomServerBlockAddition / existingCustomServerBlockAdditionConfigMap: the ConfigMap volume was declared in the Deployment but never mounted into the nginx container, so any user-supplied server-block content was silently ignored. The ConfigMap is now mounted at /etc/nginx/custom.d/01_userconfig.conf via subPath so the image's baked-in 02-userconfig.conf remains visible.
  • Document in values.yaml that existingCustomServerBlockAdditionConfigMap must expose its content under the key 01_userconfig.conf.

6.9.4-12 ​

  • Update wordpress-idx image to 0.1.10

6.9.4-11 ​

  • Update wordpress-idx image to 0.1.9

6.9.4-5 ​

  • Migrate to GitHub and ghcr.io
  • Rewrite README, remove Bitnami branding
  • Update Chart.yaml with IO ANALYTICA attribution
  • Add GitHub Actions for Docker image build

6.9.4-4 ​

  • Version bump

6.9.4-3 ​

  • Version bump

6.9.4-2 ​

  • Version bump

6.9.4-1 ​

  • Update to WordPress 6.9.4

6.9.1-7 ​

  • Update wordpress-idx image tag

6.9.1-6 ​

  • Fix default image tags

6.9.1-5 ​

  • Allow idx port in NetworkPolicy

6.9.1-4 ​

  • Fix idx OOM and probe split

Distributed under the Apache-2.0 license. WordPress, NGINX, Bitnami and other names are trademarks of their owners; this project is not affiliated with or endorsed by them.