<?xml version="1.0" encoding="utf-8" standalone="yes"?><?xml-stylesheet href="/rss.xsl" type="text/xsl"?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/"><title>Gotosocial on Hyteck</title><link href="https://hyteck.de/tags/gotosocial/"/><generator>Hugo -- gohugo.io</generator><language>en-us</language><id>https://hyteck.de/tags/gotosocial/</id><updated>2025-10-19T12:05:10+02:00</updated><link href="https://hyteck.de/tags/gotosocial/index.xml" rel="self" type="application/rss+xml"/><entry><title>Postmortem - how to completely screw up an update</title><link href="https://hyteck.de/post/postmortem-gpa/" type="application/octet-stream"/><updated>2025-10-19T12:05:10+02:00</updated><id>https://hyteck.de/post/postmortem-gpa/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>The fediverse instance &lt;a href="https://gay-pirate-assassins.de">gay-pirate-assassins.de&lt;/a> was down for a couple of days. This
postmortem will outline what went wrong and what I did to prevent things from going that wrong in the future.&lt;/p>
&lt;h1 id="timeline">Timeline&lt;/h1>
&lt;ul>
&lt;li>2025-10-05 17:26: &lt;a href="https://gay-pirate-assassins.de/@moanos/statuses/01K6TFQ1HVPAR6AYN08XYQ7XFV">Update announcement&lt;/a>&lt;/li>
&lt;li>2025-10-05 ~17:45: Update started&lt;/li>
&lt;li>2025-10-05 ~18:00: Services restart&lt;/li>
&lt;li>2025-10-05 ~18:00: GoToSocial doesn&amp;rsquo;t come up&lt;/li>
&lt;li>2025-10-12 ~10:00: Issue is found&lt;/li>
&lt;li>2025-10-12 10:30: Issue is fixed&lt;/li>
&lt;li>2025-10-12 10:31: GoToSocial is started, migrations start&lt;/li>
&lt;li>2025-10-12 15:38: Migrations finished successfully&lt;/li>
&lt;li>2025-10-12 15:38: Service available again&lt;/li>
&lt;li>2025-10-12 18:36:&lt;a href="https://gay-pirate-assassins.de/@moanos/statuses/01K7CMGF7S2TE39792CMADGEPJ">Announcement sent&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>All times are given in CEST.&lt;/p>
&lt;h2 id="the-beginning-an-update-goes-wrong">The beginning: An update goes wrong&lt;/h2>
&lt;p>I run a small fediverse server with a few users called. &lt;a href="https://gay-pirate-assassins.de/">gay-pirate-assassins&lt;/a> which is powered by &lt;a href="https://gotosocial.org/">GoToSocial&lt;/a>.
The (amazing) GoToSocial devs released &lt;code>v0.20.0-rc1&lt;/code> and &lt;code>v0.20.0-rc2&lt;/code>. As the new features seemed pretty cool, I&amp;rsquo;m
inpatient and the second release candidate seemed stable,
I decided to update to &lt;code>v0.20.0-rc2&lt;/code>. So I stared a backup (via borgmatic), waited for it to finish and confirmed it ran
successfully.
Then I changed the version number in the &lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook">mash&lt;/a>-ansible
playbook I use. Then I pulled the newest version of the playbook and it&amp;rsquo;s roles because I wanted to update all services
that run on the server. I checked
the &lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/CHANGELOG.md">Changelog&lt;/a>,
didn&amp;rsquo;t see anything and then started the update. It went through and GoToSocial started up just fine.&lt;/p>
&lt;p>But the instance start page showed me 0 users, 0 posts and 0 federated instances. &lt;strong>Something has gone horribly wrong!&lt;/strong>&lt;/p>
&lt;h2 id="migrations">Migrations&lt;/h2>
&lt;p>It was pretty clear to me, that the migrations went wrong.
The &lt;a href="https://codeberg.org/superseriousbusiness/gotosocial/releases/tag/v0.20.0-rc1">GoToSocial Migration notes&lt;/a>
specifically mentioned long-running migrations that could take several hours. I assumed that somehow, during the running
database migration, the service must have restarted and left the DB in a broken state. This issue happened to me before.&lt;/p>
&lt;p>Well, that&amp;rsquo;s what backups are for, so let&amp;rsquo;s pull it.&lt;/p>
&lt;h2 id="backups">Backups&lt;/h2>
&lt;p>Backups for this server are done two ways:&lt;/p>
&lt;ul>
&lt;li>via postgres-backup: Backups of the database are written to disk&lt;/li>
&lt;li>via &lt;a href="https://torsion.org/borgmatic/">borgmatic&lt;/a>: Backups via borg are written to backup nodes, one of them at my home&lt;/li>
&lt;/ul>
&lt;p>They run every night automatically, monitored by &lt;a href="https://healthchecks.io/">Healthchecks&lt;/a>. I triggered a manual run
before the update so that is the one I mounted using &lt;a href="https://vorta.borgbase.com/">Vorta&lt;/a>.&lt;/p>
&lt;p>And then the realization.&lt;/p>
&lt;pre tabindex="0">&lt;code>
mash-postgres:5432 $ ls -lh
total 2.1M
-r-------- 1 moanos root 418K Oct 05 04:03 gitea
-r-------- 1 moanos root 123K Oct 05 04:03 healthchecks
-r-------- 1 moanos root 217K Oct 05 04:03 ilmo
-r-------- 1 moanos root 370K Oct 05 04:03 notfellchen
-r-------- 1 moanos root 67K Oct 05 04:03 oxitraffic
-r-------- 1 moanos root 931 Oct 05 04:03 prometheus_postgres_exporter
-r-------- 1 moanos root 142K Oct 05 04:03 semaphore
-r-------- 1 moanos root 110K Oct 05 04:03 vaultwarden
-r-------- 1 moanos root 669K Oct 05 04:03 woodpecker_ci_server
&lt;/code>&lt;/pre>&lt;p>Fuck. The database gay-pirate-assassins is not there. Why?&lt;/p>
&lt;p>To explain that I have to tell you how it &lt;em>should&lt;/em> work: Services deployed by the mash-playbook are automatically wired
to the database and reverse proxy by a complex set of Ansible variables. This is great, because adding a service can
therefore be as easy as adding&lt;/p>
&lt;pre tabindex="0">&lt;code>healthchecks_enabled: true
healthchecks_hostname: health.hyteck.de
&lt;/code>&lt;/pre>&lt;p>to the &lt;code>vars.yml&lt;/code> file.&lt;/p>
&lt;p>This will then configure the postgres database automatically, based on the &lt;code>group_vars&lt;/code>. They look like this&lt;/p>
&lt;pre tabindex="0">&lt;code>mash_playbook_postgres_managed_databases_auto_itemized:
- |-
{{
({
&amp;#39;name&amp;#39;: healthchecks_database_name,
&amp;#39;username&amp;#39;: healthchecks_database_username,
&amp;#39;password&amp;#39;: healthchecks_database_password,
} if healthchecks_enabled and healthchecks_database_hostname == postgres_connection_hostname and healthchecks_database_type == &amp;#39;postgres&amp;#39; else omit)
}}
&lt;/code>&lt;/pre>&lt;p>Note that a healthchecks database is only added to the managed databases if &lt;code>healthchecks_enabled&lt;/code> is &lt;code>True&lt;/code>.&lt;/p>
&lt;p>This is really useful for backups because the borgmatic configuration also pulls the list
&lt;code>mash_playbook_postgres_managed_databases_auto_itemized&lt;/code>. Therefore, you do not need to specify which databases to back
up, it just backs up all managed databases.&lt;/p>
&lt;p>However, the database for gay-pirate assassins was not managed. In the playbook it&amp;rsquo;s only possible to configure a
service once. You can not manage multiple GoToSocial instances in the same &lt;code>vars.yml&lt;/code>. In the past, I had two instances
of GoToSocial running on the server. I therefore
followed &lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/running-multiple-instances.md">the how-to of &amp;ldquo;Running multiple instances of the same service on the same host&amp;rdquo;&lt;/a>.&lt;/p>
&lt;p>Basically this means that an additional &lt;code>vars.yml&lt;/code> must be created that is treated as a completely different server.
Databases must be created manually as they are not managed.&lt;/p>
&lt;p>With that knowledge you can understand that when I say that the database for gay-pirate-assassins was not managed,
this means it was not included in the list of databases to be backed up. The backup service thought it ran successfully,
because it backed up everything it knew of.&lt;/p>
&lt;p>So this left me with a three-month-old backup. Unacceptable.&lt;/p>
&lt;h2 id="investigating">Investigating&lt;/h2>
&lt;p>So the existing database needed to be rescued. I SSHed into the server and checked the database. It looked completely
normal.
I asked the devs if they could me provide me with the migrations as they already did in the past. However, they pointed
out that the migrations are too difficult for that approach. They suggested to delete the oldest migration to force a
re-run of the migrations.&lt;/p>
&lt;p>Here is where I was confused, because this was the &lt;code>bun_migrations&lt;/code> table:&lt;/p>
&lt;pre tabindex="0">&lt;code>gay-pirate-assassins=# SELECT * FROM bun_migrations ORDER BY id DESC LIMIT 5;
id | name | group_id | migrated_at
-----+----------------+----------+-------------------------------
193 | 20250324173534 | 20 | 2025-04-23 20:00:33.955776+00
192 | 20250321131230 | 20 | 2025-04-23 19:58:06.873134+00
191 | 20250318093828 | 20 | 2025-04-23 19:57:50.540568+00
190 | 20250314120945 | 20 | 2025-04-23 19:57:30.677481+00
&lt;/code>&lt;/pre>&lt;p>The last migration ran in April, when I updated to &lt;code>v0.19.1&lt;/code>. Strange.&lt;/p>
&lt;p>At this point I went on vacation and paused investigations, not only because the vacation was great, but also because I
bamboozeld by this state.&lt;/p>
&lt;hr>
&lt;p>After my vacation I came back, and did some backups of the database.&lt;/p>
&lt;pre tabindex="0">&lt;code>$ docker run -e PGPASSWORD=&amp;#34;XXXX&amp;#34; -it --rm --network mash-postgres postgres pg_dump -U gay-pirate-assassins -h mash-postgres gay-pirate-assassins &amp;gt; manual-backup/gay-pirate-assassins-2025-10-13.sql
&lt;/code>&lt;/pre>&lt;p>Then I deleted the last migration, as I was advised&lt;/p>
&lt;pre tabindex="0">&lt;code>DELETE FROM bun_migration WHERE id=193;
&lt;/code>&lt;/pre>&lt;p>and restarted the server. While watching the server come up it hit me in the face:&lt;/p>
&lt;pre tabindex="0">&lt;code>Oct 12 08:31:29 s3 mash-gpa-gotosocial[2251925]: timestamp=&amp;#34;12/10/2025 08:31:29.905&amp;#34; func=bundb.sqliteConn level=INFO msg=&amp;#34;connected to SQLITE database with address file:/opt/gotosocial/sqlite.db?_pragma=busy_timeout%281800000%29&amp;amp;_pragma=journal_mode%&amp;gt;
Oct 12 13:38:46 s3 mash-gpa-gotosocial[2304549]: timestamp=&amp;#34;12/10/2025 13:38:46.588&amp;#34; func=router.(*Router).Start.func1 level=INFO msg=&amp;#34;listening on 0.0.0.0:8080&amp;#34;
&lt;/code>&lt;/pre>&lt;p>The server is &lt;strong>starting from a completely different database&lt;/strong>! That explains why&lt;/p>
&lt;ul>
&lt;li>the last migration was never done&lt;/li>
&lt;li>the server showed me 0 users, 0 posts and 0 federated instances even though the postgres database had plenty of those&lt;/li>
&lt;/ul>
&lt;p>All of a sudden a SQlite database was configured. This happened because
of &lt;a href="https://github.com/mother-of-all-self-hosting/ansible-role-gotosocial/commit/df34af385f9765bda8f160f6985a47cb7204fe96">this commit&lt;/a>
which introduced SQlite support and set it as default. This was not mentioned in
the &lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/CHANGELOG.md">Changelog&lt;/a>.&lt;/p>
&lt;p>So what happened is, that the config changed and then the server was restarted and an empty DB was initialized. The
postgres DB never started to migrate.&lt;/p>
&lt;h2 id="fixing">Fixing&lt;/h2>
&lt;p>To fix it, I did the following&lt;/p>
&lt;ol>
&lt;li>Configure the playbook to use postgres for GoToSocial:&lt;/li>
&lt;/ol>
&lt;pre tabindex="0">&lt;code># vars.yml
gotosocial_database_type: postgres
&lt;/code>&lt;/pre>&lt;ol start="2">
&lt;li>Run the playbook to configure GoToSocial (but not starting the service)&lt;/li>
&lt;/ol>
&lt;pre tabindex="0">&lt;code>just run-tags install-gotosocial
&lt;/code>&lt;/pre>&lt;ol start="3">
&lt;li>Check the configuration is correct&lt;/li>
&lt;li>Start the service&lt;/li>
&lt;/ol>
&lt;p>The migrations took several hours but after that, everything looked stable again. I don&amp;rsquo;t think there are any lasting
consequences. However, the server was unavailable for several days.&lt;/p>
&lt;h2 id="learnings">Learnings&lt;/h2>
&lt;p>I believe the main issue here was not the change in the config that went unnoticed by me. While I&amp;rsquo;d ideally notice stuff
like this, the server is a hobby, and I&amp;rsquo;ll continue to not check every config option that changed.&lt;/p>
&lt;p>The larger issue was the backup. Having a backup would have made this easy to solve. And there are other, less lucky
problems where I&amp;rsquo;d be completely lost without a backup. So to make sure this doesn&amp;rsquo;t happen again, I did/will do the
following:&lt;/p>
&lt;h3 id="1-mainstream-the-config">1. Mainstream the config&lt;/h3>
&lt;p>As explained, I used a specific non-mainstream setup in the ansible playbook because, in the past, I ran two instances
of GoToSocial on the server. After shutting down one of them, I never moved gay-pirate-assassins to be part of the main
config. This means important parts of the configuration had to be done manually, which I botched.&lt;/p>
&lt;p>So in the past week I cleaned up and gay-pirate-assassins is now part of the main &lt;code>vars.yml&lt;/code> and will benefit from all
relevant automations.&lt;/p>
&lt;h3 id="2-checking-backups">2. Checking backups&lt;/h3>
&lt;p>I was confident in my backups because&lt;/p>
&lt;ul>
&lt;li>they run every night very consistently. If they fail e.g. because of a network outage I reliably get a warning.&lt;/li>
&lt;li>I verified successfully run of the backup job prior to upgrading&lt;/li>
&lt;/ul>
&lt;p>The main problem was me assuming that a successful run of the backup command, meant a successful backup. Everyone will
tell you that a backup that is not tested is not to be trusted. And they are right. However, doing frequent
test-restores
exceeds my time and server capacity. So what I&amp;rsquo;ll do instead is the following:&lt;/p>
&lt;ul>
&lt;li>mount the backup before an upgrade&lt;/li>
&lt;li>&lt;code>tail&lt;/code> the backup file as created by postgres-backup and ensure the data is from the same day&lt;/li>
&lt;li>check media folders for the last changed image&lt;/li>
&lt;/ul>
&lt;p>This is not a 100% guarantee, but I&amp;rsquo;d argue it&amp;rsquo;s a pretty good compromise for now. As the frequency of mounting backups
increases and therefore becomes faster, I&amp;rsquo;ll re-evaluate to do a test-restore at least semi-regulary.&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>I fucked up, but I was lucky that my error was recoverable and no data was lost. Next time this will hopefully be not due
to luck, but better planning!&lt;/p>
&lt;p>Any questions? Let me know!&lt;/p></content></entry><entry><title>I did something naughty: Circumventing Authorized-Fetch as implemented by GoToSocial</title><link href="https://hyteck.de/post/public-posts-with-authorized-fetch/" type="application/octet-stream"/><updated>2024-12-11T06:10:10+02:00</updated><id>https://hyteck.de/post/public-posts-with-authorized-fetch/</id><author><name>Julian-Samuel Gebühr</name></author><content type="html"> &lt;p>Yes the title is correct, but I had nothing malicious in mind!&lt;/p>
&lt;h2 id="what-this-is-about">What this is about&lt;/h2>
&lt;p>For &lt;a href="https://social.queereszentrumtuebingen.de/@qzt">@qzt@queereszentrumtuebingen.de&lt;/a> we include the public feed &lt;a href="https://queereszentrumtuebingen.de/">in a sidbar on the homepage&lt;/a>. Initially this was done using the standard API to fetch statuses &lt;code>/api/v1/accounts/{account_id}/statuses&lt;/code> and worked like a charm. The problem started when &lt;a href="https://gotosocial.org/">GoToSocial&lt;/a> (the fediverse server we use, similar to mastodon) implemented authorized fetch. This is a a good thing! Authorized fetch means, that every call to a endpoint needs to be authorized by an &lt;code>access_token&lt;/code>. You get an access token from a fedi account. It&amp;rsquo;s what fediverse clients like Tusky or Phanpy do on your behalf to get the posts that make up you timeline.&lt;/p>
&lt;p>Authorized fetch has major advantages as&lt;/p>
&lt;ul>
&lt;li>data scraping can only be done by other fediaccounts&lt;/li>
&lt;li>blocking can not be circumvented by using the public API&lt;/li>
&lt;/ul>
&lt;p>and much more. Sadly it also broke our website integration.&lt;/p>
&lt;h2 id="possible-solutions">Possible Solutions&lt;/h2>
&lt;p>So what now? I initially wanted to turn of authorized fetch for &lt;a href="https://social.queereszentrumtuebingen.de/@qzt">@qzt@queereszentrumtuebingen.de&lt;/a> by messing with the GoToSocial code and turning it off for the whole server. This would have been possible as this is the only user on the server. The GoToSocial devs helped me manage to find where to do that. But it&amp;rsquo;s not ideal and would make me build a custom docker image fore each update.&lt;/p>
&lt;p>Next idea: The whole point of authorized fetch is, that only fedi-accounts (and apps they authorized) can access the API. So lets do that! Set up a new account, add app and authorize it &lt;a href="https://docs.gotosocial.org/en/latest/api/authentication/">as described in the GoToSocial documentation&lt;/a>. I used #Bruno for that, that was much more comfortable than using curl for me.
With that authorization code you can now get an access token for your app. Put that in the Javascript that loads posts and we are good right? Sadly no. It would totally work. But it would also allow anyone to read and post on behalf of the account. That calls for malicious actors using this for scraping or spamming.&lt;/p>
&lt;p>So instead, we need a proxy that stores the access token securely and restricts the actions.&lt;/p>
&lt;h2 id="the-proxy">The proxy&lt;/h2>
&lt;p>Such a proxie must&lt;/p>
&lt;ul>
&lt;li>offer the endpoint that provides the same data as the FediverseAPI&lt;/li>
&lt;li>authorize itself to the FediverseAPI via &lt;code>access_token&lt;/code>&lt;/li>
&lt;li>restrict to read access of consenting accounts&lt;/li>
&lt;/ul>
&lt;p>The last point is really important, as we don&amp;rsquo;t want to allow others to use this endpoint to scrape data unauthorized.&lt;/p>
&lt;p>I wrote a short FastAPI server that offers this. It only implements one method&lt;/p>
&lt;pre tabindex="0">&lt;code>@app.get(&amp;#34;/api/v1/accounts/{account_id}/statuses&amp;#34;)
async def fetch_data(account_id):
if account_id not in ALLOWED_ACCOUNTS:
raise HTTPException(status_code=401, detail=&amp;#34;You can only use this proxy to access configured accounts&amp;#34;)
headers = {&amp;#34;Authorization&amp;#34;: f&amp;#34;Bearer {ACCESS_TOKEN}&amp;#34;}
response = requests.get(f&amp;#34;{EXTERNAL_API_BASE_URL}/api/v1/accounts/{account_id}/statuses&amp;#34;, headers=headers)
return response.json()
&lt;/code>&lt;/pre>&lt;p>Basically this is the whole API code, I only trimmed a few checks and error handling.&lt;/p>
&lt;h2 id="deployment">Deployment&lt;/h2>
&lt;p>To deploy, I put it in a docker container and started it via docker-compose. Reverse proxing is handled by Traefik, I won&amp;rsquo;t go into detail here.&lt;/p>
&lt;pre tabindex="0">&lt;code>services:
fediproxy.example.org:
image: docker.io/moanos/fediproxy
container_name: &amp;#34;fediproxy.example.org&amp;#34;
restart: unless-stopped
environment:
EXTERNAL_API_BASE_URL: ${EXTERNAL_API_BASE_URL}
ACCESS_TOKEN: ${ACCESS_TOKEN}
ALLOWED_ACCOUNTS: ${ALLOWED_ACCOUNTS}
labels:
- &amp;#34;traefik.enable=true&amp;#34;
- &amp;#34;traefik.docker.network=traefik&amp;#34;
- &amp;#34;traefik.http.routers.fediproxy.rule=Host(`fediproxy.example.org`)&amp;#34;
- &amp;#34;traefik.http.routers.fediproxy.service=fediproxy-service&amp;#34;
- &amp;#34;traefik.http.routers.fediproxy.entrypoints=web-secure&amp;#34;
- &amp;#34;traefik.http.routers.fediproxy.tls=true&amp;#34;
- &amp;#34;traefik.http.routers.fediproxy.tls.certResolver=default&amp;#34;
- &amp;#34;traefik.http.services.fediproxy-service.loadbalancer.server.port=8000&amp;#34;
networks:
- traefik
networks:
traefik:
name: &amp;#34;traefik&amp;#34;
external: true
&lt;/code>&lt;/pre>&lt;p>I added a short &lt;code>.env&lt;/code> to configure:&lt;/p>
&lt;pre tabindex="0">&lt;code>ACCESS_TOKEN=VERYSECRETTOKENTHATISDEFINETLYREAL
EXTERNAL_API_BASE_URL=https://gay-pirate-assassins.de
ALLOWED_ACCOUNTS=ZGGZF4G8NNOTREAL81Z8G7RTC
&lt;/code>&lt;/pre>&lt;h2 id="results">Results&lt;/h2>
&lt;p>Now I can again use something like &lt;a href="https://wordpress.org/plugins/include-mastodon-feed/#installation">the wordpress plugin Include Mastodon Feed&lt;/a> just by pointing to the proxy: &lt;code>[include-mastodon-feed instance=&amp;quot;fediproxy.example.org.de&amp;quot; account=&amp;quot;ZGGZF4G8NNOTREAL81Z8G7RTC&amp;quot;]&lt;/code>&lt;/p>
&lt;p>Hope you enjoyed the read. Source code for the proxy can be found here: &lt;a href="https://git.hyteck.de/moanos/FediProxy">https://git.hyteck.de/moanos/FediProxy&lt;/a>
If you want to play around a bit you can use &lt;a href="https://git.hyteck.de/moanos/include-fedi">https://git.hyteck.de/moanos/include-fedi&lt;/a>&lt;/p>
&lt;p>Sloth logo of GTS by &lt;a href="https://abramek.art/">Anna Abramek&lt;/a>, &lt;a href="http://creativecommons.org/licenses/by-sa/4.0/">Creative Commons BY-SA license&lt;/a>.&lt;/p></content></entry></feed>