Changelog
Unreleased
- The chart switches STARTTLS on for SMTP credentials itself. With
smtpProtocol: ""andsmtpUser,smtpPasswordorsmtpExistingSecretset, the Deployment now carriesSMTP_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 passesSMTP_TLSon and decides nothing itself. Nothing changes for a site:""stays unencrypted without credentials and STARTTLS with them, andtls,sslandnonerender 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_USERorSMTP_PASSWORDreach the container throughextraEnvVars,extraEnvVarsCMorextraEnvVarsSecret, setsmtpProtocol: tlsorsslnext to them from this version on; the wrapper no longer switches TLS on for them. - An entry named
SMTP_TLSinextraEnvVarstakes 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. WithsmtpProtocolset, the chart used to render its own next to such an entry. - A release with credentials in the SMTP values that hands
SMTP_TLS=offto the container throughextraEnvVarsCMorextraEnvVarsSecretsetssmtpProtocol: nonebefore the upgrade. The chart cannot see into those sources, and its ownSMTP_TLS=onwins over a variable from them. - Whoever runs the image without the chart sets
SMTP_TLS=onnext toSMTP_USERandSMTP_PASSWORDfrom this version on.
- The chart looks at its own SMTP values only. Where
- 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 setsPHP_SESSION_USE_STRICT_MODE=1.
Project and CI
- The template test checks
SMTP_TLSfor an emptysmtpProtocol:onwith 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 thatSMTP_TLSreaches msmtp next to credentials, and that a PHP request runs withsession.use_strict_modeon.
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 switchedfastcgi_keep_conn onin 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 ownnginx.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 tooff, 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_CHILDRENstays 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.
- The sites on 7.1.4-6 to 7.1.411 can be helped before the upgrade with a script in
Project and CI
- The smoke test checks that the NGINX configuration has
fastcgi_keep_conn offand 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.mdsays 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.httpsTargetPortandservice.nodePorts.httpsare gone fromvalues.yaml, and with them the container's variablesNGINX_HTTPS_PORT_NUMBERandWORDPRESS_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, whateverwordpressSchemeis. Their defaults followedwordpressSchemeand asked the porthttpsfor a site withwordpressScheme: https. A probe of the release's own that still names the porthttpsor 443, or asks the HTTP port with the schemeHTTPS, 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-Svcheader 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
httpshas to change.
- The chart renders no HTTPS port: not on the container, not on the Service, not in the NetworkPolicy.
- "Rebuild index" in wp-admin works right after the installation. With
idx.enabled, the search sidecar always has a key forPOST /api/reindex, and WordPress' container hands the same key to the search plugin at every start. Until now the endpoint was switched off unlessidx.reindexApiKeyoridx.existingReindexApiKeySecretwas 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>-idxunderidx-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.reindexApiKeyandidx.existingReindexApiKeySecretwork as before and take the place of the generated key. - The plugin keeps the key as the option
idx_search_api_keyin 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.reindexApiKeyreplaces 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 inidx.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.
- 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
- 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_OPTIONSis 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.
- The first start rebuilds the index once. The index file has a new name and form,
- A script in
customPostInitScriptsunder 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/ssland/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.enabledstarts 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 runsmariadb-upgradebefore 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 ranmariadb-upgradeby hand. - An installation with an external database (
mariadb.enabled: false) is not affected. One that setsmariadb.imageitself 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 image updates its system tables itself. The chart sets
- 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/idxwhatever the path was.
Project and CI
- The template test checks
MARIADB_AUTO_UPGRADEon the rendered Deployment.
7.1.409
- A new installation can store uploads. The image installed WordPress as root, and the installer left
wp-content/uploadswith 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/uploadsand 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_TIMEOUTseconds (300;0does not wait; set it throughextraEnvVars). It asks WordPress's own configuration, so the wait holds for the bundled MariaDB, an external database, awp-config.phpof 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
latestonly 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-idxis 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. Withidx.enabledthe chart runswordpress-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.registryoridx.image.repositoryat a mirror has to have the image there under the chart version, where the chart asked for a1.x.ytag before.idx.image.digeststill wins over the tag. - The image's labels
org.opencontainers.image.versionandorg.opencontainers.image.revisionname the wordpress-idx version and commit. The tags1.x.yup to1.3.7stay as they are; a later wordpress-idx version gets its1.x.ytag from the first release that builds it.
- The chart carries its metadata for Artifact Hub.
artifacthub.io/*annotations replacecategory,licenses,images,imageTagandidxImageTaginChart.yaml; the templates take the image tags from the chart version itself. The images list names every image the chart can run, andhomeis the documentation site. containerSecurityContext.enabled: truegives 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 ofcontainerSecurityContextwere those of the Bitnami chart (user 1001,runAsNonRoot, a read-only root filesystem, all capabilities dropped), with which neither container starts. They are nowrunAsUser: 0,runAsGroup: 0,runAsNonRoot: false,readOnlyRootFilesystem: falseandcapabilities.drop: [], so that enabling the context adds what the image can run with:allowPrivilegeEscalation: false,privileged: falseand theRuntimeDefaultseccomp 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.
wordpressExtraConfigContenttakes effect. The value was passed to the container under a name nothing reads. It now reaches the generatedwp-config.phpasWORDPRESS_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
extraEnvVarsentry namedWORDPRESS_CONFIG_EXTRAkeeps precedence; the value is then not rendered. A variable of that name that comes fromextraEnvVarsCMorextraEnvVarsSecretis overridden by a non-emptywordpressExtraConfigContent, since the chart cannot see into those; keep the content in one place.
metrics.serviceMonitor.metricRelabelingsandmetrics.serviceMonitor.selectortake 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.selectorwas not read at all; its labels are now labels of the ServiceMonitor, next tolabels, 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 innetworkPolicy.extraIngressadmitted it; such a rule is no longer needed. The port is admitted the way the HTTP port is: from every source withnetworkPolicy.allowExternal: true, the default. A release whoseextraIngressrule admitted the scrape from selected sources only has the port open to every source with this version;networkPolicy.allowExternal: falsenarrows all of the pod's ports to labelled clients and the namespaces ofnetworkPolicy.ingressNSMatchLabels. ForextraContainerPortsthe policy now lists each entry'scontainerPort, with itsprotocolwhere 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:
customHTAccessCMandmetrics.prometheusRule. No template read either. NGINX reads no.htaccess; alert rules for the exporter can be deployed throughextraDeploy. 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.apiVersionandsecondaryIngress.secretshave 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, andvalues.yamlnow names each as such and what to use instead.nginxConfigurationandexistingNginxConfigurationConfigMapdo 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.wordpressSkipInstalldoes not skip the installation: the image installs WordPress where the database holds none and leaves an existing site alone. The key stays, becausewordpressConfigurationrequires it; the environment variable the chart set for it, which nothing read, is gone. - The values schema covers every value.
values.schema.jsondescribed 18 of the chart's top-level keys. It now lists every value ofvalues.yamlwith its type, description and default, which is what Artifact Hub shows as the chart's values reference.- Helm checks every install, upgrade and
helm templateagainst 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--setand--set-stringdeliver them), a switch as0/1or 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 asexternalDatabase.portorwordpressPassword, accept these forms as well. The six switches it listed (mariadb.enabled,metrics.enabled, andenabledandtlsof both Ingresses) stay limited totrueandfalse. - 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.deniedPathsorwordpressPreinstallPlugins. Such a value used to render into a manifest that was wrong in some other way. chart/tests/schema-test.shfails when a documented parameter is missing from the schema, a schema entry is not invalues.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 forvalues.yamlsay to runscripts/schema-default.sh.
- Helm checks every install, upgrade and
- The notes Helm prints after an install with
ingress.enabledgive the site's URL and the command that shows the Ingress's address. They told the reader to runminikube ipand 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.pullSecretsis a key ofvalues.yamlagain, with the default[]its documentation gave; it was commented out there. The exporter's probes (metrics.livenessProbe,metrics.readinessProbe,metrics.startupProbeand theircustom*counterparts),metrics.containerSecurityContext.seLinuxOptions,externalDatabase.databaseCharsetandexternalDatabase.databaseCollationare documented now. No change in behaviour.- Documentation: the
nginx.deniedPathsexample uses neutral paths; the "Metrics & Monitoring" section of the README shows the exporter the chart ships;smtpPortsays that msmtp's default port is 465 withsmtpProtocol: ssl;wp-config.phpis 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.Ncan be tagged on the commit of its release: it runs the whole release, publishes chart and images at the candidate's version, and leaveslatestwhere 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 ofChart.yamlis not the newest one in this file.
7.1.408
- Chart versions are stable SemVer versions now. The build number moved from a
-Nsuffix into the patch component: for WordPressx.y.zand buildNthe version isx.y.PwithP = z * 100 + N.7.1.408follows7.1.4-7, and the first version for WordPress 7.1.5 will be7.1.500. SemVer reads a-Nsuffix as a pre-release, so Helm needed--devel, and a version range matched a release only if it named a pre-release itself.appVersionremains 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.
--develis 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.shholds 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 toappVersion. The WordPress release tracker proposesx.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) andapk-tools3.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_TLSis not. WithsmtpProtocol: ""the chart sets noSMTP_TLS, so a site that setssmtpUseror a password now negotiates STARTTLS where it used to send in the clear. Without credentials,""stays unencrypted. Sites withsmtpProtocol: tlsorsslare unaffected.- New
smtpProtocol: nonekeeps 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_TLSvalue per protocol; the smoke test pins the wrapper's TLS default with and without credentials.
- New
- 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_STARTTLSoption the image adds forsmtpProtocol: sslis 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 addan OCI registry) with--develor an explicit--version, since chart versions are SemVer pre-releases. A new "Chart versions" section explains the scheme. - The repository moved to a single
mainbranch; 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 inwp-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. WithwordpressConfigureCachethe 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
wordpressCacheInstanceIdsets 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.phpandobject-cache.phpinwp-content/must equal W3 Total Cache's own copies underplugins/w3-total-cache/wp-content/. A drop-in that differs, or whose plugin is gone, is adropin-modifiedfinding with its digest. This replaces the name-only allowance that came withwordpressConfigureCacheand applies on every site, so a replaced drop-in no longer passes unnoticed.fim.dropinspoints a drop-in at another plugin's copy or removes it from the check. nginx.confis 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_logwith a path, Lua/Perl/njs, ...) is annginx-conf-directivefinding naming the directives; a file nginx could not parse is one too. W3 Total Cache's rules and hand-written rules such as adenypass. A digest baseline fornginx.confremains possible where no plugin writes into it and replaces the directive check there.- A placeholder
index.phpis 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. Elsewhereindex.phpwas accepted by name alone; it is now held to the same content check, so code inwp-content/index.phporplugins/index.phpis a finding.
- Drop-ins are checked against the copy their plugin ships.
fim.php --scan-onceruns 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.shruns 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 underdocker/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.confon 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 existingnginx.confand logging "Creating site nginx.conf" each time. Existing files are unaffected: on the sites checked before the release, everynginx.confwas already owned bywww-datawith 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 everyfim.intervalseconds 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 carriesapp: <release>-fim, which an optional ServiceMonitor copies onto the samples.- Structural rules instead of file-name lists: a
mu-pluginsdirectory, executable files directly inwp-content/, loose files directly inplugins/orthemes/, executable files inuploads/,.user.inifiles, 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.phpis 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
extraVolumeMountsentry that mounts a subPath of the chart's volume below/var/www/htmlis watched as a static path. - Re-baselining from the alert: a
baseline-modifiedfinding carries the digest it found in asha256label. 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: sidecarcovers 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
/metricsstays responsive during long scans, and a hung scan surfaces as a stalewp_fim_last_scan_timestamp_seconds. - Optional
fim.prometheusRulewithWpFimViolation,WpFimWatcherStalledandWpFimWatcherAbsent, scoped to the release.
- Structural rules instead of file-name lists: a
- 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.phpdigest 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
- Update to WordPress 7.1.4. Review the WordPress release notes before upgrading.
7.1.3-8
- The
.htaccessreport now names what is actually open, and stops proposing denies that would break a site. Adeny-class file is narrowed by what it denies:- File extensions (
<Files *.php>inuploads/; Wordfence's<FilesMatch "\.php$">inlib/,views/,waf/and siblings): judged per extension.phpis covered withphpExecutionAllowlist.mode: enforce, because NGINX already answers 403 for that PHP - unlessextraAllowedPathslets 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/uploadsfor anuploads/.htaccessof this kind, which would have blocked every image on the site. - Named files (
<Files debug.log>): onedeniedPathsregex 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
.htaccessdenies.
- File extensions (
- Regex
deniedPathsentries 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: failblocks 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.shnow renders the chart per case with real values instead of patching the script, and pins every case above, including a regression for anextraAllowedPathslookup 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 newidxImageTagnow brings the matching plugin with the pod restart. - Updates on start honour
idx.pluginAutoUpdate: withfalse, 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; disableidxto 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.enabledis the only feature using it; the template test pins that, the mount, the loopback source and bothpluginAutoUpdatemodes.
7.1.3-6
- Bump
idxImageTagto1.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,librawandnginx(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
idxImageTagto1.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,libX11andlibXrender. 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
idxImageTagto1.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
- Update to WordPress 7.1.3. Review the WordPress release notes before upgrading.
7.1.2-1
- Update to WordPress 7.1.2. Review the WordPress release notes before upgrading.
7.1.1-1
- Update to WordPress 7.1.1. Review the WordPress release notes before upgrading.
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 andGEOSEARCHSTORE, 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_tableand the fixed-length replication backlog, neither of which the chart passes. Verified before the bump, against the actual image:--requirepassand--memcached_portare both still accepted, and the container comes up listening on0.0.0.0:6379and0.0.0.0:11211with exactly the flags the chart passes. Password authentication for the cache is unaffected. - The
memcached.imageexample in the chart README still showeddragonflydb/dragonfly:v1.26.0— the unprefixed reference that resolves to Docker Hub, where the project does not publish, and whichImagePullBackOffs. It now matches the shipped default. - Fix: the weekly Dragonfly tracker could never propose a bump. Its flag check piped
dragonfly --helpintogrepunderset -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.phpnow definesWORDPRESS_IDX_BASEwheneveridx.enabledis 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 throughsite_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 asfetch()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 theWORDPRESS_CONFIG_EXTRAeval 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.phpchanges 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
idxImageTagto1.3.4(wordpress-idx sidecar). Besides the constant above, that build routes the plugin's reindex call through the same base URL — it calledsite_url()directly and so ignoredWORDPRESS_IDX_BASEentirely — 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
idxImageTagto1.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 upgradedirectly afterFROM, 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 thereforewp_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,smtpUserandsmtpPassword/smtpExistingSecretworked 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. smtpProtocolhas 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 hadsmtpProtocolset 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
--tlsswitch, which upgrades via STARTTLS. The wrapper now understandsSMTP_STARTTLS(the chart sets it tooffforsmtpProtocol: 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. smtpFromEmailis 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 withsender_canonical/header_checks), performed by the newrewrite-mail-fromstep 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 withSendAsDenied, and WordPress writeswordpress@<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.smtpFromNamesets the human-readable display name of the rewritten From header ("Name" <smtpFromEmail>); non-ASCII names are RFC 2047 encoded. It requiressmtpFromEmailand 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
--passwordevaland never via argv, the loud refusal withoutSMTP_HOST, From replacement including folded headers and insert-when-missing, RFC 2047 encoding, the body staying untouched, and byte-identical passthrough whensmtpFromEmailis unset. The template test pins the value-to-variable mapping, that unknownsmtpProtocolvalues and a standalonesmtpFromNamefail the render, and that the SMTP password reaches the pod only through a Secret.
7.1.0-12
- The image no longer defaults
SMTP_HOSTtomailpit. The base image shipsSMTP_HOST="mailpit"/SMTP_PORT="1025"(its development mail-catcher) in/usr/local/etc/.env. In production that default made PHPmail()resolve a nonexistent host and fail silently on everywp_mail()call — password resets and form notifications never left the pod, with nothing in the logs but a periodic DNS lookup formailpit. Both defaults are now emptied at build time, so the sendmail wrapper refuses loudly ([mail error] SMTP_HOST is not defined) until a deployment providesSMTP_HOST; with the port unset, msmtp uses its native default (25). Build-time is the only place this can be fixed: the.envuses:=expansion, which applies the default even when the variable is set but empty, so a runtimeSMTP_HOST=""cannot disable it. Sites sending through an SMTP plugin (e.g. Post SMTP) are unaffected — they never used the wrapper. Sites relying on PHPmail()gain nothing untilSMTP_HOSTis set, but their failure is now visible instead of silent.
7.1.0-11
WP_HOME/WP_SITEURLnow resolve under wp-cli. The generatedwp-config.phpderived 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 toscheme:///— 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 newWORDPRESS_HOSTNAMEenvironment variable, fed fromingress.hostname(the canonical name reaches the site through the ingress, so the public scheme stays valid from inside the pod — ahttps://localhostwould 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.phpchanges 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.htaccessrules; the WordPress root is immutable here, and every boot from the second one on logged W3TC's misleadingFTP credentials don't allow to write to file /var/www/html/.htaccesserror (harmless — NGINX never reads.htaccess, and the cache options were all applied regardless). With the server declared,fix_environmentmaintains its rules in the plugin-managednginx.confinstead.
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
defaultModekeys (the 7.1.0-9 edit added0750without removing the pre-existing0755on the following line).helm template,helm lintand 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
idxImageTagto1.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 itsrun-partsinvocation neither descends into subdirectories nor runs non-executable files, so even the correct path would not have executed a directory mount.customPostInitScriptsand thewordpressConfigureCacheW3 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 achecksum/postinitrollout 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:
wordpressConfigureCacheconfigures 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 theadvanced-cache.php/object-cache.phpdrop-ins. On such sites the script now logs[configure-cache] w3-total-cache is not activeand exits. To install and activate W3TC declaratively, list it inwordpressPreinstallPlugins. 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'stwo_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. SetwordpressPreinstallPlugins: []to opt out. customPostInitScriptsaccepts.shscripts only, and templating fails loudly on other extensions — the previously documented.sql/.phpsupport 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 totargetUrl's root (deliberately not path-preserving: the old paths do not exist at the target). This complementsredirect.hosts, which stays the right choice for aliases whose content lives on attargetUrl— 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 410innginxCustomServerBlockAdditionnever fires for.phppaths, 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.deniedPathsstill ranks first, so ahoneypotpath keeps answering 418 on a retired host. redirect.enabledwith an emptyredirect.hostslist is now valid as long asredirect.goneHostshas 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,
hostnamessuffix entries) and the chart template test (map rendering, checksum, primary-Ingress routing, no leakage into the redirect resources). - Bump
idxImageTagto1.3.1(wordpress-idx sidecar). Image availability on GHCR verified before the bump.
7.1.0-7
- Fix: an empty or comments-only
.htaccessunderwp-contentstopped the pod from starting. The init container runs withset -e, and the filter that strips comments from a.htaccessexits non-zero when nothing is left — aborting the init script whatevernginx.htaccessPolicyis 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 withsubPath, and asubPathmount 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
.htaccessreport now classifies files by content instead of proposingdenyfor every one. The first version did that, and on a real site three of seven proposals were wrong: W3 Total Cache'spub/.htaccessguards 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.htaccessthat saysallow from allfor.phpin 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 alocationorrewritein the site's/wordpress/nginx.conf— W3 Total Cache writes its own NGINX rules there — are reported as plugin-managed; adeniedPathsentry would override those rules. UnderhtaccessPolicy: failonly an uncovered blanket deny blocks the start. Listing a deny-with-exceptions directory indeniedPathsnow produces a warning. - Added
docker/tests/htaccess-test.sh, wired into CI: it runs the check in busybox against awp-contenttree 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
versionbut not theimageTagannotation inChart.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; withrestApiHardening.modeset toenforceorreport, 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 whenversion,imageTagand theimagesannotation 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: denyanswers 403,action: honeypotanswers 418 (useful with a CrowdSec teapot-ban scenario: probers of a known-removed backdoor ban themselves). A plainlocation /dir { deny all; }in the per-sitenginx.confdoes not achieve this: it loses to the static-file regex location forzip|doc|xls|…, and can never produce a honeypot status on a.phppath. Default is an empty list; nothing changes unless you add entries. - New
nginx.htaccessPolicy(warn[default] |fail): the init container now reports.htaccessfiles underwp-contenton every start — NGINX never reads them, so a plugin-writtendeny from allprotects nothing and says nothing — together with thedeniedPathsentry that would close the directory.failrefuses to start while any such directory is not covered. - The Prometheus exporter moved from
bitnami/nginx-exporterto NGINX's ownnginx/nginx-prometheus-exporter:1.5.3. Bitnami withdrew the versioned tags frombitnami/*— onlylatestremains there — so the pinned1.4.1-debian-12-r5could no longer be pulled andmetrics.enabled: trueended inImagePullBackOff. The new image is distroless, its entrypoint is the exporter binary (the chart'scommand: [exporter]override is gone) and it runs as1001:1001;--nginx.scrape-uriand--web.listen-addressare the same upstream flags as before, sometrics.extraArgscarries over. If you pin a Bitnami image viametrics.image, note that those images need the command override this chart no longer sets. - Added a weekly workflow tracking the pinned
nginx-prometheus-exporterandwordpress-idxversions, 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 referencedragonflydb/dragonfly:v1.26.0had no registry prefix and therefore resolved to Docker Hub, where that tag does not exist —memcached.enabled: trueended inImagePullBackOff. 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--requirepassand--memcached_portand 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
--requirepassand its password is managed by the chart, but the post-init script configured only the server addresses — so withexternalCache.type: redisthe plugin connected to an authenticated cache without credentials. All three caches now receive it. - New values
externalCache.password,externalCache.existingSecretandexternalCache.existingSecretPasswordKey: an external Redis withrequirepasscould 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_CACHEis now defined in the generatedwp-config.php, controlled by the newwordpressWpCachevalue (null[default] followswordpressConfigureCache,true/falseset it explicitly). WordPress only loadswp-content/advanced-cache.phpwhen 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 regenerateswp-config.phpon 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 throughobject-cache.phpand 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 — thewp/v2/usersREST route in both of its spellings (/wp-json/wp/v2/usersand the/?rest_route=/wp/v2/usersfallback), 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.extraDeniedPathsadds further patterns;reportmode 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_nicenamefromuser_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=1stay reachable. - Added a chart template test (
chart/tests/template-test.sh, wired into CI alongsidehelm 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 everysubPathexists 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 needsallow-snippet-annotations: truein the controller ConfigMap, a cluster-wide setting the chart does not own.
7.1.0-3
- The
/healthz.phpreadiness 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.phpresponses are rendered by PHP. - The published container images now carry an
org.opencontainers.image.sourcelabel, 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). Bothdocker-build.shscripts now take--push/--no-push,-yand--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/*.phpand thehealthz.phpprobe); every other.phprequest is answered with 403. The check runs as an$urimap plus a rewrite-phase guard, independent of location matching order. Plugins and themes are unaffected — their code isinclude()d by WordPress via the entry points above; only legacy plugins with direct-access.phpendpoints need an explicit allow entry. - New values:
phpExecutionAllowlist.mode(enforce[default] |report|off) andphpExecutionAllowlist.extraAllowedPaths(additional NGINX map regex patterns).reportmode 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
nginxCustomServerBlockAdditiondid not render the server block addition ConfigMap (thewordpress.nginx.createServerblockConfigmaphelper checkednginxConfigurationinstead), while the Deployment still mounted it — leaving the pod stuck on a missing ConfigMap. The helper now checksnginxCustomServerBlockAdditionandexistingCustomServerBlockAdditionConfigMap.
7.1.0-1
- Update to WordPress 7.1. Review the WordPress release notes before upgrading.
7.0.4-1
- Update to WordPress 7.0.4. Review the WordPress release notes before upgrading.
7.0.3-2
- Add
idx.pluginAutoUpdate(defaulttrue): when the wordpress-idx sidecar serves the bundled WordPress plugin, WordPress installs plugin updates automatically. Set tofalseto only surface updates in wp-admin. Passed to the sidecar as thePLUGIN_AUTO_UPDATEenvironment variable. - Bump
idxImageTagto1.3.0, the wordpress-idx image that serves the bundled plugin and its update manifest.
7.0.3-1
- Update to WordPress 7.0.3. Review the WordPress release notes before upgrading.
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
imageTagannotation 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/idxImageTagannotations 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 byrke2-ingress-nginx(or upstream ingress-nginx) — the legacy path; nginx-style annotations are interpreted directly by the controller.nginx-traefik: served by Traefik'skubernetesIngressNGINXbridge provider, which reads the samekind:Ingressand 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 nativekubernetesIngressprovider. 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 peringress.ingressClassName:nginx: onekind:Ingresswithnginx.ingress.kubernetes.io/permanent-redirect(cert-manager ingress-shim manages the cert).nginx-traefik/traefik: onekind:Middleware(RedirectRegex with capture group → path-preserving 308) +kind:IngressRoutematching allredirect.hosts, plus a free-standingkind:Certificateifredirect.annotations["cert-manager.io/cluster-issuer"]is set.
Unlike the retired
from-to-www-redirectannotation (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-levelcommonAnnotationsand theredirect.annotationsoverrides — same convention as the primary Ingress.
Path preservation matrix (different by design):
nginx: redirect drops the request path. Limitation of nginx-ingress'spermanent-redirectannotation — 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 aMiddlewarewithredirectRegex(^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 coverwww.<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.
- For the cert-expansion side effect: set
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 forOPTIONS,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.confviasubPathso the image's baked-in02-userconfig.confremains visible. - Document in
values.yamlthatexistingCustomServerBlockAdditionConfigMapmust expose its content under the key01_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