ANT-2026-XKHXVCFV · supabase/postgres

other low

Severity Claude low · Security research firm - · Maintainer -

Discovered by Claude Mythos Preview

REPORT

Anthropic's analysis of this finding, sealed at approval.

ANT-2026-XKHXVCFV: Root-executed binaries fetched from S3 without integrity check

Several Ansible tasks (admin-mgr.yml:11, admin-api.yml:40-44, supabase-admin-agent.yml:51-68) use get_url to pull tarballs from https://supabase-public-artifacts-bucket.s3.amazonaws.com/ and install the contents as root-run binaries (admin-mgr is a NOPASSWD sudo target; supabase-admin-agent runs as root via systemd). None of these get_url calls set the checksum: parameter, so nothing verifies the downloaded artifact matches what the authors published. An attacker who can write to that S3 bucket, or MITM the image-build network with a trusted CA, can substitute a trojaned tarball. The result is root code execution on every AMI/qcow2 image built after the substitution.

Target

Project: supabase/postgres
Location: ansible/tasks/internal/admin-mgr.yml:11
Discovery: static analysis — not yet dynamically reproduced

Technical Details

The get_url tasks omit the checksum: parameter, so integrity of the fetched tarball depends solely on TLS transport and the security of the S3 bucket. Because the extracted binaries are installed into root-privileged execution paths (NOPASSWD sudoers entry for /usr/bin/admin-mgr and a root systemd unit for supabase-admin-agent), any tampering with the artifact translates directly into root on the target instance.

Reproduction

  1. Obtain write access to supabase-public-artifacts-bucket (or establish a TLS-trusted MITM on the build network).
  2. Replace a referenced tarball (e.g. supabase-admin-api__linux_arm64.tar.gz) with a trojaned build.
  3. Wait for the next AMI/qcow2 image build; Ansible get_url fetches the tarball without checksum verification.
  4. Trojaned binary is installed at /opt/supabase-admin-api / /usr/bin/admin-mgr and executed as root via systemd and NOPASSWD sudoers.

[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]

Suggested Fix

Pin every downloaded executable by cryptographic hash (add checksum: to each get_url task) or verify a detached signature at install time, independent of transport security.

Acknowledgement

This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-XKHXVCFV.


Reference: ANT-2026-XKHXVCFV
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure

UPSTREAM FIX

The change that resolved this finding.

diff --git a/ansible/files/adminapi.service.j2 b/ansible/files/adminapi.service.j2
index 305d1ac629..4e956703a5 100644
--- a/ansible/files/adminapi.service.j2
+++ b/ansible/files/adminapi.service.j2
@@ -14,7 +14,7 @@ Restart=always
 RestartSec=3
 TimeoutStopSec=10
 Environment="AWS_USE_DUALSTACK_ENDPOINT=true"
-{% if qemu_mode is defined and qemu_mode %}
+{% if qemu %}
 Environment="AWS_SDK_LOAD_CONFIG=true"
 {% endif %}
 StandardOutput=journal
diff --git a/ansible/files/gotrue.service.j2 b/ansible/files/gotrue.service.j2
index 481ce7c986..4105e92c66 100644
--- a/ansible/files/gotrue.service.j2
+++ b/ansible/files/gotrue.service.j2
@@ -90,7 +90,7 @@ Environment=GOTRUE_RELOADING_POLLER_ENABLED=false
 # ensures only 1 reload operation occurs during a burst of config updates.
 Environment=GOTRUE_RELOADING_GRACE_PERIOD_INTERVAL=2s
 
-{% if qemu_mode is defined and qemu_mode %}
+{% if qemu %}
 # v3 does not use filesystem notifications for config reloads.
 Environment=GOTRUE_RELOADING_NOTIFY_ENABLED=false
 {% else %}
diff --git a/ansible/files/postgres_exporter.service.j2 b/ansible/files/postgres_exporter.service.j2
index dcb107cb77..4cf067a9a2 100644
--- a/ansible/files/postgres_exporter.service.j2
+++ b/ansible/files/postgres_exporter.service.j2
@@ -6,7 +6,7 @@ StartLimitBurst=0
 
 [Service]
 Type=simple
-ExecStart=/opt/postgres_exporter/postgres_exporter --disable-settings-metrics --extend.query-path="/opt/postgres_exporter/queries.yml" --disable-default-metrics --no-collector.locks --no-collector.replication --no-collector.replication_slot --no-collector.stat_bgwriter --no-collector.stat_database --no-collector.stat_user_tables --no-collector.statio_user_tables --no-collector.wal {% if qemu_mode is defined and qemu_mode %}--no-collector.database {% endif %}
+ExecStart=/opt/postgres_exporter/postgres_exporter --disable-settings-metrics --extend.query-path="/opt/postgres_exporter/queries.yml" --disable-default-metrics --no-collector.locks --no-collector.replication --no-collector.replication_slot --no-collector.stat_bgwriter --no-collector.stat_database --no-collector.stat_user_tables --no-collector.statio_user_tables --no-collector.wal {% if qemu %}--no-collector.database {% endif %}
 
 User=postgres
 Group=postgres
