Guide v1.1.2 · updated on 10/23/2026
Your browser already protects you. Your server, your Python, your Node, your containers, and your n8n might not. Here is how to really test them and how to fix each layer.

This is a security guide for anyone running a server, website, automation, or AI agent. In October 2026, two serious pieces of news arrived together: the secret keys of 8 certificates from the certificate authority eMudhra leaked (one of them was literally the number 4), and a flaw in PHP, called PHP2Shell, lets attackers break into sites without a password. The guide shows how to test whether each tool is still exposed, using a script that installs and changes nothing (it only runs the tests), and how to update each layer. All you need is a terminal on the machine you want to check.
One breaks the trust behind the HTTPS padlock outside the browser. The other opens PHP sites to break-ins without a login.
From the certificate authority eMudhra, created in a ceremony on 04/30/2025: 6 for websites (TLS) and 2 for signing software. The chains go through the root emSign ECC Root CA – G3 and the cross-signed AAA Certificate Services (Sectigo).
P-384 keys should be gigantic numbers. These came out smaller than 256: 4, 56, 66, 115, 155, 156, 230 and 237. Anyone can find them in under a second. Discovered on 10/17/2026 by the researcher "Wayne".
With the key of a trusted certificate authority, you can issue a "valid" HTTPS certificate for any site and sit in the middle of the connection. As of 10/21 there was no confirmed exploitation, but the keys are public.
Browsers: Chrome and Firefox (10/17), Microsoft (10/20), Apple (10/22). Fixed Firefox: 158.0.1 (NSS 3.131.1).
Everything that is not a browser and has its own certificate list: old curl, Python (certifi), Node, Go, Docker, n8n, AI agents, Electron apps, Rust programs, devices without updates.
Two critical flaws in PHP (CVE-2026-109618 and CVE-2026-109619) that allow running commands on the server without logging in. Advisory from the PHP team on 10/21/2026.
Every PHP installation that serves pages: PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP, and LiteSpeed. Higher risk with mod_php. Includes most WordPress sites.
8.5.12 · 8.4.27 · 8.3.36 · 8.2.35. Details and what to do in PHP2Shell.
The test sites below use certificates from the compromised chains. If the tool connects, it still trusts the broken keys. If you get a certificate error, it's protected.
curl https://dv-g3b-emudhra.we-have-a-problem.com/ curl https://dv-g3b-aaa-emudhra.we-have-a-problem.com/ # "SSL certificate problem" = protegido # shows the page = EXPOSTO
node -e "require('https').get('https://dv-g3b-emudhra.we-have-a-problem.com/',r=>{console.log('EXPOSTO');process.exit()}).on('error',e=>{console.log(/CERT|ISSUER|UNABLE_TO|SELF_SIGNED|DEPTH_ZERO|ERR_TLS/.test(e.code)?'protegido':'SEM TESTE',e.code);process.exit()})" python3 -c "import ssl,urllib.request as u,urllib.error as E try: import certifi; ctx=ssl.create_default_context(cafile=certifi.where()) except ImportError: ctx=ssl.create_default_context() try: u.urlopen('https://dv-g3b-emudhra.we-have-a-problem.com/',context=ctx); print('EXPOSTO') except E.HTTPError: print('EXPOSTO') except Exception as e: print('protegido' if 'CERTIFICATE_VERIFY_FAILED' in str(e) else 'SEM TESTE', e)"
A network error (ENOTFOUND, timeout) is not "protected". When in doubt, use audita.sh.
audita.sh is for Linux. On Windows (PowerShell) and macOS (Terminal), the quick test is the same curl: curl.exe https://dv-g3b-emudhra.we-have-a-problem.com/ (Windows) or curl https://dv-g3b-emudhra.we-have-a-problem.com/ (macOS), and the same for the dv-g3b-aaa-emudhra chain. Certificate error = protected. If you have Node or Python installed, also run the tests above.
AAA chain and accepted the G3 chain (or the other way around). A single test can give a false sense of security. And a network error (DNS, timeout) is not "protected": it's "no test".audita.sh script: everything at once, changes nothingIt tests the real connection with curl, wget, every Node, Deno, and Bun found on disk (including the ones bundled inside tools), the system Python, each Python environment (venv or conda) with its own certifi, and, from the inside, every running Docker container. It checks the Java list (the cacerts file), the PHP version on the server and in containers (PHP2Shell), outdated certifi, and processes that need to be restarted. At the end, it groups what it found by type, with the fix for each. It doesn't install, change, or fix anything, but it runs the tools it tests (that's how it proves the connection). It's a survey: it points out problems; it doesn't certify that the machine is safe.
Golden rule: never run a script from the internet without looking at what it does. This one has about 650 lines, commented.
curl -fsSLO https://raw.githubusercontent.com/inematds/auditoria-seguranca/main/audita.sh less audita.sh # read it chmod +x audita.sh
Or clone the repository: git clone https://github.com/inematds/auditoria-seguranca.
Without sudo it audits your user (and the containers, if you're in the docker group). With sudo it also sees other users' folders. The disk search takes up to 3 minutes; --rapido skips that part.
./audita.sh # full sudo ./audita.sh # includes Docker containers and other users ./audita.sh --rapido # without searching the disk for venvs, Node, Deno, Bun, and JDKs ./audita.sh --sem-docker # doesn't enter the containers ./audita.sh --comandos # shows the fix commands (runs none of them) ./audita.sh --go # also tests Go for real (see below) ./audita.sh --raiz /opt # searches the disk only in this folder (repeat --raiz for several)
Two tests are not connection tests, and the script says so on the line. Java: it checks whether Java's trust file (cacerts) still has the two roots, because testing the connection would require compiling Java code. Go: the real test compiles a small program in a temporary folder (about 100 MB, deleted at the end). Since this writes to disk, it only runs with --go.
With sudo, the Node installed by your user's nvm isn't on the default path; the disk search still finds it (don't use --rapido in that case).
sudo, the script doesn't run as root anything that someone else can swap. A program found on disk (Node, Deno, Bun, a venv's Python, a JDK's keytool) only runs if the file, every link, and every folder on the path up to the root belong to root or to the owner and nobody else can write to them. A user's program runs as that user (runuser), never as root. Anything that doesn't pass becomes SEM TESTE (NO TEST), with the reason. Even so, run as root only on a machine you administer.== meuservidor · Ubuntu 24.04.3 LTS · 23/10/2026 02:36 · audita.sh 1.1.2 -- versões ca-certificates 20260816ubuntu0.24.04.1 -- conexão real (G3 = cadeia emSign ECC Root CA - G3 · AAA = cadeia cruzada AAA Certificate Services) protegido curl G3 protegido curl AAA EXPOSTO node v24.13.0 /usr/bin/node G3 protegido node v24.13.0 /usr/bin/node AAA versão: ATUALIZE para 24.22.1+ EXPOSTO deno 2.7.14 ~/.deno/bin/deno G3 EXPOSTO venv ~/projetos/app/.venv G3 protegido venv ~/projetos/app/.venv AAA -- Java: lista de confiança (cacerts) — conferida pelo keytool, sem conexão EXPOSTO java /etc/ssl/certs/java/cacerts G3 (lista do keystore, não conexão) -- containers Docker em execução (teste de dentro de cada um) [n8n] n8nio/n8n:2.4.8 EXPOSTO node v20.19.0 G3 [painel-db] adminer:latest EXPOSTO php 8.4.21 PHP PHP2Shell: atualize para 8.4.27+ -- processos a reiniciar (não reinicie no meio de job, upload ou live; escolha a hora) REINICIE pid 3361 meubot.service (sistema) /usr/bin/node dist/index.js o binário foi atualizado depois que o processo começou -- resumo meuservidor: 7 EXPOSTO · 0 SEM TESTE · 0 VERIFIQUE · 1 REINICIE -- o que corrigir, por tipo (conta itens; o resumo acima conta linhas, 2 por item: G3 e AAA) 1 Node ex.: /usr/bin/node (v24.13.0) → sudo apt update && sudo apt install --only-upgrade nodejs 1 Deno ex.: ~/.deno/bin/deno (2.7.14) → ~/.deno/bin/deno upgrade 1 containers de versão fixada (grupo C: subir a versão) ex.: n8n (n8nio/n8n:2.4.8) → versão fixada: a tag pode ter sido republicada com a base corrigida — salve o digest atual, puxe a mesma tag e recrie; se o digest não mudou ou seguir EXPOSTO, suba a versão 1 processos a reiniciar (fora de job, upload ou live) ex.: meubot.service (sistema) → sudo systemctl restart meubot.service
Sample excerpt (the real output has more lines; the script writes in Portuguese). G3 and AAA are the two "trust paths" tested: each site certificate is signed by an intermediate, which is signed by a root that your program trusts.
apt changelog <package> | grep CVE-2026-109618. Checked and fixed? Run with --php-ok and VERIFIQUE stops holding up the exit code.At the end comes the what to fix, by type block: how many items of each type, an example, and the suggested command. The summary counts lines (each tool appears twice, G3 and AAA); the block counts items. With --comandos the full list comes out, one command per item, for you to review and run one by one. The script never runs the fix.
At the end there is also the escopo desta rodada (scope of this run) block: what was left out (--rapido, --sem-docker, --raiz, --php-ok, Go without --go, and which user it ran as).
Exit code: 0 the checks that were done passed, within the printed scope; 1 there is EXPOSTO; 2 there is SEM TESTE; 3 nothing connects, but processes still need to be restarted or PHP needs checking. Exit 0 doesn't mean the whole machine is safe. In monitoring, treat any exit other than 0 as an alert, and don't release an automatic deploy on exit 0 alone.
The script goes through SSH itself and runs there, without copying anything to the server. Use key-based access.
./audita.sh --ssh root@servidor1 --ssh deploy@servidor2 # or a list (one destination per line; don't version this file) ./audita.sh --hosts servidores.txt # specific SSH key (applies to all destinations) ./audita.sh --chave ~/.ssh/minha_chave --ssh root@servidor1
If SSH doesn't connect or drops midway (timeout, bash missing there), the line says SEM TESTE (NO TEST) with the code and the exit is not 0: a server that did not respond never passes as clean. Example servidores.txt: one line root@203.0.113.10, another deploy@vps2 # loja. There, the script runs as the SSH user: to see the containers and the folders of other users, log in as root (or as a user in the docker group).
The audit is only done when the summary shows 0 EXPOSTO · 0 SEM TESTE · 0 VERIFIQUE · 0 REINICIE (exit 0) on every machine, in a full pass (without --rapido, and with the scope block showing nothing important left out). A process that was already running keeps the old list in memory: updating without restarting doesn't fix it.
Each tool can have its own list of trusted certificates. The system's ca-certificates protects curl and anything that uses the system list. Node, Python with certifi, Go, Electron, Rust, and every container have their own.
Before. Back up what can't be recovered: the database, volumes, the old image under a backup tag, and the configuration. And confirm the system is healthy right now, so you can compare afterward.
docker exec meu-postgres pg_dumpall -U postgres > backup-$(date +%F).sql # database docker tag meuapp:latest meuapp:pre-update # old image as a backup docker inspect meuapp > meuapp.inspect.json # ports, env, volumes sudo tar czf config-$(date +%F).tgz /etc/nginx /caminho/do/app/.env # configuration curl -fsS https://meusite/health && echo OK # health before docker exec meu-postgres psql -U postgres -d app -c "select count(*) from clientes" # write down the numbers
After. Plenty of things that were running can turn out to be incompatible with the new version. We saw an unpinned dependency break an app, a venv resync and undo the fix, and a proxy hold on to the old address and answer 502. So test and validate: the service came up, it responds, it does what it did before, and the data is there (same counts). Only call it done when it's ok and live.
| Layer | Fixed version | Where the list lives |
|---|---|---|
| Ubuntu | 20260816ubuntu0.<Ubuntu version> (e.g., 20260816ubuntu0.24.04.1) | system |
| Debian | 20250419~deb12u2 (Debian 12) · 20250419+deb13u1 (Debian 13) | system |
| Alpine | ca-certificates 20261020-r0 | system / containers |
| Python | certifi 2026.10.19 · pip 26.2.2 · uv 0.13.5 · Python itself: 3.15.1 · 3.14.9 · 3.13.17 | in each venv |
| Node | 22.23.4 · 24.22.1 · 26.12.1 (Node 20: no fix) | inside the binary |
| Go | 1.27.3 · 1.26.10 + recompile | on Linux, the system's (see step 4) |
| Java (OpenJDK) | the two roots removed from cacerts (keytool -delete) or an updated JDK | each JDK's cacerts file |
| Deno · Bun | rejected both chains in our test: Deno 2.9.8 · Bun 1.4.4 | inside the binary |
| Firefox / NSS | 158.0.1 · NSS 3.131.1 | browser |
| Electron | 44.9.1 · 43.8.1 · 42.11.16 | inside each app |
| Rust | rustls-webpki 0.103.16 · webpki-roots 1.0.10 | inside each program |
| FreeBSD | ca_root_nss 1.131.1 (ports) · base: 15.1-RELEASE-p6 · 14.5-RELEASE-p3 · 14.4-RELEASE-p12 (FreeBSD-SA-26:73) | system |
# Ubuntu / Debian sudo apt update && sudo apt install --only-upgrade ca-certificates dpkg-query -W ca-certificates # check the version # Alpine apk upgrade --no-cache ca-certificates # Fedora / RHEL sudo dnf upgrade ca-certificates
Keep automatic updates on (unattended-upgrades on Ubuntu/Debian). In our case, that's what fixed the system on its own.
requests and many packages use certifi, not the system list. Each venv has its own. audita.sh lists the outdated ones.
/caminho/do/venv/bin/python -m pip install -U certifi pip python3 -c "import certifi; print(certifi.__version__, certifi.where())" # 2026.10.19 or newer # tools installed with pipx / uv tool pipx upgrade-all # or: pipx runpip <app> install -U certifi uv self update # uv 0.13.5 or newer uv tool upgrade --all # conda environments (miniconda/anaconda) conda update -n <environment> certifi
Watch out for the "system" Python: if EXPOSTO (EXPOSED) shows up in certifi mode, there's a certifi installed with pip --user in your home folder (~/.local/lib/python3.X/). The path printed by the command above tells you which one. On Ubuntu 24.04 and Debian 12+, pip refuses to touch that ("externally-managed-environment"); since the change stays in your user folder only, use:
python3 -m pip install --user --break-system-packages -U certifi
The system package's certifi (/usr/lib/python3/dist-packages) doesn't need this: on Debian/Ubuntu it points to the ca-certificates list.
Venv without pip (created by uv): the -m pip command fails. Use uv pointing to the venv's Python:
uv pip install --python /caminho/do/venv/bin/python -U certifi
uv run trap: a project that starts with uv run resyncs the venv from uv.lock on every start and undoes the new certifi. Raise certifi in the lock itself: uv lock --upgrade-package certifi && uv sync. audita.sh warns you when the venv has a uv.lock next to it.Node ships its certificate list inside the program itself. Updating the system changes nothing for it.
# with nvm nvm install 22 && nvm alias default 22 # 22.23.4 or newer # with the NodeSource repository (apt): it's separate per version. # If you're on 20, switch the repository to 22 before installing: curl -fsSLo nodesource_setup.sh https://deb.nodesource.com/setup_22.x less nodesource_setup.sh # read it sudo bash nodesource_setup.sh && sudo apt install -y nodejs # already on 22 or 24? Just update sudo apt update && sudo apt install --only-upgrade nodejs node -v
Stopgap for those who can't switch today (tested by us): make Node use the already-updated system list. It only works if ca-certificates is fixed. --use-system-ca does not work: it adds the two lists together and was still EXPOSTO (EXPOSED) in our test.
export NODE_OPTIONS=--use-openssl-ca # in the app's service/environment; then restart
Node downloaded from nvm or nodejs.org may not find the system list on its own: if the test errors out on everything, add SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt and confirm with audita.sh. NODE_OPTIONS is the environment variable that passes options to every Node started there (in systemd: Environment=NODE_OPTIONS=--use-openssl-ca; in Docker: the compose environment:).
Also look for embedded Node: Playwright, AI agents, and apps ship their own Node. audita.sh finds those binaries on disk; update the tool that brought them. In the case of Playwright inside a Python venv, Node lives in …/site-packages/playwright/driver/node: update the package (pip install -U playwright; with npm, npm i playwright@latest) or use the NODE_OPTIONS=--use-openssl-ca stopgap in the service that uses it.
Install Go 1.27.3 or 1.26.10 and build the binary again (go build). Third-party Go programs: update to whatever version they release after 10/21.
What we saw in our test, on Linux: a program built with Go 1.26.3 rejected both chains with the fixed system list and connected when we pointed it to an old list (SSL_CERT_FILE). In other words, on Linux Go follows the system list, or the list of the container it runs in. Fixing ca-certificates already protects a good share of Go programs; those that embed their own list (the x509roots/fallback package) or run in a container with an old image are still exposed. So recompile anyway. For the real test: ./audita.sh --go.
Java doesn't read the system list directly: it uses its own file, cacerts. On Debian/Ubuntu that file is generated from ca-certificates, but it may not be rebuilt on update. In our case, the system was already fixed and OpenJDK 8's cacerts was still from 2025, with both roots inside.
# where is the cacerts of the Java you use readlink -f $(dirname $(readlink -f $(which java)))/../lib/security/cacerts ls /usr/lib/jvm/ # other installed JDKs # does it still have the roots? keytool -list -keystore /etc/ssl/certs/java/cacerts -storepass changeit | grep -qiE "emsign_ecc_root_ca_-_g3|comodo_aaa" && echo EXPOSTO || echo limpo # Debian/Ubuntu: keep a copy and remove the two roots from cacerts sudo cp -a /etc/ssl/certs/java/cacerts /etc/ssl/certs/java/cacerts.bak sudo keytool -delete -alias debian:emsign_ecc_root_ca_-_g3.pem -keystore /etc/ssl/certs/java/cacerts -storepass changeit sudo keytool -delete -alias debian:comodo_aaa_services_root.pem -keystore /etc/ssl/certs/java/cacerts -storepass changeit
update-ca-certificates -f alone is not enough. In our test it rewrote the file, but the two roots were still there, even with the system already fixed. That's why the removal is done by hand, with the keytool -delete above. The names (debian:…) are Debian/Ubuntu's; on another JDK, use the name keytool -list shows. Then run the test again: it must return limpo. The fixed system already removed those two roots from its own list (check: grep -iE "emSign_ECC_Root_CA_-_G3|Comodo_AAA" /etc/ca-certificates.conf shows both with a ! in front, which means "distrust"); cacerts just fell behind. Only remove them from Java after you see that. If some site stops opening in a Java program after this, update the JDK and ca-certificates-java instead of putting the root back. Remember: for Java, audita.sh checks the list, it doesn't make a connection.A JDK with its own cacerts (downloaded separately, inside an IDE, in an /opt): update the JDK. changeit is the file's default password, which is public; it only protects against accidental writes. Then restart the Java programs.
Like Node, both ship the list embedded. In our test, Deno 2.7.12 and Bun 1.3.13 connected; after updating, Deno 2.9.8 and Bun 1.4.4 rejected both chains.
deno upgrade bun upgrade # one-line test (certificate error = protected) deno eval "await fetch('https://dv-g3b-emudhra.we-have-a-problem.com/')" bun -e "await fetch('https://dv-g3b-emudhra.we-have-a-problem.com/')"
Both live in the user's folder (~/.deno, ~/.bun): run the update as the owning user, not with sudo.
The container doesn't use the server's ca-certificates. Old image = old list. The fix depends on how the image got there, and audita.sh already sorts the exposed containers into these groups (using docker inspect, without touching anything).
redis:alpine, nginx:alpine, postgres:16-alpine, …:latest, …:main. The vendor swaps the image behind the same tag: just pull and recreate.
An image made with docker build from your own Dockerfile. Rebuilding from scratch can break the app; the safe path is the layer on top.
kong:2.8.1, gotrue:v2.197.0. A tag changes little, but it isn't immutable: the vendor can republish the same tag with the fixed base (what never changes is the digest, sha256:…). Pull again and compare the digest; if it didn't change or it's still EXPOSTO, bump the version, and sometimes wait for the vendor.
A container without curl, wget, Node, or Python (e.g., postgres:16 Debian) shows up as SEM TESTE "nenhuma ferramenta de teste" (no test tool) (counts as a test that did not run, and the exit code is not 0): the script has no way to test the connection. Check the list through the package (docker exec <c> dpkg-query -W ca-certificates or apk list -I ca-certificates-bundle) and compare it with the version table.
docker compose up -d --pull always redis # one service; repeat for the others docker compose pull && docker compose up -d # all at once (see the warning about databases below) # proof: the list's package inside the container (Alpine) docker compose exec redis apk list -I ca-certificates-bundle # 20261020-r0 or newer
When testing by hand with busybox's wget (Alpine), the rejection shows up as ssl_client: SSL_connect or Connection reset by peer, which looks like a network error. To be sure, test a good site with the same wget (wget -O /dev/null https://www.google.com/): if that one opens, the test site's failure was a certificate failure. audita.sh already does this check.
latest tag or no major version (postgres:latest, mysql:latest, mongo): the pull may bring a new major version (e.g., Postgres 16 → 18), which won't open the old data directory. Before the pull, pin the major version you use today (postgres:16-alpine) and run pg_dumpall.docker run): save the recipe first, docker inspect redis > redis.inspect.json, and look at .Mounts. Without a volume, the data dies with the container: for Redis, run redis-cli save and docker cp redis:/data/dump.rdb . beforehand; then bring up the new one with the same ports and variables and put the file back.docker compose up -d <service>). audita.sh shows those as "just recreate".:main image from 10/16 stayed EXPOSTO (EXPOSED) after the pull. In that case, use the layer on top (below) until the official one is out.Rebuilding with docker compose build --pull downloads the new base and every dependency with no version pin. In our case, a Node app pulled in new plugins and stopped working. The technique that worked: keep the image that already runs and only swap the list and Node on top.
# 1) keep the current image under another name (it's your way back) docker tag meuapp:latest meuapp:pre-emudhra # 2) Dockerfile.emudhra FROM meuapp:pre-emudhra USER root # installing requires root COPY --from=node:22-bookworm-slim /usr/local/bin/node /usr/local/bin/node # only if the image has Node 22; same major version and same distro as the base RUN apt-get update && apt-get install -y --only-upgrade ca-certificates && rm -rf /var/lib/apt/lists/* # Alpine: RUN apk add -u ca-certificates ca-certificates-bundle RUN python3 -m pip install -U certifi # if the app has Python with its own pip USER app # go back to the original user (see: docker inspect -f '{{.Config.User}}' meuapp:pre-emudhra) # 3) build on top and recreate docker build -f Dockerfile.emudhra -t meuapp:latest . docker compose up -d meuapp # went wrong? go back: docker tag meuapp:pre-emudhra meuapp:latest && docker compose up -d meuapp
.node) built for Node 20 don't load on 22. An image on Node 20 can't be fixed this way; it needs to be rebuilt with a 22 base.pip refuses with externally-managed-environment, the Python is the image's system one (apt): the updated ca-certificates already covers that Python. Remove the pip line; don't use || true, which hides the error.node:22-… in the COPY must be from the same family as the base (bookworm with Debian 12, alpine with Alpine), or the binary won't run.uv run: uv resyncs the venv from uv.lock and undoes the new certifi. Raise certifi in the lock, or swap just the pinned certifi's cacert.pem file for the one from the new list.KeyError: 'ContainerConfig': docker-compose rm -f -s <service> before the up.node:20 base for node:22 (or 24) in the Dockerfile, pin the dependency versions, and rebuild for real (docker compose build --pull).Bump the version in the compose file or the configuration to one released after 10/21/2026 and test again. If the vendor hasn't rebuilt the image yet, there's nothing to pull: you wait on them.
Local Supabase (the one from the supabase start CLI) is the typical case: the CLI pins each image. The path is this, in this order:
# 1) database backup (with everything running) docker exec supabase_db_<projeto> pg_dumpall -U postgres > supabase-backup.sql # 2) new CLI + stop and start (in the project folder; without --no-backup, which would erase the data) npx -y supabase@latest stop && npx -y supabase@latest start # 3) test again ./audita.sh # full: --rapido doesn't find venvs or who to restart
docker rm): use the CLI.supabase start throws yours away.start may bring up more services than before (in our case, from 4 to 12 containers). Check the machine's memory.localhost only, exposure is low. Only a few services make outbound HTTPS connections (authentication, dashboard, logs).n8n runs on Node. Old version = old Node inside the container.
# 1) find the volume name (compose usually puts the folder name in front) docker volume ls | grep n8n # e.g.: meuprojeto_n8n_data # 2) stop n8n and back up (with it stopped, the database doesn't come out half-written) docker compose stop n8n docker run --rm -v meuprojeto_n8n_data:/data -v "$PWD":/bk alpine tar czf /bk/n8n-backup.tgz -C /data . # 3) if compose pins the version (image: n8nio/n8n:2.4.8), swap it for the current one first: # image: n8nio/n8n:2.44.6 (or newer; check the release notes) # 4) update docker compose pull n8n && docker compose up -d n8n docker compose exec n8n node -v # confirm the new Node
Pulling the same tag may bring a republished image. Check whether the digest changed (docker image inspect -f '{{.RepoDigests}}' n8nio/n8n:2.4.8, before and after): if it didn't change, bumping the version is the way.
KeyError: 'ContainerConfig' and leave n8n stopped. Remove the old container first (the data stays in the volume): docker-compose rm -f -s n8n && docker-compose up -d n8n. Even better: migrate to docker compose v2.Other ways to keep the data: if n8n uses a mounted folder (e.g., ./n8n_data or ~/.n8n), copy the folder with n8n stopped; if it uses Postgres, run pg_dump on the database. In every case, keep the encryption key with it (N8N_ENCRYPTION_KEY or the config file in the n8n folder): without it, the saved credentials don't come back.
After updating, check the log (docker compose logs n8n). Big version jumps can bring new warnings, like the one about the Code node's Python runner.
cargo update -p rustls-webpki -p webpki-roots and recompile.A program that's already running held on to the old list in memory: updating without restarting doesn't fix it. Bots, APIs, workers, agents, n8n, and any Python server with a venv (uvicorn, gunicorn) need to restart after the fix. In the processos a reiniciar section, audita.sh lists who is still running with the old program (Node was swapped after the process started) or started before its venv's new certifi.
# by hand: venv Python and Node processes, with run time in seconds ps -eo pid,etimes,args | grep -E "venv/bin/python|/node " | grep -v grep # services that are running systemctl list-units --type=service --state=running systemctl --user list-units --type=service --state=running pm2 list # restart sudo systemctl restart <servico> # system services systemctl --user restart <servico> # your user's services pm2 restart all # Node apps managed by pm2 (pm2 update restarts pm2 itself) docker compose up -d # containers: recreates with the new image
In the script's list, each process comes with what to restart: (sistema) means sudo systemctl restart, (--user) means systemctl --user restart, and processo avulso is something you opened in the terminal, in tmux, or in pm2: restart just that program.
user@1000.service (or another number): it's the user's entire session. It takes down terminals, tmux, jobs, and uploads all at once.Heads up: docker compose restart only restarts the same container, with the old image. After a pull or build, the right command is docker compose up -d.
And run audita.sh again.
Updated? Check, service by service: it came up (docker compose ps, systemctl status), it responds (curl -fsS https://meusite/health), it does what it did before (open it, log in, run a flow), and the data is there (the same counts as before). Went wrong: go back with the backup image (docker tag meuapp:pre-update meuapp:latest && docker compose up -d meuapp). Note: if the new version already migrated the database (n8n, Postgres, apps with migrations), the old image won't open the migrated database: then the way back is to restore the volume backup or the dump together with the old image. On a big version jump, test first on a copy of the volume. And run audita.sh again. It's only done when it's ok and live.
In the early hours of 10/23/2026 we audited 6 servers with this method. No names or addresses: the pattern is what matters.
The ca-certificates on all 6 servers had already been fixed by the automatic update. curl was rejecting the bad chains.
Node, Python environments (certifi), and containers, including n8n, were still trusting the broken keys.
n8n didn't come back up after the pull: docker-compose 1.29 threw KeyError 'ContainerConfig' with the new image, and the service stayed down for a few minutes. Removing the old container before the up fixed it.
An nginx in a container mounted its configuration from a file in /tmp. Cleaning /tmp turned the path into an empty folder, and when the container was recreated, nginx wouldn't start. Mounted configuration has to live in a permanent folder.
The same day we ran audita.sh on the work machine, the one with dozens of projects, Python environments, and local containers. The system and curl were protected. The rest wasn't: 134 EXPOSTO lines (EXPOSED). Fixing it layer by layer, it dropped to 70, and then only what depended on a vendor was left. What we learned, in the order it showed up:
cacerts was from 2025 and still had both roots, even with the system already fixed. update-ca-certificates -f rewrote the file, but the roots stayed: we had to remove both with keytool -delete.deno upgrade and bun upgrade fixed it.pip --user in the user's folder took precedence over the system certifi and left the "system python3" EXPOSTO (EXPOSED).uv pip install --python ….uv run undid the fix at every start, because of uv.lock.:pre-emudhra..bak) forgotten after the version swap: it wasn't running, but it's the kind of leftover someone runs by mistake. Delete old copies you don't use. The pattern behind all of this is just one: anything that isn't the system gets left behind.Notice from the PHP team dated 10/21/2026: two critical flaws let attackers run code on the server without logging in. The flaw is in PHP itself, before WordPress or any application opens the page. A security plugin won't stop it.
CVE-2026-109618: the out-of-memory handling stops PHP with half-processed request data, and the cleanup treats that data as valid.CVE-2026-109619: an internal request state survives after the memory is freed, and the next request in the same process uses that stale state.Shopify, Wix, Squarespace: PHP is the company's responsibility. Check the platform's security notice; if you can't find one, ask support.
Test the site (step below) and check the PHP version in the hosting panel. Below the fixed version: ask your provider to update today.
It's on you, and it's urgent: attacks started even before the fix was released. Update, restart, and look for signs of intrusion.
OpenAI published the php2shell-checker (Apache 2.0 license). It reads the version the site announces in its headers; it doesn't send an attack. It needs git and Node installed on your computer (not on the server).
git clone https://github.com/openai/php2shell-checker && cd php2shell-checker node phpcheck.js "https://seusite.com.br" # OK (0) · POTENTIALLY VULNERABLE (1) · UNKNOWN (3)
Or ask your agent (Claude Code, Codex): "run the php2shell-checker only against my site and explain the result". UNKNOWN is not a green light: the site hides its version; check in the panel or on the server.
php -v php-fpm8.3 -v # adjust the version number apt changelog php8.3-fpm | head -30 # Debian/Ubuntu: look for CVE-2026-109618 # PHP inside containers (panels, WordPress, adminer, phpMyAdmin) for c in $(docker ps --format '{{.Names}}'); do echo "$c: $(docker exec $c php -v 2>/dev/null | head -1)"; done
Don't forget the containers: an image with old PHP is just as vulnerable as the server, and database panels are often exposed on a port. audita.sh already shows the PHP version of each container and of the server and compares them with the fixed ones (EXPOSTO (EXPOSED) below them; VERIFIQUE (CHECK) when the number comes from a distribution package). To fix: pull the new image and recreate the container (group A of the Docker section).
Before: back up the site (sudo tar czf site-$(date +%F).tgz /var/www) and the database (mysqldump --all-databases > wp-$(date +%F).sql), and note whether the home page and /wp-admin open. After: check both again, plus php -v. See the before and after updating checklist.
sudo apt update apt list --upgradable 2>/dev/null | grep -i php # see what will change sudo apt install --only-upgrade $(dpkg -l 'php*' | awk '/^ii/{print $2}') # only the installed PHP packages sudo systemctl restart php8.3-fpm # or apache2, if you use mod_php
Using the ondrej/php PPA repository (common for having several versions)? It also needs to carry the fixed version; check with php -v afterward. mod_php is PHP running inside Apache; PHP-FPM is PHP as a separate service, used with nginx or Apache.
The PHP team itself warns: these measures reduce known attack paths, they do not make the installation safe. Only updating fixes it. Apply them in php.ini, in the FPM pool, or in the server configuration (not in ini_set() or .user.ini) and restart all workers.
# in the FPM pool (e.g. /etc/php/8.3/fpm/pool.d/www.conf), which the application can't change: php_admin_value[memory_limit] = -1 php_admin_value[post_max_size] = 8M # 32M if the site needs uploads php_admin_value[upload_max_filesize] = 6M # always below post_max_size php_admin_flag[session.upload_progress.enabled] = off # PHP 8.5 only: php_admin_flag[fatal_error_backtraces] = off
memory_limit = -1 (against CVE-2026-109618) removes PHP's memory limit. This removes the memory failure the attacker controls, but it doesn't close the path completely: a real allocation failure (the server running out of memory) can still reach the vulnerable point. And a buggy script can consume all of the server's RAM. That's why the PHP team recommends setting a limit outside PHP: the memory limit of the container (Docker) or of the service (cgroup, in systemd MemoryMax=), preferably killing only the affected process.RLIMIT_AS/RLIMIT_DATA (ulimit memory limits) instead: they can trigger the very out-of-memory failure that is vulnerable.session.upload_progress.enabled and fatal_error_backtraces (against CVE-2026-109619) blocks only the confirmed paths, not other variants.session.upload_progress.enabled = 0 even after updating, until the next regular PHP release. The risk only exists in a rare combination: the app sets session.serialize_handler to php_binary or php_serialize in the configuration and switches to php with ini_set() before session_start(), with upload progress on (the default). An app that never switches the serialize_handler at runtime isn't affected by this path and can leave the feature on. If you already turned it off on 10/21, there's nothing more to do. audita.sh warns (an info line) when the feature is on in the command-line PHP; what counts is the FPM or Apache configuration.# .php files created or changed in the last 7 days inside the site find /var/www -name '*.php' -mtime -7 -ls # .php inside the WordPress uploads folder (there should be none) find /var/www -path '*wp-content/uploads*' -name '*.php' -ls # scheduled tasks you didn't create crontab -l; sudo crontab -l -u www-data; ls -la /etc/cron.* # recent logins and new users last -20; tail -5 /etc/passwd wp user list --role=administrator --path=/var/www/seusite # WP-CLI; as root, add --allow-root
Found something you didn't create: take the site offline, change passwords and keys, and restore from a backup made before the intrusion.
Artificial intelligence is finding old flaws faster and faster. The defense is routine, not panic.
curl | bashcurl ... | bash runs code from the internet as your user. Download it first, read it, and only then run it.sha256sum arquivo) or the signature published by the project.package-lock.json, requirements.txt with versions, Docker image with a tag) and update on purpose, not by accident../audita.sh on all machines: summary at zero.Every issued certificate goes into a public log (Certificate Transparency). Look up your domain on crt.sh and check whether a certificate you didn't request has appeared. A CAA record in DNS says which certificate authorities may issue for your domain.
dig CAA seudominio.com.br +short--pull: an old cached image brings an old list.audita.sh in the pipeline as an alarm: any exit other than 0 blocks the deploy (1 EXPOSTO, 2 SEM TESTE, 3 asks for a restart). Exit 0 alone doesn't release anything: it only counts within the scope printed at the end, and the script is a survey, not a certificate..env outside the repository); if a key leaked, replace it, don't just delete it.A company that signs HTTPS certificates. It's the "notary" that says: this site really is who it claims to be.
The root is the parent signature your program trusts. It signs intermediates, which sign the sites. The sequence is the chain. The leaked keys belong to intermediates.
A type of elliptic-curve key. A good P-384 key is a number with more than 100 digits; eMudhra's were smaller than 256.
A Linux package with the system's list of trusted certificate authorities. Used by curl, wget, and many programs.
A Python package with its own list of certificate authorities. requests uses it, not the system's list.
An isolated Python environment for a project, with its own packages (and its own certifi).
A lightweight "machine" inside the server, with its own system and certificate list. Old image = old list.
Managers that keep programs running on the server and restart them.
The file where Java stores the certificate authorities it trusts. Each JDK can have its own; keytool shows what's inside.
The name after the colon in a Docker image. A floating tag (latest, alpine) changes content over time; a pinned tag (2.8.1) changes little, but it isn't immutable: the vendor can republish the same tag. What never changes is the digest (sha256:…).
When a certificate authority is also signed by another, older root, so it works on old devices. Here: the eMudhra chain also goes through the AAA Certificate Services root.
A minimal set of commands (sh, wget…) used in small images like Alpine. Its wget shows different errors than the regular wget.
A public number for a security flaw, used to look up the fix (e.g., CVE-2026-109618).