Case study Legacy Migration & PHP Security

PHP2Shell Remediation: PHP 7.4 to 8.3 Migration in One Day

How Actinode migrated a B2B SaaS API from PHP 7.4 to PHP 8.3.36 in one day on AWS EKS, with no added pods and a brief tenant interruption.

Explore the project
The project at a glance
Client
Confidential B2B SaaS client
Industry
Multi-tenant B2B SaaS
Timeline
One day
Project date
21 October 2026
1 dayDelivery window
6Hostnames on PHP 8.3.36
0Additional pods retained
~10 secError window on one tenant host

Reported project results · Specific to the original project scope. See results and context below.

The brief

Project overview

PHP2Shell vulnerability remediation, PHP runtime upgrade and legacy Symfony application migration on AWS EKS.

Based on Actinode’s project delivery account. The client is anonymised. The cutover included approximately 10 seconds of HTTP 503 errors on one tenant host; this was not a zero-downtime migration.

Business context

A multi-tenant B2B SaaS platform depended on two production API stacks in the same AWS EKS Kubernetes cluster. The legacy stack ran PHP 7.4.33 with Symfony 5.2; the newer stack ran PHP 8.1 with Symfony 6.4 LTS. Each served a main hostname and two dedicated tenant hostnames.

The PHP2Shell disclosure made the runtime upgrade urgent. The client needed a practical route away from unsupported PHP without turning an emergency security update into a full application rewrite. The project addressed runtime exposure; it did not claim to resolve every application security issue.

Requirements and constraints

Move all six production hostnames onto PHP 8.3.36, retain the existing microservice routes, limit disruption and preserve a fast rollback path. Avoid permanent additional Kubernetes workloads or CPU reservations.

The application also needed dependency compatibility checks, a normal development-to-production release path and explicit follow-up work for issues outside the urgent runtime cutover.

Original challenge

The two application branches had diverged by 49 commits unique to the newer branch and 18 unique to the legacy branch. Updating the older Symfony application in place would require broader compatibility changes; a trial cherry-pick conflicted in every file it touched.

A separate deployment based on the newer code was built and deployed, but would duplicate workloads and reserve about 2.5 additional vCPU. Cluster nodes already had 82–94% of CPU capacity reserved, even though actual use was much lower.

The deciding discovery was that the stacks used the same database and signing keys. After checking this shared foundation, the team could reuse the existing newer application pods rather than keep a third deployment.

Solution and contribution

Actinode split the work into two stages: upgrade the newer stack to the patched PHP runtime, then move legacy hostnames onto that stack through Kubernetes Ingress changes.

PHP 8.3.36 was the chosen security release for this environment, rather than a claim that every application should use the same version. The PHP2Shell vulnerabilities are tracked as CVE-2026-109618 and CVE-2026-109619. Runtime patching and application migration were treated as related but distinct work.

The legacy pods stayed running during the cutover so the team could restore the previous routing if needed. Only the root application backend changed; the existing microservice routes remained in place.

Implementation

How we delivered it

Resolve the PHP compatibility blocker

Composer identified lcobucci/clock 3.0.0 as the locked dependency blocking PHP 8.3. Resolution in an official Composer container updated that package to 3.5.0 without changing other locked packages. This avoided a Composer failure encountered on a PHP 8.1 development machine.

Build and validate the production image

The FPM base image changed to PHP 8.3.36. Composer’s PHP requirement and platform configuration were aligned with the target runtime. The linux/amd64 production image was checked for the required MongoDB, GD, Intl, PDO MySQL, PDO PostgreSQL and ZIP extensions. Platform requirements, production cache clearing and Symfony container linting passed.

Release through development and production

The newer application was released to the development environment for testing before production. Its production pods rolled out healthy on the updated image, establishing the destination for legacy traffic.

Back up and switch routing incrementally

The team exported the three legacy Ingress definitions before editing them. A guarded JSON patch checked the existing backend before changing the main hostname. After validating the main host, the two tenant hostnames were switched in turn.

Verify the backend and retain rollback

Observed routing changed to the PHP 8.3.36 backend in about 20 seconds per hostname. Ten subsequent requests to the main legacy host reached the upgraded runtime, and the inspected newer-stack pod logs showed HTTP 200 responses. One tenant host experienced about 10 seconds of HTTP 503 errors; no errors were observed on the other two legacy hosts. Saved routing definitions and running legacy pods provided the rollback path.

The outcome

Results & business impact

  • All six production hostnames were reported on PHP 8.3.36 after the upgrade and routing cutover.

  • Legacy hostnames moved from PHP 7.4.33 and Symfony 5.2 to the existing PHP 8.3.36 and Symfony 6.4 LTS application.

  • The selected routing approach required no additional pods or CPU reservations; temporary alternative workloads were listed for cleanup.

  • The team retained the previous workloads and three saved Ingress definitions for rollback during the stabilisation period.

These are observations from this deployment, not an uptime SLA or an independent security audit. Ten post-cutover requests confirmed the new runtime on the main legacy host; that sample does not establish long-term availability or the absence of compromise.

What this means for the business

Reused an existing upgraded application path instead of committing to a full rewrite during the security response.

Avoided keeping a duplicate deployment that would consume scarce reserved cluster capacity.

Made the cutover observable and reversible while documenting the remaining maintenance work.

Conclusion

The fastest workable legacy migration came from understanding the existing systems before adding infrastructure. A shared database and compatible authentication foundation allowed Actinode to upgrade one stack and redirect legacy traffic through small, reversible routing changes. That decision was specific to this application and required validation; it is not a universal shortcut for migrating independent systems.

Next steps in the original project account

The delivery account records outstanding work: reconcile three legacy-only fixes, change the rollout strategy for single-replica tenant deployments, commit the legacy Ingress definitions to source control, remove temporary alternative workloads, and retire old pods after a stable period.

A further framework upgrade and remediation of 67 Composer security advisories across 21 packages remained on the backlog. Completing the PHP runtime upgrade did not mean the application was free of other security issues.

Technology & expertise

  • PHP 8.3.36Runtime
  • Symfony 6.4 LTSApplication framework
  • ComposerDependency management
  • DockerContainer images
  • AWS EKS / KubernetesOrchestration and ingress
  • AWS ALB / ECRTraffic routing and image registry
  • Bitbucket PipelinesBuild and release

Further reading

Related engineering guides

Security references

Explore more case studies

Discuss a focused software engagement

Tell us the problem, users and constraints. We can discuss whether discovery, a first release or a system improvement is the right next step.

Discuss your project scope