diff --git a/hosts/fw/modules/dnsmasq.nix b/hosts/fw/modules/dnsmasq.nix index 0859fa6..ace789b 100644 --- a/hosts/fw/modules/dnsmasq.nix +++ b/hosts/fw/modules/dnsmasq.nix @@ -12,7 +12,6 @@ server = [ "/epicenter.works/10.50.60.1" - "/epicenter.intra/10.50.60.1" "/akvorrat.at/10.50.60.1" "9.9.9.9" "149.112.112.11" diff --git a/hosts/fw/modules/wireguard.nix b/hosts/fw/modules/wireguard.nix index 3ab00e8..9edc537 100644 --- a/hosts/fw/modules/wireguard.nix +++ b/hosts/fw/modules/wireguard.nix @@ -47,7 +47,7 @@ endpoint = "5.9.131.17:51821"; publicKey = "T7jPGSapSudtKyWwi2nu+2hjjse96I4U3lccRHZWd2s="; presharedKeyFile = config.sops.secrets.wg_epicenter_works_psk.path; - allowedIPs = [ "10.14.1.0/24" "10.14.2.0/24" "10.14.11.0/24" "10.14.40.0/24" "10.14.50.0/24" "10.25.0.0/24" "10.50.60.0/24" "10.60.60.0/24" ]; + allowedIPs = [ "10.14.1.0/24" "10.14.2.0/24" "10.14.11.0/24" "10.14.40.0/24" "10.25.0.0/24" "10.50.60.0/24" "10.60.60.0/24" ]; } ]; }; diff --git a/hosts/mail/configuration.nix b/hosts/mail/configuration.nix index cb1fc81..dff1253 100644 --- a/hosts/mail/configuration.nix +++ b/hosts/mail/configuration.nix @@ -10,7 +10,6 @@ ./modules/openldap.nix ./modules/dovecot.nix ./modules/postfix.nix - ./modules/dkim-fueltide.nix ./utils/modules/borgbackup.nix ./utils/modules/promtail diff --git a/hosts/mail/modules/dkim-fueltide.nix b/hosts/mail/modules/dkim-fueltide.nix deleted file mode 100644 index 2a0af27..0000000 --- a/hosts/mail/modules/dkim-fueltide.nix +++ /dev/null @@ -1,28 +0,0 @@ -{ config, pkgs, ... }: - -{ - sops.secrets.rspamd-dkim-fueltide-io-key = { - owner = "rspamd"; - group = "rspamd"; - mode = "0400"; - }; - - # rspamd's dkim_signing module in rspamd.nix picks up per-domain keys from - # /var/lib/rspamd/dkim/$domain.$selector.key. This one-shot drops the - # fueltide.io key into place before rspamd starts. - systemd.services.rspamd-dkim-fueltide-setup = { - description = "Install fueltide.io DKIM key into rspamd"; - wantedBy = [ "multi-user.target" ]; - before = [ "rspamd.service" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - script = '' - install -d -o rspamd -g rspamd -m 0750 /var/lib/rspamd/dkim - install -o rspamd -g rspamd -m 0400 \ - ${config.sops.secrets.rspamd-dkim-fueltide-io-key.path} \ - /var/lib/rspamd/dkim/fueltide.io.default.key - ''; - }; -} diff --git a/hosts/mail/secrets.yaml b/hosts/mail/secrets.yaml index 689069b..a50f7ce 100644 --- a/hosts/mail/secrets.yaml +++ b/hosts/mail/secrets.yaml @@ -1,48 +1,47 @@ -borg-passphrase: ENC[AES256_GCM,data:BPfGmuF0wI6LAge/wWObEHhUxfyNHYmFHJW3kkFxxHQDjQqQtORfGiQGUYnzw6BhJa7FGpvHHiagLbSZcpXvWw==,iv:jzm3toujgf2rCwDokbR3/YEs6BBwt5DNUyzoLQiBlSE=,tag:/X/7tG1bG/wqNhshMfUkSg==,type:str] -borg-ssh-key: ENC[AES256_GCM,data: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,iv:8qdeLajGkVgn5xw44BJNUbUZQH2cMq5mBnZByvktsuI=,tag:YjNLIl0mw7h+6wfI5hYnQQ==,type:str] -netdata-claim-token: ENC[AES256_GCM,data:XB+OXsHtohopphWDWbW7dAI/UXbntsHRIOt4OiWI4QPy1pamL7f9x4QPTMUM2TfVqxrRYGdvDXh0fnUTIK8OqoksrrjdOiy2fQ6k4W7y11+/Un2bEXTMrS3GT3BcVYN9ppc/VUhgX/JDmIm9EptLyASOV0VyQCHOkTVLuyYfQva7tetVgX+W,iv:8cpwuMQi3IAAYSGOzKPTsr+SrUW95UB+YCZBO0sDdEw=,tag:WBcvCoknTgkxgbWRAKWwLA==,type:str] -openldap-rootpw: ENC[AES256_GCM,data:GtR9nwx1f5zx8D8p6cmvCyM1lKyKXDdcum6mCvU87Jm/C868qRiatLDBbP6qUsDzzyFG+9hyVPetik88kGhvrw==,iv:j5JYdAbUga5eUFmIUNrPNZ0G6Sx1zYtb68nNVAClpXs=,tag:WpcrFPRuqTpRZmcrr6T/Vg==,type:str] -dovecot-ldap-password: ENC[AES256_GCM,data:86vTpWKCKINNrkD+a1UJeJkECW+vmIwXrtD4KPyNBmmPN6xi+LutzEDuwIGKQrC1ISTcmjo3SePsR1KTDSqJ3A==,iv:kqyT1bEyCWHvs8o6wwSC+08jtuOc/gA77yFCkv75gQg=,tag:hLt7Vw5WltVI1L83adcepA==,type:str] -rspamd-dkim-fueltide-io-key: ENC[AES256_GCM,data:Rhiuu+vc6XyYnY/Syjs1qvrZeIW3HEPcK/GNUEN+FDQ/vBMHwSnw5XTmS8DjyvMMNB6ga+YrRAOPFu1P4Py23w3HEA9YL/+5g1QLOveBwS159peRSBsGB029JHiFLrZ4cpiaBSn36gr+/ygSpQRM0JPWhJkh8t456bnc9rtFSn8MBgT/AnZh5SYGfhNyrj7HfRV09espFSqRMQyccxykiQi84ojasQiq/lCLv9TLz07bZAQRbu/cEAe2yQKfPrgCCY3m27wJf/Hd/iCWv4/uWXcqlD8lCnK0Auat0s5Qq36oKUF6GMmVME2jRSLjwo9Uo6rT16iUA6lV/TrsYPAcxBtkXroiZn8sp6aXRvOHaWuLwClF3ajfZ06PO4WB2Wi7up6s3WASGF7GX4zyPmi8FEfo/QnCTeqcLGZdBrwqkAkPZ7S7IB0pYCHWLfp47TX2IB7T4AB6RJZ6LQ1nwqLgChjEZXF7eylVZyNPPLpSV3WegdkvqmLA+o5WyzGI76hkKNou91Quizd2BhbWKfvlbcqLvu3jWnsp/3xKmFS+7CdhB5P+pkNYnvQN7r33UrVhm3jSbDF5NJqMiVmgX5H8/4m9bNS7KRDbGQdQo/egploOc5PKUVrxlXFTpEofpoGBy/XW+69y0GoPYPAB4nFT0fIGox/N6bxgP232MMZKn3wBqiI3l8UVQRU/OT71bta/gQo+Y+PljSn18hF41k77j421FXG41rL1Psuc5SSKKI4vxxogHE3RdbhEvE/mSawM+kgWGOBfK0VejxGkBjPRKO+bP5HnN0J4L35D+3y0zpYkHTJJP99T+14GECmogk1GrAj8EQFRCSgE3bq58VlUKV9ZmBDS+/gbpxZ/bcXWpg7OQZ3OvZFKh3MgmphYQhiqy2kamLOisqU3ueQNgA9h4QEX48cPxBo69hTGsfn+IMI1lJYjZsm7hykvGWxfHN3uH971ZYh8Ct3xaFX8Z3pNBmvuWm11aLzglwOVTQJXFBBRbIB3v1mHexGa13QjEXJ2smZyaW5cdpLBVEYmWCR+sHoOq+7V6F67bpEOyfVKl4pyRXgg+u7t049MsLqyZ5i0yTVmSEvqGdwhkYQZi3y3RiMOFvorb63ePuSSfxXrs0GXdIfQhtWkUMGXDk+r+UC+l0jh31VJ3gwNBiDit8WKmLpKEMvaTi4Z+by6oZY3s5G/f5j0HXpOqnJi944bxF/5eXa6YQ==,iv:CC1jJ0YBTUwiwX8fPXub1+yG+eeDIUBorv7mgTRWGLw=,tag:M7L0763goCdaM5o8UZ9QTQ==,type:str] +borg-passphrase: ENC[AES256_GCM,data:D6+ZedxUQ7m/m0YkM5m/B4kFsNySJjFyh8Gmhn3Mpe+mqEzzMRjAbwmGzx9i9Lnr1dTjRElUOgevnnvW5J2KRA==,iv:cG4w1KsEm1SOTni9bsbSW1+ypzjjs2Q42I+4xvcCAu0=,tag:WkkNVa27Uy5nFpmXaIH6ww==,type:str] +borg-ssh-key: ENC[AES256_GCM,data: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,iv:yA1CkRMapP1S3zMwu6Tj0/0/HHpwD1yRAm/qrZx/kPs=,tag:SYg2IoXeD9fMYb35J/AJ1Q==,type:str] +netdata-claim-token: ENC[AES256_GCM,data:ECx8zLnU/dj08vfA76oVbVzL3JG9MLBoFmxSjtjiFbSiFtdaHtG/8u5FEuyQ1bQMQntV91xj7x1kY8fAp7VNbWyC13pOEOrt6rvJYch14eM3bqNvfGeqgJsHmAaRbY6mBrxJBkiRJBLYVil4e1oDNZVnzFQ4ditXZbMGtAV2063K1MRI/48p,iv:viE84mOp5KSdj8vdK5XxR0W9A54oPxQO5ahnpPLeAdE=,tag:WjzKjGXRRAc7vlzreFHbng==,type:str] +openldap-rootpw: ENC[AES256_GCM,data:W0em1Dffg+IUoynwwPD4NjFksR38ZO4mhWFI83ALvYcwYIplxw/gDRLGCqbSt6TR5C65CKr1sOUiU+4Xq3UWmw==,iv:BHQhISTIYuwSM3KiSb0mEEo3BMNo6FXEDXoIvI3SZrU=,tag:tX8gfnk1JYnaNionk/jrLg==,type:str] +dovecot-ldap-password: ENC[AES256_GCM,data:JYAt8/WggwclNEPO9CaWfQsvQBA8DDJCU2km93HpowoVwIdvQ/0lQHeXndPYe1EmJGJ3vLErie+Zn2kDINIMqQ==,iv:HR0QJ0GgQks3NzhfXwjHupCKcPOekkiTcp5Jxbz7CxI=,tag:19m7F6TjGUPOuHQJuUq2pw==,type:str] sops: age: - recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1dWxFbG1McEYzWlN2WmZ6 - enU5bnRjblI4ZHhvVHhIMGdBdzR5VFBrL1E0CkljRmpqTko3NDdXTS9RWDVXaDZl - bVVjbGJwalZuT3VMdUErUUg3N2JiL1UKLS0tIEcrYTNGSFYvd0VLRnJ2V0syNGNz - UlNlWURkNmk0dXBRQ212U0dWaXpxM0UKS+6vyPlzyhlgbj+1OHdv07I8CKK3dLKN - 8jY30HiMPoBWS6Rk8mItRcLi56aTEGUsbdg85fxy8TUvdEdxgxLA0g== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0NWZPWXltTVNXNGxPd0hZ + R0U4VzN5WlI0WWZrRVVFMmpnckpMMkREaTBvCm54eTZtZlZzRVpwRmg4Ulp0VG5w + VnJkc29nN0VBRFR1U1J6L0RQeWlLNlkKLS0tIDJ3eTdiUWJzbURvSk1neEhyakJS + Z2MzZi8ybW1PMngyRGk4NHhIMzZsem8KZuy1TWwvkFGsAVMIEk2+bwDcsmYziUjj + Wd4wMK1XuLnJyFYPt6CwzBAPG+1LQzmYWdC9mNI00YZM6XneU3OisQ== -----END AGE ENCRYPTED FILE----- - recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhS2s4bWlZN2pjRUticDJY - eGtIVEZEVkM3c2RKVmVKQnF3Z1cxbnBzZEh3CmRFN2c0T2FjV0UyMUxKREJsUnhl - YWZ3WGJOZWptd1c2SG5pTy82djBmVXMKLS0tIC9YamwwNHV3RjNtZ25mY2NPVTRQ - a1NSUlY4cWFWYzVYdVFxVFdNQm5DZzAKKmUA1AbqsFOhpczeHtiPnOcVMVp92m// - fB+AfPQUdb2/4p87PpzE/2xUMUTgY5Eng2KaHyJHq0gh+5XKhsDi3Q== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBZEhsK0x1QkczeFIvL0JI + UWY5R252WkZvR0s2SStlWVBMQk9ENFpaRHpRClg3VjhpYW5UbzJkODRFYWF2aGpr + ajE3aUFhZStYY0NJYlg1QTZqVHJsODAKLS0tIGsyRHlXSVQyV2RXVCswRVlsbktV + c0Z5ZXhtb0wrT0Q3WU1ONjFiNk1WOVkKHxnDqJkGfiqrlAyzJHYVbJlR1/jluFU+ + hM/wENwqtlZ7RCSdG68AssgP9zukO94sV9mAtbfOdeVwXa1LU66Ncw== -----END AGE ENCRYPTED FILE----- - recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOUk80d2VXVHp6UU1sYVBz - WGY4ZmFINHVzV0lRbVdxczl4MjVWbWRMR21JCkJSVVV1b3RPZnBnUlF5N0RsRkZO - cDRqYTFPRm5lUkhRUnVTQ0hCVXRVancKLS0tIFB5SWw1L1Q5NWROZk1ucE5nZjRt - QUdNcjB4OHNNcENpWnJXTEw5K0ZqcFEKlO7SN3jy8KUCjcO1vYLo4INsNlLi9s7H - mMUbt+4kwruhY8gN3UB0ATDAD2MpcxprdfZEq7swxtxsWOLA+IpcXQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5TG9wT2JHN2pOVjRueUF2 + UGJkM2d5VFpLT0hKVmIwV2Qva25ubk1lK0ZBCkJiNWpuZ3grQ0lkSDlCMDBwYjRR + cDlPVHhtWlpnaVFYMFJqWWY2ZVFGNncKLS0tIFZQVVRSQXVOZnNDOHVwTHBraUx3 + MVRVRlRQMFcyelNvL3FaNjc3U3VYbmsKZ+rJ/EFb3KNyyJ5hqO/wV4AtO1FJCeB/ + oazkDDoFBE+uhiLmdCy41eYkqW8Owt/zrO29nITeJ5EtGAXTbACcgg== -----END AGE ENCRYPTED FILE----- - recipient: age1jyeppc8yl2twnv8fwcewutd5gjewnxl59lmhev6ygds9qel8zf8syt7zz4 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2LzBJVk5KcjdVWng3azU5 - N0dNQzRWcmlQMnRzWXk0MmZrK2ltbnBDMkJrCnpmenBlUExLOEtaM1gzdUg0RW9T - Z3dDcVRqVmU1WXg1eWVDaGlLdjRSRGsKLS0tIE5hYVNkWHVKNWlmdGIzTDhuSStS - aTJueXRDNDlvUEZHajVHZEpyVnlVVGMKK7gUYs3D1BUeD8pH81iy7Hoc0VjCCYCq - PAnweggfzOVvZj8YHUBZ6/kfAODdjQi/16B9yBR6A0K499/+FGeazg== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDZlJYSG51NEE3emlTVDM0 + WEE4LzFqazdZQkRZSUlqQ0dzYURkbWc5RWxnCnJobm5LVnkxZkFIeTNWWUJvOUFU + SlZhZDBsdHhDRzFVQjhsN3F1dE9SVDAKLS0tIFBlOEwxallncjBxWDZCSkhZdlJN + b21icTBmeFM1cnVkaXAySHFzam1hYmcKULP2EuMGhspSusYPZs/DTksaZb0Asfel + mVn9Unqe2b9tT5cchGrxLiDJ+2YvfTA0s/JpDtLN+MpiRQQl0vJikg== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-04-22T20:20:18Z" - mac: ENC[AES256_GCM,data:lmtkTa+zts+gA9HPRrfCCzlj3TvDL7ROf6+OmPIPHx+e7yIeLXuvDDGlEATkVLc3CfetdFpd0cMOb5UYixqqE75ivNxZHwh+g3qwHAdmNP2NtjWTkTi1fSPjuuwSWG6e1lHCmX5SS/bmnnT/bfCRCDruyVtm766d7iWicLuGq1M=,iv:jBTDksnZRJrV0jJ8QccK8Ov5lAPf+dfSQ6D88icUMXQ=,tag:zlfequv/RHz1Y21uMvwseQ==,type:str] + lastmodified: "2024-07-08T11:20:50Z" + mac: ENC[AES256_GCM,data:GPUwpSAz6fj7mRxX1ebEb2sLAMLkQLuKPXk+B3+zZmA6+D7gAKrrBGUWHqYA9DMMY0r32OZSccGRmeKqdA7sWmzdIJTcBu8EyER1nJqVFJiXcOOdTkCLdOM4xW969YE0lBKpIAQ40E7YXYYwkI1JINneIBTuXkvIBmSQ3Bt2+ak=,iv:VEPNQxDLzxyTxkn8dI6xNDe9ESk2RojSNYYEwT+Ggas=,tag:cfUEKU3arSJl+lEOa+4iRA==,type:str] unencrypted_suffix: _unencrypted - version: 3.12.1 + version: 3.8.1 diff --git a/hosts/web-arm/configuration.nix b/hosts/web-arm/configuration.nix index 79ba795..17927db 100644 --- a/hosts/web-arm/configuration.nix +++ b/hosts/web-arm/configuration.nix @@ -20,6 +20,7 @@ ./modules/blackbox-exporter.nix ./modules/updns.nix ./modules/atticd.nix + ./modules/supabase ./utils/modules/autoupgrade.nix ./utils/modules/promtail @@ -42,7 +43,6 @@ ./modules/scana11y.nix ./modules/wireguard.nix - ./modules/fueltide-backup ]; nixpkgs.overlays = [ diff --git a/hosts/web-arm/modules/fueltide-backup/RESTORATION.md b/hosts/web-arm/modules/fueltide-backup/RESTORATION.md deleted file mode 100644 index 400e2fa..0000000 --- a/hosts/web-arm/modules/fueltide-backup/RESTORATION.md +++ /dev/null @@ -1,129 +0,0 @@ -# Fueltide Supabase Restoration Runbook - -Use this when the upstream Supabase project at `majxbigjafpzayzboxsf.supabase.co` is gone, broken, or you want to move to a new project. - -## What this backup covers - -The nightly `fueltide-backup.service` on `web-arm` produces three SQL files per run under `/var/backup/fueltide-supabase//`: - -- `roles.sql` — cluster roles (via `pg_dumpall --roles-only --no-role-passwords`) -- `schema.sql` — DDL: tables, functions, triggers, RLS policies, views, extensions, types (via `pg_dump --schema-only`) -- `data.sql` — all row data, including `auth.users`, `auth.identities`, `storage.objects` metadata (via `pg_dump --data-only`) -- `sha256.txt` — checksums for verification - -These files are included in the nightly borgbackup run (03:00 UTC) and shipped to the Hetzner Storage Box at `u149513-sub8`. - -## What this backup does **not** cover - -- **Supabase Edge Functions** — lives in the `fueltide` app repo, deployed via `supabase functions deploy`. No action needed beyond redeploying from source. -- **Storage bucket files** — not in use for this project (only DB-backed data). -- **Control-plane settings** — auth providers, SMTP, email templates, API keys. These live in Supabase's dashboard, not the database. Must be reapplied manually (steps below). - ---- - -## Restoration steps - -### 1. Provision a fresh Supabase project - -Dashboard → New project. Use the same region (`eu-west-1`). Record: -- New **project ref** (20-char subdomain) -- New **database password** -- New **session pooler hostname** (Project Settings → Database → Connection string → Session pooler) — the cluster prefix (`aws-1-`, `aws-0-`, etc.) may differ from the old project. - -### 2. Fetch the latest dump from borg - -From `web-arm.cloonar.com`: - -```bash -borg-list # find newest archive, e.g. web-arm-2026-04-24 -mkdir -p /mnt/borg -borg-mount web-arm-2026-04-24 /mnt/borg -ls /mnt/borg/var/backup/fueltide-supabase/ # pick newest timestamped directory -cp -r /mnt/borg/var/backup/fueltide-supabase/ /tmp/restore -borg umount /mnt/borg - -cd /tmp/restore -sha256sum -c sha256.txt # verify integrity -``` - -If `web-arm` itself is lost, fetch from any machine with the borg SSH key + passphrase (secrets are in sops under `borg-ssh-key` / `borg-passphrase`). - -### 3. Restore the database - -```bash -export NEW_URL="postgres://postgres.:@:5432/postgres" - -# roles (some will error because Supabase-managed roles already exist — safe to ignore) -psql "$NEW_URL" -f /tmp/restore/roles.sql || true - -# schema -psql "$NEW_URL" -f /tmp/restore/schema.sql - -# data -psql "$NEW_URL" -f /tmp/restore/data.sql -``` - -Expected noise that is safe to ignore: -- `role "supabase_admin" already exists`, same for `authenticator`, `service_role`, `anon`, `authenticated`, `dashboard_user` -- `extension "pg_graphql" already exists` (if schema uses `CREATE EXTENSION` without `IF NOT EXISTS` for any extension not pre-installed — rare) -- `schema "auth" already exists` - -Stop and investigate if you see errors like `permission denied`, `syntax error`, or `duplicate key value`. - -### 4. Redeploy Edge Functions from the app repo - -From a checkout of the fueltide app repo: - -```bash -supabase link --project-ref -supabase functions deploy # deploys all functions in supabase/functions/ -``` - -If specific function secrets are configured (via `supabase secrets set`), re-set them from the app repo's documented env values. - -### 5. Reapply dashboard-only settings - -These live in Supabase's control plane and are **not** in any dump: - -| Setting | Location | Notes | -|---|---|---| -| Google OAuth provider | Authentication → Providers → Google | Client ID + secret from SOPS (commit `67e81d3` added these) | -| Apple OAuth provider | Authentication → Providers → Apple | Services ID + Team ID + Key ID + P8 key from SOPS | -| SMTP settings | Authentication → SMTP Settings | Sender `noreply@fueltide.io`, use the mail host's SMTP creds | -| Email templates | Authentication → Email Templates | Fueltide-branded magic link, confirm, recovery — bodies in commit `67e81d3` | -| API keys | Project Settings → API | A **new** `anon` and `service_role` are generated per project — copy them | - -### 6. Update app clients - -Update the iOS app (and any server-side callers) with: - -- `SUPABASE_URL = https://.supabase.co` -- `SUPABASE_ANON_KEY = ` -- `SUPABASE_SERVICE_ROLE_KEY = ` (server-side only) - -Update CSP in `hosts/web-arm/sites/fueltide.io.nix` (currently commented out, references `*.supabase.co`) if you reinstate it. - -### 7. Smoke test - -- Sign up + sign in via email magic link (confirms SMTP + email templates) -- Sign in via Google (confirms OAuth provider) -- Sign in via Apple (confirms OAuth provider) -- Read a known row from the largest app table (confirms data restored, RLS intact) -- Insert + read back a new row (confirms writes work) -- Call an edge function (confirms functions redeployed) - -### 8. Update this backup service to point at the new project - -Edit `hosts/web-arm/modules/fueltide-backup/default.nix`: - -- Set `project = ""` -- Set `poolerHost = ""` (the region + cluster may differ) -- If the new project is on a different Postgres major version, update `pg = pkgs.postgresql_XX` - -Rotate the `fueltide-supabase-db-password` secret in `hosts/web-arm/secrets.yaml` via: - -```bash -nix-shell -p sops --run 'sops hosts/web-arm/secrets.yaml' -``` - -Deploy, then run `systemctl start fueltide-backup.service` manually on `web-arm` and verify a new dump lands under `/var/backup/fueltide-supabase/`. diff --git a/hosts/web-arm/modules/fueltide-backup/default.nix b/hosts/web-arm/modules/fueltide-backup/default.nix deleted file mode 100644 index 7680f4d..0000000 --- a/hosts/web-arm/modules/fueltide-backup/default.nix +++ /dev/null @@ -1,64 +0,0 @@ -{ config, pkgs, ... }: - -let - project = "majxbigjafpzayzboxsf"; - poolerHost = "aws-1-eu-west-1.pooler.supabase.com"; - outDir = "/var/backup/fueltide-supabase"; - # retain local dumps for this many days; borg handles offsite retention - retainDays = 1; - # match the upstream Supabase Postgres major version - pg = pkgs.postgresql_17; -in { - sops.secrets.fueltide-supabase-db-password = { }; - - systemd.tmpfiles.rules = [ "d ${outDir} 0700 root root -" ]; - - systemd.services.fueltide-backup = { - description = "Dump upstream Supabase database for ${project}"; - path = [ pg pkgs.coreutils pkgs.findutils ]; - serviceConfig = { - Type = "oneshot"; - User = "root"; - LoadCredential = "db-password:${config.sops.secrets.fueltide-supabase-db-password.path}"; - }; - script = '' - set -euo pipefail - - export PGPASSWORD - PGPASSWORD=$(cat "$CREDENTIALS_DIRECTORY/db-password") - export PGHOST="${poolerHost}" - export PGPORT=5432 - export PGUSER="postgres.${project}" - export PGDATABASE=postgres - - TS=$(date -u +%Y%m%dT%H%M%SZ) - OUT="${outDir}/$TS" - mkdir -p "$OUT" - chmod 700 "$OUT" - - # cluster roles (Supabase-managed roles already exist on a fresh project; - # restore errors for those are expected and benign) - pg_dumpall --roles-only --no-role-passwords > "$OUT/roles.sql" - - # schema: tables, functions, triggers, RLS policies, views, extensions - pg_dump --schema-only --no-owner --no-privileges > "$OUT/schema.sql" - - # data: all rows (includes auth.users, storage.objects metadata, etc.) - pg_dump --data-only --no-owner > "$OUT/data.sql" - - ( cd "$OUT" && sha256sum *.sql > sha256.txt ) - - find "${outDir}" -mindepth 1 -maxdepth 1 -type d \ - -mtime +${toString retainDays} -exec rm -rf {} + - ''; - }; - - systemd.timers.fueltide-backup = { - wantedBy = [ "timers.target" ]; - timerConfig = { - OnCalendar = "*-*-* 02:30:00"; - Persistent = true; - RandomizedDelaySec = "10m"; - }; - }; -} diff --git a/hosts/web-arm/modules/supabase/default.nix b/hosts/web-arm/modules/supabase/default.nix new file mode 100644 index 0000000..4519e7e --- /dev/null +++ b/hosts/web-arm/modules/supabase/default.nix @@ -0,0 +1,452 @@ +{ config, lib, pkgs, ... }: + +let + kongEntrypoint = pkgs.writeTextFile { + name = "kong-entrypoint.sh"; + executable = true; + text = builtins.readFile ./kong-entrypoint.sh; + }; + + envGenerateScript = pkgs.writeShellScript "supabase-env-generate" + (builtins.readFile ./env-generate.sh); + + # Common extra options for all containers to join the supabase network + supabaseNet = [ "--network=supabase-net" ]; + +in +{ + # --- SOPS secret --- + sops.secrets.supabase-env = { }; + + # --- Persistent data directories --- + systemd.tmpfiles.rules = [ + "d /var/lib/supabase/db/data 0700 root root -" + "d /var/lib/supabase/storage 0755 root root -" + "d /var/lib/supabase/functions 0755 root root -" + "d /var/lib/supabase/snippets 0755 root root -" + ]; + + + # --- Systemd services: network, env generation, and container ordering --- + systemd.services = + let + containerNames = [ + "supabase-db" + "supabase-analytics" + "supabase-auth" + "supabase-rest" + "supabase-realtime" + "supabase-storage" + "supabase-imgproxy" + "supabase-meta" + "supabase-studio" + "supabase-kong" + "supabase-vector" + "supabase-pooler" + "supabase-functions" + ]; + mkContainerDeps = name: { + "podman-${name}" = { + after = [ "init-supabase-network.service" "supabase-env-generate.service" ]; + requires = [ "init-supabase-network.service" "supabase-env-generate.service" ]; + }; + }; + in + lib.mkMerge (map mkContainerDeps containerNames ++ [ + { + init-supabase-network = { + description = "Create supabase-net Podman network"; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + # '-' prefix tells systemd to ignore non-zero exit (network may already exist) + ExecStart = "-${pkgs.podman}/bin/podman network create supabase-net"; + }; + }; + supabase-env-generate = { + description = "Generate Supabase per-container env files from SOPS secrets"; + wantedBy = [ "multi-user.target" ]; + path = [ pkgs.jq ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "${envGenerateScript} ${config.sops.secrets.supabase-env.path}"; + }; + }; + # Seed the edge-runtime's bootstrap `main` function. The container's + # entrypoint requires `/home/deno/functions/main/index.ts` to exist; + # without it edge-runtime fails with "could not find an appropriate + # entrypoint". Re-seed on every activation so updates to the bootstrap + # are picked up, while leaving user-authored functions untouched. + supabase-functions-seed = { + description = "Seed Supabase edge-functions main bootstrap"; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + install -d -m 0755 /var/lib/supabase/functions/main + install -m 0644 ${./functions/main/index.ts} /var/lib/supabase/functions/main/index.ts + ''; + }; + podman-supabase-functions = { + after = [ "supabase-functions-seed.service" ]; + requires = [ "supabase-functions-seed.service" ]; + }; + } + ]); + + # --- Containers --- + virtualisation.oci-containers.containers = { + + # 1. PostgreSQL + supabase-db = { + image = "supabase/postgres:15.8.1.085"; + environment = { + POSTGRES_HOST = "/var/run/postgresql"; + PGPORT = "5432"; + POSTGRES_PORT = "5432"; + PGDATABASE = "postgres"; + POSTGRES_DB = "postgres"; + JWT_EXP = "3600"; + }; + environmentFiles = [ "/run/supabase/db.env" ]; + volumes = [ + "/var/lib/supabase/db/data:/var/lib/postgresql/data" + "${./sql/_supabase.sql}:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:ro" + "${./sql/realtime.sql}:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:ro" + "${./sql/logs.sql}:/docker-entrypoint-initdb.d/migrations/99-logs.sql:ro" + "${./sql/pooler.sql}:/docker-entrypoint-initdb.d/migrations/99-pooler.sql:ro" + "${./sql/webhooks.sql}:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:ro" + "${./sql/roles.sql}:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:ro" + "${./sql/jwt.sql}:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:ro" + "supabase-db-config:/etc/postgresql-custom" + ]; + cmd = [ + "postgres" + "-c" "config_file=/etc/postgresql/postgresql.conf" + "-c" "log_min_messages=fatal" + ]; + extraOptions = supabaseNet ++ [ + "--network-alias=db" + "--shm-size=2g" + ]; + }; + + # 2. Analytics (Logflare) + supabase-analytics = { + image = "supabase/logflare:1.31.2"; + dependsOn = [ "supabase-db" ]; + environment = { + LOGFLARE_NODE_HOST = "127.0.0.1"; + DB_USERNAME = "supabase_admin"; + DB_DATABASE = "_supabase"; + DB_HOSTNAME = "db"; + DB_PORT = "5432"; + DB_SCHEMA = "_analytics"; + LOGFLARE_SINGLE_TENANT = "true"; + LOGFLARE_SUPABASE_MODE = "true"; + POSTGRES_BACKEND_SCHEMA = "_analytics"; + LOGFLARE_FEATURE_FLAG_OVERRIDE = "multibackend=true"; + }; + environmentFiles = [ "/run/supabase/analytics.env" ]; + extraOptions = supabaseNet ++ [ + "--network-alias=analytics" + ]; + }; + + # 3. Auth (GoTrue) + supabase-auth = { + image = "supabase/gotrue:v2.186.0"; + dependsOn = [ "supabase-db" "supabase-analytics" ]; + environment = { + GOTRUE_API_HOST = "0.0.0.0"; + GOTRUE_API_PORT = "9999"; + API_EXTERNAL_URL = "https://supabase.cloonar.com"; + GOTRUE_DB_DRIVER = "postgres"; + GOTRUE_SITE_URL = "https://supabase.cloonar.com"; + GOTRUE_URI_ALLOW_LIST = ""; + GOTRUE_DISABLE_SIGNUP = "false"; + GOTRUE_JWT_ADMIN_ROLES = "service_role"; + GOTRUE_JWT_AUD = "authenticated"; + GOTRUE_JWT_DEFAULT_GROUP_NAME = "authenticated"; + GOTRUE_JWT_EXP = "3600"; + GOTRUE_EXTERNAL_EMAIL_ENABLED = "true"; + GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED = "false"; + GOTRUE_MAILER_AUTOCONFIRM = "true"; + GOTRUE_SMTP_ADMIN_EMAIL = "admin@cloonar.com"; + GOTRUE_SMTP_HOST = "supabase-mail"; + GOTRUE_SMTP_PORT = "2500"; + GOTRUE_SMTP_USER = ""; + GOTRUE_SMTP_PASS = ""; + GOTRUE_SMTP_SENDER_NAME = "Supabase"; + GOTRUE_MAILER_URLPATHS_INVITE = "/auth/v1/verify"; + GOTRUE_MAILER_URLPATHS_CONFIRMATION = "/auth/v1/verify"; + GOTRUE_MAILER_URLPATHS_RECOVERY = "/auth/v1/verify"; + GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE = "/auth/v1/verify"; + GOTRUE_EXTERNAL_PHONE_ENABLED = "false"; + GOTRUE_SMS_AUTOCONFIRM = "false"; + }; + environmentFiles = [ "/run/supabase/auth.env" ]; + extraOptions = supabaseNet ++ [ + "--network-alias=auth" + ]; + }; + + # 4. REST (PostgREST) + supabase-rest = { + image = "postgrest/postgrest:v14.6"; + dependsOn = [ "supabase-db" ]; + environment = { + PGRST_DB_SCHEMAS = "public,storage,graphql_public"; + PGRST_DB_MAX_ROWS = "1000"; + PGRST_DB_EXTRA_SEARCH_PATH = "public"; + PGRST_DB_ANON_ROLE = "anon"; + PGRST_DB_USE_LEGACY_GUCS = "false"; + PGRST_APP_SETTINGS_JWT_EXP = "3600"; + }; + environmentFiles = [ "/run/supabase/rest.env" ]; + cmd = [ "postgrest" ]; + extraOptions = supabaseNet ++ [ + "--network-alias=rest" + ]; + }; + + # 5. Realtime + supabase-realtime = { + image = "supabase/realtime:v2.76.5"; + dependsOn = [ "supabase-db" ]; + environment = { + PORT = "4000"; + DB_HOST = "db"; + DB_PORT = "5432"; + DB_USER = "supabase_admin"; + DB_NAME = "postgres"; + DB_AFTER_CONNECT_QUERY = "SET search_path TO _realtime"; + DB_ENC_KEY = "supabaserealtime"; + ERL_AFLAGS = "-proto_dist inet_tcp"; + DNS_NODES = "''"; + RLIMIT_NOFILE = "10000"; + APP_NAME = "realtime"; + SEED_SELF_HOST = "true"; + RUN_JANITOR = "true"; + DISABLE_HEALTHCHECK_LOGGING = "true"; + }; + environmentFiles = [ "/run/supabase/realtime.env" ]; + extraOptions = supabaseNet ++ [ + # Hostname must be realtime-dev.supabase-realtime for tenant ID parsing + "--hostname=realtime-dev.supabase-realtime" + "--network-alias=realtime-dev.supabase-realtime" + ]; + }; + + # 6. Storage + supabase-storage = { + image = "supabase/storage-api:v1.44.2"; + dependsOn = [ "supabase-db" "supabase-rest" "supabase-imgproxy" ]; + environment = { + POSTGREST_URL = "http://rest:3000"; + STORAGE_PUBLIC_URL = "https://supabase.cloonar.com"; + REQUEST_ALLOW_X_FORWARDED_PATH = "true"; + FILE_SIZE_LIMIT = "52428800"; + STORAGE_BACKEND = "file"; + GLOBAL_S3_BUCKET = "stub"; + FILE_STORAGE_BACKEND_PATH = "/var/lib/storage"; + TENANT_ID = "stub"; + REGION = "stub"; + ENABLE_IMAGE_TRANSFORMATION = "true"; + IMGPROXY_URL = "http://imgproxy:5001"; + }; + environmentFiles = [ "/run/supabase/storage.env" ]; + volumes = [ + "/var/lib/supabase/storage:/var/lib/storage" + ]; + extraOptions = supabaseNet ++ [ + "--network-alias=storage" + ]; + }; + + # 7. Imgproxy + supabase-imgproxy = { + image = "darthsim/imgproxy:v3.30.1"; + environment = { + IMGPROXY_BIND = ":5001"; + IMGPROXY_LOCAL_FILESYSTEM_ROOT = "/"; + IMGPROXY_USE_ETAG = "true"; + IMGPROXY_AUTO_WEBP = "true"; + IMGPROXY_MAX_SRC_RESOLUTION = "16.8"; + }; + volumes = [ + "/var/lib/supabase/storage:/var/lib/storage" + ]; + extraOptions = supabaseNet ++ [ + "--network-alias=imgproxy" + ]; + }; + + # 8. Meta (pg-meta) + supabase-meta = { + image = "supabase/postgres-meta:v0.95.2"; + dependsOn = [ "supabase-db" ]; + environment = { + PG_META_PORT = "8080"; + PG_META_DB_HOST = "db"; + PG_META_DB_PORT = "5432"; + PG_META_DB_NAME = "postgres"; + PG_META_DB_USER = "supabase_admin"; + }; + environmentFiles = [ "/run/supabase/meta.env" ]; + extraOptions = supabaseNet ++ [ + "--network-alias=meta" + ]; + }; + + # 9. Studio + supabase-studio = { + image = "supabase/studio:2026.03.16-sha-5528817"; + dependsOn = [ "supabase-analytics" ]; + environment = { + HOSTNAME = "::"; + STUDIO_PG_META_URL = "http://meta:8080"; + POSTGRES_PORT = "5432"; + POSTGRES_HOST = "db"; + POSTGRES_DB = "postgres"; + PGRST_DB_SCHEMAS = "public,storage,graphql_public"; + PGRST_DB_MAX_ROWS = "1000"; + PGRST_DB_EXTRA_SEARCH_PATH = "public"; + DEFAULT_ORGANIZATION_NAME = "Default Organization"; + DEFAULT_PROJECT_NAME = "Default Project"; + SUPABASE_URL = "http://kong:8000"; + SUPABASE_PUBLIC_URL = "https://supabase.cloonar.com"; + NEXT_PUBLIC_ENABLE_LOGS = "true"; + NEXT_ANALYTICS_BACKEND_PROVIDER = "postgres"; + LOGFLARE_URL = "http://analytics:4000"; + SNIPPETS_MANAGEMENT_FOLDER = "/app/snippets"; + EDGE_FUNCTIONS_MANAGEMENT_FOLDER = "/app/edge-functions"; + }; + environmentFiles = [ "/run/supabase/studio.env" ]; + volumes = [ + "/var/lib/supabase/snippets:/app/snippets" + "/var/lib/supabase/functions:/app/edge-functions" + ]; + extraOptions = supabaseNet ++ [ + "--network-alias=studio" + ]; + }; + + # 10. Kong (API Gateway) + supabase-kong = { + image = "kong/kong:3.9.1"; + dependsOn = [ "supabase-studio" ]; + environment = { + KONG_DATABASE = "off"; + KONG_DECLARATIVE_CONFIG = "/usr/local/kong/kong.yml"; + KONG_DNS_ORDER = "LAST,A,CNAME"; + KONG_DNS_NOT_FOUND_TTL = "1"; + KONG_PLUGINS = "request-transformer,cors,key-auth,acl,basic-auth,request-termination,ip-restriction,post-function"; + KONG_NGINX_PROXY_PROXY_BUFFER_SIZE = "160k"; + KONG_NGINX_PROXY_PROXY_BUFFERS = "64 160k"; + KONG_PROXY_ACCESS_LOG = "/dev/stdout combined"; + }; + environmentFiles = [ "/run/supabase/kong.env" ]; + ports = [ + "127.0.0.1:8000:8000" + "127.0.0.1:8443:8443" + ]; + volumes = [ + "${./kong.yml}:/home/kong/temp.yml:ro" + "${kongEntrypoint}:/home/kong/kong-entrypoint.sh:ro" + ]; + entrypoint = "/home/kong/kong-entrypoint.sh"; + extraOptions = supabaseNet ++ [ + "--network-alias=kong" + ]; + }; + + # 11. Vector (log collection) + supabase-vector = { + image = "timberio/vector:0.53.0-alpine"; + environment = { }; + environmentFiles = [ "/run/supabase/vector.env" ]; + volumes = [ + "${./vector.yml}:/etc/vector/vector.yml:ro" + "/var/run/docker.sock:/var/run/docker.sock:ro" + ]; + cmd = [ "--config" "/etc/vector/vector.yml" ]; + extraOptions = supabaseNet ++ [ + "--network-alias=vector" + "--security-opt=label=disable" + ]; + }; + + # 12. Pooler (Supavisor) + supabase-pooler = { + image = "supabase/supavisor:2.7.4"; + dependsOn = [ "supabase-db" ]; + environment = { + PORT = "4000"; + CLUSTER_POSTGRES = "true"; + REGION = "local"; + ERL_AFLAGS = "-proto_dist inet_tcp"; + POOLER_POOL_MODE = "transaction"; + POSTGRES_PORT = "5432"; + POSTGRES_DB = "postgres"; + POOLER_TENANT_ID = "default-tenant"; + POOLER_DEFAULT_POOL_SIZE = "20"; + POOLER_MAX_CLIENT_CONN = "100"; + DB_POOL_SIZE = "10"; + }; + environmentFiles = [ "/run/supabase/pooler.env" ]; + volumes = [ + "${./pooler.exs}:/etc/pooler/pooler.exs:ro" + ]; + cmd = [ + "/bin/sh" "-c" + "/app/bin/migrate && /app/bin/supavisor eval \"$(cat /etc/pooler/pooler.exs)\" && /app/bin/server" + ]; + extraOptions = supabaseNet ++ [ + "--network-alias=pooler" + ]; + }; + + # 13. Edge Functions + supabase-functions = { + image = "supabase/edge-runtime:v1.71.2"; + dependsOn = [ "supabase-kong" ]; + environment = { + SUPABASE_URL = "http://kong:8000"; + SUPABASE_PUBLIC_URL = "https://supabase.cloonar.com"; + VERIFY_JWT = "false"; + }; + environmentFiles = [ "/run/supabase/functions.env" ]; + volumes = [ + "/var/lib/supabase/functions:/home/deno/functions" + "supabase-deno-cache:/root/.cache/deno" + ]; + cmd = [ "start" "--main-service" "/home/deno/functions/main" ]; + extraOptions = supabaseNet ++ [ + "--network-alias=functions" + ]; + }; + }; + + # --- Nginx reverse proxy --- + services.nginx.virtualHosts."supabase.cloonar.com" = { + forceSSL = true; + enableACME = true; + acmeRoot = null; + locations."/" = { + proxyPass = "http://127.0.0.1:8000"; + proxyWebsockets = true; + extraConfig = '' + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + client_max_body_size 50M; + ''; + }; + }; +} diff --git a/hosts/web-arm/modules/supabase/env-generate.sh b/hosts/web-arm/modules/supabase/env-generate.sh new file mode 100644 index 0000000..ecf4f1b --- /dev/null +++ b/hosts/web-arm/modules/supabase/env-generate.sh @@ -0,0 +1,92 @@ +set -euo pipefail +umask 077 +mkdir -p /run/supabase + +set -a +source "$1" +set +a + +# URL-encode password for use in connection strings +PG_PASS_ENCODED=$(printf '%s' "$POSTGRES_PASSWORD" | jq -sRr @uri) + +cat > /run/supabase/db.env < /run/supabase/analytics.env < /run/supabase/auth.env < /run/supabase/rest.env < /run/supabase/realtime.env < /run/supabase/storage.env < /run/supabase/meta.env < /run/supabase/studio.env < /run/supabase/kong.env < /run/supabase/vector.env < /run/supabase/pooler.env < /run/supabase/functions.env < | null = null +if (SUPABASE_URL) { + try { + SUPABASE_JWT_KEYS = jose.createRemoteJWKSet( + new URL('/auth/v1/.well-known/jwks.json', SUPABASE_URL) + ) + } catch (e) { + console.error('Failed to fetch JWKS from SUPABASE_URL:', e) + } +} + +function getAuthToken(req: Request) { + const authHeader = req.headers.get('authorization') + if (!authHeader) { + throw new Error('Missing authorization header') + } + const [bearer, token] = authHeader.split(' ') + if (bearer !== 'Bearer') { + throw new Error(`Auth header is not 'Bearer {token}'`) + } + return token +} + +async function isValidLegacyJWT(jwt: string): Promise { + if (!JWT_SECRET) { + console.error('JWT_SECRET not available for HS256 token verification') + return false + } + + const encoder = new TextEncoder(); + const secretKey = encoder.encode(JWT_SECRET) + + try { + await jose.jwtVerify(jwt, secretKey); + } catch (e) { + console.error('Symmetric Legacy JWT verification error', e); + return false; + } + return true; +} + +async function isValidJWT(jwt: string): Promise { + if (!SUPABASE_JWT_KEYS) { + console.error('JWKS not available for ES256/RS256 token verification') + return false + } + + try { + await jose.jwtVerify(jwt, SUPABASE_JWT_KEYS) + } catch (e) { + console.error('Asymmetric JWT verification error', e); + return false + } + + return true; +} + +async function isValidHybridJWT(jwt: string): Promise { + const { alg: jwtAlgorithm } = jose.decodeProtectedHeader(jwt) + + if (jwtAlgorithm === 'HS256') { + console.log(`Legacy token type detected, attempting ${jwtAlgorithm} verification.`) + + return await isValidLegacyJWT(jwt) + } + + if (jwtAlgorithm === 'ES256' || jwtAlgorithm === 'RS256') { + return await isValidJWT(jwt) + } + + return false; +} + +Deno.serve(async (req: Request) => { + if (req.method !== 'OPTIONS' && VERIFY_JWT) { + try { + const token = getAuthToken(req) + const isValidJWT = await isValidHybridJWT(token); + + if (!isValidJWT) { + return new Response(JSON.stringify({ msg: 'Invalid JWT' }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }) + } + } catch (e) { + console.error(e) + return new Response(JSON.stringify({ msg: e.toString() }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }) + } + } + + const url = new URL(req.url) + const { pathname } = url + const path_parts = pathname.split('/') + const service_name = path_parts[1] + + if (!service_name || service_name === '') { + const error = { msg: 'missing function name in request' } + return new Response(JSON.stringify(error), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }) + } + + const servicePath = `/home/deno/functions/${service_name}` + console.error(`serving the request with ${servicePath}`) + + const memoryLimitMb = 150 + const workerTimeoutMs = 1 * 60 * 1000 + const noModuleCache = false + const importMapPath = null + const envVarsObj = Deno.env.toObject() + const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]]) + + try { + const worker = await EdgeRuntime.userWorkers.create({ + servicePath, + memoryLimitMb, + workerTimeoutMs, + noModuleCache, + importMapPath, + envVars, + }) + return await worker.fetch(req) + } catch (e) { + const error = { msg: e.toString() } + return new Response(JSON.stringify(error), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }) + } +}) diff --git a/hosts/web-arm/modules/supabase/kong-entrypoint.sh b/hosts/web-arm/modules/supabase/kong-entrypoint.sh new file mode 100644 index 0000000..f1da449 --- /dev/null +++ b/hosts/web-arm/modules/supabase/kong-entrypoint.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Legacy API keys, not sb_ API keys -> pass apikey through unchanged +export LUA_AUTH_EXPR="\$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or headers.apikey)" +export LUA_RT_WS_EXPR="\$(query_params.apikey)" + +# Substitute environment variables in the Kong declarative config +awk '{ + result = "" + rest = $0 + while (match(rest, /\$[A-Za-z_][A-Za-z_0-9]*/)) { + varname = substr(rest, RSTART + 1, RLENGTH - 1) + if (varname in ENVIRON) { + result = result substr(rest, 1, RSTART - 1) ENVIRON[varname] + } else { + result = result substr(rest, 1, RSTART + RLENGTH - 1) + } + rest = substr(rest, RSTART + RLENGTH) + } + print result rest +}' /home/kong/temp.yml > "$KONG_DECLARATIVE_CONFIG" + +# Remove empty key-auth credentials (unconfigured opaque keys) +sed -i '/^[[:space:]]*- key:[[:space:]]*$/d' "$KONG_DECLARATIVE_CONFIG" + +exec /entrypoint.sh kong docker-start diff --git a/hosts/web-arm/modules/supabase/kong.yml b/hosts/web-arm/modules/supabase/kong.yml new file mode 100644 index 0000000..52af820 --- /dev/null +++ b/hosts/web-arm/modules/supabase/kong.yml @@ -0,0 +1,265 @@ +_format_version: '2.1' +_transform: true + +consumers: + - username: DASHBOARD + - username: anon + keyauth_credentials: + - key: $SUPABASE_ANON_KEY + - username: service_role + keyauth_credentials: + - key: $SUPABASE_SERVICE_KEY + +acls: + - consumer: anon + group: anon + - consumer: service_role + group: admin + +basicauth_credentials: + - consumer: DASHBOARD + username: '$DASHBOARD_USERNAME' + password: '$DASHBOARD_PASSWORD' + +services: + - name: auth-v1-open + url: http://auth:9999/verify + routes: + - name: auth-v1-open + strip_path: true + paths: + - /auth/v1/verify + plugins: + - name: cors + - name: auth-v1-open-callback + url: http://auth:9999/callback + routes: + - name: auth-v1-open-callback + strip_path: true + paths: + - /auth/v1/callback + plugins: + - name: cors + - name: auth-v1-open-authorize + url: http://auth:9999/authorize + routes: + - name: auth-v1-open-authorize + strip_path: true + paths: + - /auth/v1/authorize + plugins: + - name: cors + - name: auth-v1-open-jwks + url: http://auth:9999/.well-known/jwks.json + routes: + - name: auth-v1-open-jwks + strip_path: true + paths: + - /auth/v1/.well-known/jwks.json + plugins: + - name: cors + - name: auth-v1 + url: http://auth:9999/ + routes: + - name: auth-v1-all + strip_path: true + paths: + - /auth/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + - name: rest-v1 + url: http://rest:3000/ + routes: + - name: rest-v1-all + strip_path: true + paths: + - /rest/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + - name: graphql-v1 + url: http://rest:3000/rpc/graphql + routes: + - name: graphql-v1-all + strip_path: true + paths: + - /graphql/v1 + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "Content-Profile: graphql_public" + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + - name: realtime-v1-ws + url: http://realtime-dev.supabase-realtime:4000/socket + protocol: ws + routes: + - name: realtime-v1-ws + strip_path: true + paths: + - /realtime/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "x-api-key:$LUA_RT_WS_EXPR" + replace: + querystring: + - "apikey:$LUA_RT_WS_EXPR" + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + - name: realtime-v1-rest + url: http://realtime-dev.supabase-realtime:4000/api + protocol: http + routes: + - name: realtime-v1-rest + strip_path: true + paths: + - /realtime/v1/api + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + - name: storage-v1 + url: http://storage:5000/ + routes: + - name: storage-v1-all + strip_path: true + paths: + - /storage/v1/ + plugins: + - name: cors + - name: request-transformer + config: + add: + headers: + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" + - name: post-function + config: + access: + - | + local auth = kong.request.get_header("authorization") + if auth == nil or auth == "" or auth:find("^%s*$") then + kong.service.request.clear_header("authorization") + end + - name: functions-v1 + url: http://functions:9000/ + read_timeout: 150000 + routes: + - name: functions-v1-all + strip_path: true + paths: + - /functions/v1/ + plugins: + - name: cors + - name: well-known-oauth + url: http://auth:9999/.well-known/oauth-authorization-server + routes: + - name: well-known-oauth + strip_path: true + paths: + - /.well-known/oauth-authorization-server + plugins: + - name: cors + - name: meta + url: http://meta:8080/ + routes: + - name: meta-all + strip_path: true + paths: + - /pg/ + plugins: + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - name: dashboard + url: http://studio:3000/ + routes: + - name: dashboard-all + strip_path: true + paths: + - / + plugins: + - name: cors + - name: basic-auth + config: + hide_credentials: true diff --git a/hosts/web-arm/modules/supabase/pooler.exs b/hosts/web-arm/modules/supabase/pooler.exs new file mode 100644 index 0000000..791d61c --- /dev/null +++ b/hosts/web-arm/modules/supabase/pooler.exs @@ -0,0 +1,30 @@ +{:ok, _} = Application.ensure_all_started(:supavisor) + +{:ok, version} = + case Supavisor.Repo.query!("select version()") do + %{rows: [[ver]]} -> Supavisor.Helpers.parse_pg_version(ver) + _ -> nil + end + +params = %{ + "external_id" => System.get_env("POOLER_TENANT_ID"), + "db_host" => "db", + "db_port" => System.get_env("POSTGRES_PORT"), + "db_database" => System.get_env("POSTGRES_DB"), + "require_user" => false, + "auth_query" => "SELECT * FROM pgbouncer.get_auth($1)", + "default_max_clients" => System.get_env("POOLER_MAX_CLIENT_CONN"), + "default_pool_size" => System.get_env("POOLER_DEFAULT_POOL_SIZE"), + "default_parameter_status" => %{"server_version" => version}, + "users" => [%{ + "db_user" => "pgbouncer", + "db_password" => System.get_env("POSTGRES_PASSWORD"), + "mode_type" => System.get_env("POOLER_POOL_MODE"), + "pool_size" => System.get_env("POOLER_DEFAULT_POOL_SIZE"), + "is_manager" => true + }] +} + +if !Supavisor.Tenants.get_tenant_by_external_id(params["external_id"]) do + {:ok, _} = Supavisor.Tenants.create_tenant(params) +end diff --git a/hosts/web-arm/modules/supabase/sql/_supabase.sql b/hosts/web-arm/modules/supabase/sql/_supabase.sql new file mode 100644 index 0000000..8882968 --- /dev/null +++ b/hosts/web-arm/modules/supabase/sql/_supabase.sql @@ -0,0 +1,2 @@ +\set pguser `echo "$POSTGRES_USER"` +CREATE DATABASE _supabase WITH OWNER :pguser; diff --git a/hosts/web-arm/modules/supabase/sql/jwt.sql b/hosts/web-arm/modules/supabase/sql/jwt.sql new file mode 100644 index 0000000..93a8041 --- /dev/null +++ b/hosts/web-arm/modules/supabase/sql/jwt.sql @@ -0,0 +1,4 @@ +\set jwt_secret `echo "$JWT_SECRET"` +\set jwt_exp `echo "$JWT_EXP"` +ALTER DATABASE postgres SET "app.settings.jwt_secret" TO :'jwt_secret'; +ALTER DATABASE postgres SET "app.settings.jwt_exp" TO :'jwt_exp'; diff --git a/hosts/web-arm/modules/supabase/sql/logs.sql b/hosts/web-arm/modules/supabase/sql/logs.sql new file mode 100644 index 0000000..794b086 --- /dev/null +++ b/hosts/web-arm/modules/supabase/sql/logs.sql @@ -0,0 +1,5 @@ +\set pguser `echo "$POSTGRES_USER"` +\c _supabase +create schema if not exists _analytics; +alter schema _analytics owner to :pguser; +\c postgres diff --git a/hosts/web-arm/modules/supabase/sql/pooler.sql b/hosts/web-arm/modules/supabase/sql/pooler.sql new file mode 100644 index 0000000..516d986 --- /dev/null +++ b/hosts/web-arm/modules/supabase/sql/pooler.sql @@ -0,0 +1,5 @@ +\set pguser `echo "$POSTGRES_USER"` +\c _supabase +create schema if not exists _supavisor; +alter schema _supavisor owner to :pguser; +\c postgres diff --git a/hosts/web-arm/modules/supabase/sql/realtime.sql b/hosts/web-arm/modules/supabase/sql/realtime.sql new file mode 100644 index 0000000..231cded --- /dev/null +++ b/hosts/web-arm/modules/supabase/sql/realtime.sql @@ -0,0 +1,3 @@ +\set pguser `echo "$POSTGRES_USER"` +create schema if not exists _realtime; +alter schema _realtime owner to :pguser; diff --git a/hosts/web-arm/modules/supabase/sql/roles.sql b/hosts/web-arm/modules/supabase/sql/roles.sql new file mode 100644 index 0000000..c507c29 --- /dev/null +++ b/hosts/web-arm/modules/supabase/sql/roles.sql @@ -0,0 +1,6 @@ +\set pgpass `echo "$POSTGRES_PASSWORD"` +ALTER USER authenticator WITH PASSWORD :'pgpass'; +ALTER USER pgbouncer WITH PASSWORD :'pgpass'; +ALTER USER supabase_auth_admin WITH PASSWORD :'pgpass'; +ALTER USER supabase_functions_admin WITH PASSWORD :'pgpass'; +ALTER USER supabase_storage_admin WITH PASSWORD :'pgpass'; diff --git a/hosts/web-arm/modules/supabase/sql/webhooks.sql b/hosts/web-arm/modules/supabase/sql/webhooks.sql new file mode 100644 index 0000000..7d5238b --- /dev/null +++ b/hosts/web-arm/modules/supabase/sql/webhooks.sql @@ -0,0 +1,153 @@ +BEGIN; + CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions; + CREATE SCHEMA supabase_functions AUTHORIZATION supabase_admin; + GRANT USAGE ON SCHEMA supabase_functions TO postgres, anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres, anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role; + CREATE TABLE supabase_functions.migrations ( + version text PRIMARY KEY, + inserted_at timestamptz NOT NULL DEFAULT NOW() + ); + INSERT INTO supabase_functions.migrations (version) VALUES ('initial'); + CREATE TABLE supabase_functions.hooks ( + id bigserial PRIMARY KEY, + hook_table_id integer NOT NULL, + hook_name text NOT NULL, + created_at timestamptz NOT NULL DEFAULT NOW(), + request_id bigint + ); + CREATE INDEX supabase_functions_hooks_request_id_idx ON supabase_functions.hooks USING btree (request_id); + CREATE INDEX supabase_functions_hooks_h_table_id_h_name_idx ON supabase_functions.hooks USING btree (hook_table_id, hook_name); + COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.'; + CREATE FUNCTION supabase_functions.http_request() + RETURNS trigger + LANGUAGE plpgsql + AS $function$ + DECLARE + request_id bigint; + payload jsonb; + url text := TG_ARGV[0]::text; + method text := TG_ARGV[1]::text; + headers jsonb DEFAULT '{}'::jsonb; + params jsonb DEFAULT '{}'::jsonb; + timeout_ms integer DEFAULT 1000; + BEGIN + IF url IS NULL OR url = 'null' THEN + RAISE EXCEPTION 'url argument is missing'; + END IF; + IF method IS NULL OR method = 'null' THEN + RAISE EXCEPTION 'method argument is missing'; + END IF; + IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN + headers = '{"Content-Type": "application/json"}'::jsonb; + ELSE + headers = TG_ARGV[2]::jsonb; + END IF; + IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN + params = '{}'::jsonb; + ELSE + params = TG_ARGV[3]::jsonb; + END IF; + IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN + timeout_ms = 1000; + ELSE + timeout_ms = TG_ARGV[4]::integer; + END IF; + CASE + WHEN method = 'GET' THEN + SELECT http_get INTO request_id FROM net.http_get(url, params, headers, timeout_ms); + WHEN method = 'POST' THEN + payload = jsonb_build_object( + 'old_record', OLD, 'record', NEW, 'type', TG_OP, + 'table', TG_TABLE_NAME, 'schema', TG_TABLE_SCHEMA + ); + SELECT http_post INTO request_id FROM net.http_post(url, payload, params, headers, timeout_ms); + ELSE + RAISE EXCEPTION 'method argument % is invalid', method; + END CASE; + INSERT INTO supabase_functions.hooks (hook_table_id, hook_name, request_id) + VALUES (TG_RELID, TG_NAME, request_id); + RETURN NEW; + END + $function$; + DO + $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_functions_admin') THEN + CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION; + END IF; + END + $$; + GRANT ALL PRIVILEGES ON SCHEMA supabase_functions TO supabase_functions_admin; + GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA supabase_functions TO supabase_functions_admin; + GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA supabase_functions TO supabase_functions_admin; + ALTER USER supabase_functions_admin SET search_path = "supabase_functions"; + ALTER table "supabase_functions".migrations OWNER TO supabase_functions_admin; + ALTER table "supabase_functions".hooks OWNER TO supabase_functions_admin; + ALTER function "supabase_functions".http_request() OWNER TO supabase_functions_admin; + GRANT supabase_functions_admin TO postgres; + DO + $$ + BEGIN + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_pg_net_admin') THEN + REASSIGN OWNED BY supabase_pg_net_admin TO supabase_admin; + DROP OWNED BY supabase_pg_net_admin; + DROP ROLE supabase_pg_net_admin; + END IF; + END + $$; + DO + $$ + BEGIN + IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_net') THEN + GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + END IF; + END + $$; + CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access() + RETURNS event_trigger + LANGUAGE plpgsql + AS $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM pg_event_trigger_ddl_commands() AS ev + JOIN pg_extension AS ext ON ev.objid = ext.oid + WHERE ext.extname = 'pg_net' + ) THEN + GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + END IF; + END; + $$; + COMMENT ON FUNCTION extensions.grant_pg_net_access IS 'Grants access to pg_net'; + DO + $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_event_trigger WHERE evtname = 'issue_pg_net_access') THEN + CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end WHEN TAG IN ('CREATE EXTENSION') + EXECUTE PROCEDURE extensions.grant_pg_net_access(); + END IF; + END + $$; + INSERT INTO supabase_functions.migrations (version) VALUES ('20210809183423_update_grants'); + ALTER function supabase_functions.http_request() SECURITY DEFINER; + ALTER function supabase_functions.http_request() SET search_path = supabase_functions; + REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC; + GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres, anon, authenticated, service_role; +COMMIT; diff --git a/hosts/web-arm/modules/supabase/vector.yml b/hosts/web-arm/modules/supabase/vector.yml new file mode 100644 index 0000000..cb6ca90 --- /dev/null +++ b/hosts/web-arm/modules/supabase/vector.yml @@ -0,0 +1,255 @@ +api: + enabled: true + address: 0.0.0.0:9001 + +sources: + docker_host: + type: docker_logs + exclude_containers: + - supabase-vector + +transforms: + project_logs: + type: remap + inputs: + - docker_host + source: |- + .project = "default" + .event_message = del(.message) + .appname = del(.container_name) + del(.container_created_at) + del(.container_id) + del(.source_type) + del(.stream) + del(.label) + del(.image) + del(.host) + del(.stream) + router: + type: route + inputs: + - project_logs + route: + kong: '.appname == "supabase-kong"' + auth: '.appname == "supabase-auth"' + rest: '.appname == "supabase-rest"' + realtime: '.appname == "realtime-dev.supabase-realtime"' + storage: '.appname == "supabase-storage"' + functions: '.appname == "supabase-edge-functions"' + db: '.appname == "supabase-db"' + kong_logs: + type: remap + inputs: + - router.kong + source: |- + req, err = parse_nginx_log(.event_message, "combined") + if err == null { + .timestamp = req.timestamp + .metadata.request.headers.referer = req.referer + .metadata.request.headers.user_agent = req.agent + .metadata.request.headers.cf_connecting_ip = req.client + .metadata.response.status_code = req.status + url, split_err = split(req.request, " ") + if split_err == null { + .metadata.request.method = url[0] + .metadata.request.path = url[1] + .metadata.request.protocol = url[2] + } + } + if err != null { + abort + } + kong_err: + type: remap + inputs: + - router.kong + source: |- + .metadata.request.method = "GET" + .metadata.response.status_code = 200 + parsed, err = parse_nginx_log(.event_message, "error") + if err == null { + .timestamp = parsed.timestamp + .severity = parsed.severity + .metadata.request.host = parsed.host + .metadata.request.headers.cf_connecting_ip = parsed.client + url, err = split(parsed.request, " ") + if err == null { + .metadata.request.method = url[0] + .metadata.request.path = url[1] + .metadata.request.protocol = url[2] + } + } + if err != null { + abort + } + auth_logs: + type: remap + inputs: + - router.auth + source: |- + parsed, err = parse_json(.event_message) + if err == null { + .metadata.timestamp = parsed.time + .metadata = merge!(.metadata, parsed) + } + rest_logs: + type: remap + inputs: + - router.rest + source: |- + parsed, err = parse_regex(.event_message, r'^(?P