diff --git a/ansible/files/supabase_admin_agent_config/supabase-admin-agent_salt.service.j2 b/ansible/files/supabase_admin_agent_config/supabase-admin-agent_salt.service.j2
index d93d87dfa4..d5647c8865 100644
--- a/ansible/files/supabase_admin_agent_config/supabase-admin-agent_salt.service.j2
+++ b/ansible/files/supabase_admin_agent_config/supabase-admin-agent_salt.service.j2
@@ -6,7 +6,7 @@ Requires=local-fs.target
 
 [Service]
 Type=oneshot
-{% if qemu_mode is defined and qemu_mode %}
+{% if qemu %}
 ExecStart=/opt/supabase-admin-agent/supabase-admin-agent --config /opt/supabase-admin-agent/config.yaml salt --apply --store-result --targets-key  targetsv3-main.yaml
 User=root
 Group=root
diff --git a/ansible/files/supabase_admin_agent_config/supabase-admin-agent_salt.timer.j2 b/ansible/files/supabase_admin_agent_config/supabase-admin-agent_salt.timer.j2
index 390e6da570..52a4a81558 100644
--- a/ansible/files/supabase_admin_agent_config/supabase-admin-agent_salt.timer.j2
+++ b/ansible/files/supabase_admin_agent_config/supabase-admin-agent_salt.timer.j2
@@ -4,7 +4,7 @@ Requires=supabase-admin-agent_salt.service
 
 [Timer]
 {# We're using a significantly lower frequency for triggering this agent on qemu images for the moment. Once we've performed additional validations re: the aggregate impact of running it more frequently, the frequency can be increased. #}
-{% if qemu_mode is defined and qemu_mode %}
+{% if qemu %}
 OnCalendar=*-*-* 0,6,12,18:00:00
 RandomizedDelaySec={{ supabase_admin_agent_splay }}
 AccuracySec=1h
diff --git a/ansible/playbook.yml b/ansible/playbook.yml
index 488dcc26fe..1a2543244a 100644
--- a/ansible/playbook.yml
+++ b/ansible/playbook.yml
@@ -2,6 +2,10 @@
   become: yes
 
   pre_tasks:
+    - name: Set the platform arch as a fact
+      ansible.builtin.set_fact:
+        platform: "{{ 'amd64' if ansible_facts['architecture'] == 'x86_64' else 'arm64' }}"
+
     - import_tasks: tasks/setup-system.yml
   vars_files:
     - ./vars.yml
@@ -40,124 +44,119 @@
       tags:
         - install-pgbouncer
         - install-supabase-internal
-      when: nixpkg_mode or stage2_nix
 
     - name: Install WAL-G
       import_tasks: tasks/setup-wal-g.yml
-      when: nixpkg_mode or stage2_nix
 
     - name: Install pgBackRest
-      import_tasks: 
+      import_tasks:
         file: tasks/setup-pgbackrest.yml
-      when:
-        - nixpkg_mode or stage2_nix
 
     - name: Install Gotrue
+      when: not stage2
       import_tasks: tasks/setup-gotrue.yml
       tags:
         - install-gotrue
         - install-supabase-internal
-      when: nixpkg_mode
-  
+
     - name: Install PostgREST
+      when: not stage2
       import_tasks: tasks/setup-postgrest.yml
       tags:
         - install-postgrest
         - install-supabase-internal
-      when: nixpkg_mode
 
     - name: Install Envoy
+      when: not stage2
       import_tasks: tasks/setup-envoy.yml
       tags:
         - install-supabase-internal
-      when: nixpkg_mode
 
     - name: Install Kong
+      when: not stage2
       import_tasks: tasks/setup-kong.yml
       tags:
         - install-supabase-internal
-      when: nixpkg_mode
 
     - name: Install nginx
+      when: not stage2
       import_tasks: tasks/setup-nginx.yml
       tags:
         - install-supabase-internal
-      when: nixpkg_mode
 
     - name: Install Supabase specific content
+      when: not stage2
       import_tasks: tasks/setup-supabase-internal.yml
       tags:
         - install-supabase-internal
-      when: nixpkg_mode
 
     - name: Fix IPv6 NDisc issues (disabled)
+      when: not stage2 and not qemu
       import_tasks: tasks/fix-ipv6-ndisc.yml
       tags:
         - install-supabase-internal
-      when: nixpkg_mode and (qemu_mode is undefined)
 
     - name: Adjust APT update intervals
+      when: not stage2
       copy:
         src: files/apt_periodic
         dest: /etc/apt/apt.conf.d/10periodic
-      when: nixpkg_mode
-      
+
     - name: Transfer init SQL files
+      when: stage2
       copy:
         src: files/{{ item.source }}
         dest: /tmp/{{ item.dest }}
       loop: "{{ sql_files }}"
-      when: stage2_nix
 
     - name: Create postgres role
+      when: stage2
       become: yes
       become_user: postgres
       shell:
         cmd: /usr/lib/postgresql/bin/psql --username=supabase_admin -d postgres -c "create role postgres superuser login; alter database postgres owner to postgres;"
-      when: stage2_nix
 
     - name: Execute init SQL files
+      when: stage2
       become: yes
       become_user: postgres
       shell:
         cmd: /usr/lib/postgresql/bin/psql -f /tmp/{{ item.dest }}
       loop: "{{ sql_files }}"
-      when: stage2_nix
 
     - name: Delete SQL scripts
+      when: stage2
       file:
         path: /tmp/{{ item.dest }}
         state: absent
       loop: "{{ sql_files }}"
-      when: stage2_nix
 
     - name: First boot optimizations
+      when: stage2
       import_tasks: tasks/internal/optimizations.yml
       tags:
         - install-supabase-internal
-      when: stage2_nix
-      
+
     - name: Finalize AMI
+      when: not stage2
       import_tasks: tasks/finalize-ami.yml
       tags:
         - install-supabase-internal
-      when: nixpkg_mode
-      
+
     - name: Enhance fail2ban
+      when: not stage2
       import_tasks: tasks/setup-fail2ban.yml
-      when: nixpkg_mode
-
 
     # Install EC2 instance connect
     # Only for AWS images
     - name: install EC2 instance connect
+      when: not qemu
       become: yes
       apt:
         pkg:
           - ec2-instance-connect
       tags:
         - aws-only
-      when: qemu_mode is undefined
 
     # Install this at the end to prevent it from kicking in during the apt process, causing conflicts
     - name: Install security tools
@@ -172,53 +171,53 @@
       import_tasks: tasks/clean-build-dependencies.yml
 
     - name: Run migrations
+      when: sta
… (truncated)

https://github.com/supabase/postgres/commit/aae1bedc9d6906ba329529ad2435ba124c428a7e

TIMELINE

Recorded dates, in order.

  1. 2026-05-10 Discovered or logged
  2. 2026-05-15 Sent to maintainer
  3. 2026-05-15 Maintainer acknowledged
  4. 2026-07-29 Patch released
  5. 2026-08-18 Publicly revealed
PROVENANCE

SHA-3-512 hash:

1b5a9a965ae05bb37cfce6d4b4b02c96f9fc8d5224f919cb1295890545119c82fa29b858b54e2114cfeb8cd8bd88b85f6fe03325b6edf61cbf01ae6b3b91197c

Committed 2026-05-18 00:56 UTC

Revealed 2026-08-18 14:12 UTC

Verify (download preimage.json)

Show preimage JSON
{
  "ant_id": "ANT-2026-XKHXVCFV",
  "bug_class": "other",
  "claude_severity": "low",
  "commit_sha": null,
  "created_at": "2026-05-14T22:03:28+00:00",
  "description": "Several Ansible tasks (admin-mgr.yml:11, admin-api.yml:40-44, supabase-admin-agent.yml:51-68) use get_url to pull tarballs from https://supabase-public-artifacts-bucket.s3.amazonaws.com/ and install the contents as root-run binaries (admin-mgr is a NOPASSWD sudo target; supabase-admin-agent runs as root via systemd). None of these get_url calls set the checksum: parameter, so nothing verifies the downloaded artifact matches what the authors published. An attacker who can write to that S3 bucket, or MITM the image-build network with a trusted CA, can substitute a trojaned tarball. The result is root code execution on every AMI/qcow2 image built after the substitution.",
  "discovered_at": "2026-05-10T00:00:00+00:00",
  "location": "ansible/tasks/internal/admin-mgr.yml:11",
  "poc_sha256": null,
  "preimage_version": 1,
  "project": "supabase/postgres",
  "reproduction": [
    "1. Obtain write access to supabase-public-artifacts-bucket (or establish a TLS-trusted MITM on the build network).",
    "2. Replace a referenced tarball (e.g. supabase-admin-api_<ver>_linux_arm64.tar.gz) with a trojaned build.",
    "3. Wait for the next AMI/qcow2 image build; Ansible get_url fetches the tarball without checksum verification.",
    "4. Trojaned binary is installed at /opt/supabase-admin-api / /usr/bin/admin-mgr and executed as root via systemd and NOPASSWD sudoers."
  ],
  "technical_details": "The get_url tasks omit the `checksum:` parameter, so integrity of the fetched tarball depends solely on TLS transport and the security of the S3 bucket. Because the extracted binaries are installed into root-privileged execution paths (NOPASSWD sudoers entry for /usr/bin/admin-mgr and a root systemd unit for supabase-admin-agent), any tampering with the artifact translates directly into root on the target instance.",
  "title": "Root-executed binaries fetched from S3 without integrity check",
  "vendor_severity": null
}