Merge branch 'main' into miho-s3-validation

This commit is contained in:
Mihovil Ilakovac 2025-02-28 18:35:56 +01:00
commit 5f08f50a09
11 changed files with 355 additions and 28 deletions

View File

@ -99,6 +99,12 @@ export default defineConfig({
picture: '/milica.jpg', // Images in the `public` directory are supported.
url: 'https://wasp.sh',
},
martin: {
name: 'Martin',
title: 'CTO @ Wasp',
picture: '/martin.jpg', // Images in the `public` directory are supported.
url: 'https://wasp.sh',
},
},
}),
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

@ -0,0 +1,210 @@
---
title: "Incident Report: Security Vulnerability in Open SaaS \"updateCurrentUser\" function"
date: 2025-02-26
tags:
- incident-report
authors: martin
---
import { Image } from 'astro:assets';
On Feb 12th, 2025, we learned about a security vulnerability in the `updateCurrentUser` function of our Open SaaS template. Users of apps built with Open SaaS can exploit this vulnerability to edit any field in their own `User` database record, including fields they weren't supposed to have write permissions for, like `subscriptionPlan` and `isAdmin`.
**If you created your app with the Open SaaS template before Feb 14th of '25, your app potentially suffers from this vulnerability, and you should apply the fix from this report as soon as possible**. Check out [vulnerability](#the-vulnerability) and [fix](#the-fix) sections.
The vulnerability does not affect the "vanilla" Wasp apps (Wasp apps not built with Open SaaS template) or those that modified the problematic part of the code enough to eliminate the problem.
Since then we fixed the vulnerability in all the versions of the Open SaaS template, did a [coordinated vulnerability disclosure](https://en.wikipedia.org/wiki/Coordinated_vulnerability_disclosure) (culminating with this report) with the suggested fix, reviewed all the other templates and example apps of ours for similar security vulnerabilities, analyzed what at the first place enabled such omission to happen on our side, and prepared a plan on how to minimize the chance of similar mistakes happening in the future.
We sincerely apologize for the impact and inconvenience caused by our mistake. Caring about code quality is at the center of our culture here at Wasp, but in this instance, we failed to follow up on our standards. We are deeply disappointed by it and will ensure we learn from it, improve, and regain your trust in the code we ship, especially as Wasp is heading from Beta toward 1.0.
## The vulnerability
The vulnerability is caused by the `updateCurrentUser` function in `src/user/operations.ts` (or in `src/server/actions.ts` if you used an older version of Open SaaS):
```tsx
export const updateCurrentUser: UpdateCurrentUser<Partial<User>, User> = async (user, context) => {
if (!context.user) {
throw new HttpError(401);
}
return context.entities.User.update({
where: {
id: context.user.id,
},
data: user, // <- This is the problem!
});
};
```
While this Wasp Action correctly allows the user to modify only data in their own `User` database record, and not of other users, it does also allow them to potentially change ANY of the fields on their own User db model, including fields like `credits`, `subscriptionPlan`, and `isAdmin`, due to `data: user` line in `User.update()` call. Particularly troublesome is the ability to set `isAdmin` to `true`, as it gives them further privileges they shouldn't have.
An example of how a bad actor could exploit this is by creating a user account in your app, obtaining their own auth credentials via browser dev tools, and then sending a modified request to the HTTP route of `updateCurrentUser` Wasp Action with a payload that sets the `isAdmin` field to `true` for themselves.
## The fix
The fix consists of three main steps:
1. Refactor `updateCurrentUser` function to `updateCurrentUserLastActiveTimestamp`
2. Implement additional Wasp Action(s) for updating user data if needed
3. Refactor `updateUserById` function to `updateUserIsAdminById`
### Refactor `updateCurrentUser` to `updateCurrentUserLastActiveTimestamp`
In the Open SaaS template, as it comes when you create a new project with it, the Wasp Action `updateCurrentUser` isn't used for anything else but updating the `lastActiveTimestamp` field on the `User` model, despite its general nature. Therefore, we recommend the following fix:
1. Rename the operation `updateCurrentUser` to `updateCurrentUserLastActiveTimestamp`. Make sure to update its name in all the places: `main.wasp`, client code (i.e. `src/client/App.tsx`), server code (i.e. `src/user/operations.ts`).
2. Rewrite the operation `updateCurrentUserLastActiveTimestamp` in `src/user/operations.ts` so it receives no arguments and only updates the `lastActiveTimestamp` field on the `User`:
```tsx
export const updateCurrentUserLastActiveTimestamp: UpdateCurrentUserLastActiveTimestamp<void, User> = async (_args, context) => {
if (!context.user) {
throw new HttpError(401);
}
return context.entities.User.update({
where: {
id: context.user.id,
},
data: {
lastActiveTimestamp: new Date(),
},
});
};
```
Notice that also the name of the type of the operation changed, so you will want to update the type import, and we also changed the operation's Input type to `void`.
3. Remove all arguments from the call to `updateCurrentUserLastActiveTimestamp` in `src/client/App.tsx:`
```tsx
if (today.getTime() - lastSeenAt.getTime() > 5 * 60 * 1000) {
updateCurrentUserLastActiveTimestamp(); // <- no args anymore
}
```
### Implement additional Wasp Action(s) for updating user data if needed
If you were using `updateCurrentUser` in your code beyond just updating `lastActiveTimestamp`, to allow the user to update some other `User` fields, we recommend also defining additional, more specialized Wasp Action(s) that will handle this additional usage.
For example, let's say that in your app you additionally defined `fullName` and `address` fields on the `User` model, and you were using `updateCurrentUser` to allow the user to update those. In that case, we recommend defining an additional Wasp Action called `updateCurrentUserPersonalData`. It could look something like this:
```tsx
export const updateCurrentUserPersonalData: UpdateCurrentUserPersonalData<Pick<User, "fullName" | "address">, User> = async (personalData, context) => {
if (!context.user) {
throw new HttpError(401);
}
// NOTE: This is also a good place to do data validation if you want to.
const fullName = personalData.fullName
const address = personalData.address
return context.entities.User.update({
where: {
id: context.user.id,
},
data: { fullName, address }
});
};
```
### Refactor `updateUserById` to `updateUserIsAdminById`
Finally, while not a security vulnerability, we also recommend updating the related Wasp Action, `updateUserById` (you can find it next to where the `updateCurrentUser` function was), in a similar fashion, to ensure it can't do more than we need it to:
1. Rename from `updateUserById` to `updateUserIsAdminById`.
2. Rewrite `updateUserIsAdminById` to only allow setting the `isAdmin` field:
```tsx
export const updateUserIsAdminById: UpdateUserIsAdminById<{ id: User['id'], isAdmin: User['isAdmin'] }, User> = async ({ id, isAdmin }, context) => {
if (!context.user) {
throw new HttpError(401);
}
if (!context.user.isAdmin) {
throw new HttpError(403);
}
if (id === undefined || isAdmin === undefined) {
throw new HttpError(400);
}
return context.entities.User.update({
where: { id },
data: { isAdmin },
});
};
```
Notice that we modified the shape of the operation input (now it is `{ id, isAdmin }`), so you will also want to update the calls to this operation accordingly.
---
## Additional reading
This second part of the report is not required reading: all you need to know in order to fix the vulnerability is in the "[The vulnerability](#the-vulnerability)" and the "[The fix](#the-fix)" sections. But, if you want to learn more about what caused the vulnerability, how we handled it, and what are we doing to prevent similar mistakes from happening in the future, read on!
## Coordinated vulnerability disclosure
The challenging part about handling a security vulnerability like this one is that we have to make the knowledge of it public so that all the people with affected apps learn about it and how to fix it, but then at the same time we are also making that knowledge easily available to any bad actors that might want to try to exploit it.
One of the popular approaches is [coordinated vulnerability disclosure](https://en.wikipedia.org/wiki/Coordinated_vulnerability_disclosure) and it is also what we chose to follow in this instance. We decided to disclose the vulnerability in stages, with 1-week pauses in between:
1. Stage 1 (private disclosure): We assembled a list of everybody we knew was building and deploying apps with Wasp / Open SaaS, be it from our community on Discord, from online search, or from interacting with them in the past. We privately reached out to everybody on the list and shared the details of the vulnerability and the fix, while also asking them to keep it confidential till we go public with it.
2. Stage 2 (community disclosure): About a week later, we shared the details of the vulnerability in our Discord community, while again asking people not to share it publicly till we go public with it.
3. Stage 3 (public disclosure): Finally, a week after the Stage 2, we shared the vulnerability fully publicly.
## How did this happen?
**TL;DR**: Failure in our code review process.
At Wasp, we care a lot about code quality, the code review process, and [software craftsmanship](https://martinsosic.com/book/2018/08/17/book-the-software-craftsman.html) in general. PRs get thoroughly reviewed, we do our best to write Clean Code (with a grain of salt), we think a lot about [naming](https://wasp.sh/blog/2023/10/12/on-importance-of-naming-in-programming), we produce [RFCs](https://wasp.sh/blog/2023/12/05/writing-rfcs) for any complex feature, our codebase is strongly typed (Haskell and TypeScript), we keep track of all the issues and ideas publicly on our GitHub to not lose sight of them and to also get community input and be transparent.
Commitment to these practices does get tested regularly: Wasp is moving fast and is changing a lot since it is still pre-1.0, so there is always more tech debt going on than one would like, but we always felt like we managed to stay on the good side of our commitment to these practices: they enable us to be efficient but also to enjoy and be proud of our work.
So what happened then, what enabled this vulnerability in Open SaaS?
Open SaaS started as a one-person experiment, a simple template/boilerplate starter for Wasp, so we didn't do the usual thorough code reviewing of every PR at the very start but thought we would do it fully later, once it shaped up a bit. Also, it is just a template, not a library/framework, people can read/modify the code as they like.
Open SaaS did shape up, and not only have people started using it, but it really picked up, more than we ever expected, and we were getting a lot of positive and constructive feedback, feature requests, ideas, bug reports, … . We started reviewing all the new code thoroughly, but we still haven't done the full retroactive review. We have done some of it, for parts of more sensitive modules, and some of it happened naturally through refactoring, but we haven't done it systematically for the whole codebase. We would discuss during every quarterly planning how we should do it this quarter, but there was always something with a higher priority, especially on the Wasp side, and Open SaaS was doing great, if there was anything serious, we would already know about it, we thought.
And then we learned about a function in our codebase that allows a user to set any data, without runtime validation, as a partial update for their `User` record in the database. This function was barely even used in the Open SaaS codebase at this point: it was used only to update a single field in the `User` database model, and even that usage should have been refactored into something better already. This function was an obvious code smell, but we never reviewed it properly.
The fact is, we never should have made Open SaaS publicly available without doing a full code review of it first. Once the code is out there, be it just an example app, a template, or a library, we can't guess how it or its usage will evolve, or how will our priorities evolve. Once an exception in the (review) process is made, it is much harder to find the time to catch up on it, than if we did it when we should have done it in the first place.
## What we are doing to prevent similar mistakes
- **No code/documentation goes public without a thorough review.**
We have been doing this from the very start for the Wasp framework codebase, but we were more lenient with the templates and example apps. From now on, there will be no exceptions.
- **We checked all our existing templates and example apps for vulnerabilities.**
- **We have done a thorough review of the Open SaaS template codebase.**
We have already merged a lot of code quality improvements based on it, and we are in the process of merging the rest.
- **We will make it harder at the Wasp framework level to make a similar mistake.**
The mistake of passing unvalidated/unparsed data is too easy to make - we will, latest for Wasp 1.0, enforce [runtime data validation in Wasp](https://github.com/wasp-lang/wasp/issues/1241), for Operations, APIs, and other externally facing methods. We also have good ideas for advanced access control support in Wasp, which should further make it harder to make these kinds of mistakes.
## Timeline
What follows is the timeline of the actions we have taken since we learned about the vulnerability, in order to minimize its impact:
- Feb 12th, 2025 (Wed), 10 pm CET: we learned about the vulnerability (thanks [Ivan Vlahov](https://github.com/vlahovivan)!)
- Feb 13th (Thu):
- Made an action plan on how to handle the incident, including how we will execute the coordinated disclosure.
- Fixed all the versions of the Open SaaS template, to prevent new projects from being affected.
- Feb 14th (Fri):
- Wrote the "Incident Notification" document with a detailed explanation of the problem and the suggested fix.
- Compiled a list of the people we know are deploying Open SaaS / Wasp apps and privately shared the "Incident Notification" document with them, giving them ~ a week of head start before we go more public with the incident.
- Reviewed all the other Wasp templates and example apps for similar security issues.
- Started a deep (re)review of all the Open SaaS code (that will continue into the next week).
- Feb 17th (Mon):
- Continued deep review of Open SaaS code.
- Feb 18th (Tue):
- Continued deep review of Open SaaS code.
- Finalized first draft of this Incident Report document.
- Feb 19th (Wed):
- Continued deep review of Open SaaS code.
- Feb 20th (Thu):
- Continued deep review of Open SaaS code.
- Notified our Discord community about the incident by sharing the "Incident Notification" document with them, giving them a week of head start before we go fully public with the incident.
- Feb 21st (Fri):
- Finalized the deep review of the Open SaaS code (while continuing with the code improvements).
- Feb 26th (Wed):
- Went public with the incident by publishing and sharing this Incident Report.

View File

@ -0,0 +1,97 @@
---
title: "Meet Marko Saric, Co-founder of Privacy-friendly Plausible Analytics"
date: 2025-02-27
tags:
- webdev
- saas
authors: milica
---
import { Image } from 'astro:assets';
import StarOpenSaaSCTA from '../../../components/StarOpenSaaSCTA.astro';
import plausibleCommunity from '../../../assets/plausible/plausible-community.png';
In this interview, [Marko Saric](https://x.com/markosaric) shared his thoughts on privacy and running a bootstrapped SaaS business. [Plausible](https://plausible.io/) integration is already [available in Open SaaS](https://docs.opensaas.sh/guides/analytics/#plausible) as a privacy-friendly alternative to Google Analytics. We hope this interview helps you understand the value of such a product, and the nature of running an open source business.
Here's a few other things we've covered in this interview:
- Tackling big tech privacy issues.
- How bootstrapping your business fuels independence and transparency.
- Real, practical advice for growing your SaaS the smart way.
Let's dive in!
## Can you share a bit about your background and what led you to start Plausible?
I'm Marko Saric, co-founder of Plausible Analytics.
My journey with Plausible began with a growing awareness of the privacy issues surrounding Google and its products. For many years, I was a user of Google's services but over time (and thanks to Snowden, Cambridge Analytica and other privacy scandals), I became more aware of the negative aspects of surveillance capitalism. This led me to explore better, more ethical alternatives to the big tech products.
I started sharing these alternatives on my blog which is how I connected with my co-founder Uku. We both had experience in tech and a shared vision of working on a privacy-friendly analytics tool so we decided to work together on Plausible. I'm focused on marketing and communication side of things while Uku is focused on design and development.
## For those unfamiliar with Plausible, how would you describe its core mission in just a few sentences?
Plausible Analytics is an easy to use, lightweight, open source and privacy-friendly analytics tool. Our mission is to provide website owners with useful insights while respecting visitor privacy.
We have been working on Plausible for more than 6 years now, have more than 14,000 active subscribers at this point and have counted more than 136 billion pageviews so far.
<div className="flex justify-center">
<img src="https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExYnJ0MzBudXNsd2Nnb2VtYWs3dWQ3ZXJjNTd1amh3bjMwaGhxbmwybiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3oKIPEqDGUULpEU0aQ/giphy.gif" alt="Data is everywhere!" />
</div>
## Plausible is bootstrapped and open-source—what made you choose this path instead of taking the more common VC route?
We chose to bootstrap and open source Plausible because we wanted to maintain control and independence while also being more privacy-friendly and transparent.
Both of us have worked at venture funded startups in the past and neither of us had good experiences with investors so going bootstrapped was pretty much the way to do this if we wanted to do things our way.
We're in the privacy niche so open sourcing our product allows us to build trust as people can inspect our code to verify that our actions match our words. People cannot do that with Google Analytics and other competing products.
>Just like Plausible, Wasp is an open-source project too! We'd appreciate it if you could [star Wasp on GitHub as a sign of support](https://github.com/wasp-lang/wasp)! ⭐️
## Do you have any advice for people who are considering bootstrapping their company? Do you have any books or podcasts to recommend?
I think it's a good idea to start bootstrapped even if you do wish to get funded. You should focus on creating a great product that solves a real problem and on spreading the word about it. If you do that well, you'll have investors reaching out to you even if you don't want or need them.
I recommend reading **"Rework" by Jason Fried and David Heinemeier Hansson**. It offers unconventional but valuable insights into running a startup.
Another good book is **"This Is Marketing" by Seth Godin**. It's about how many startups confuse marketing with spending money on advertising, spamming, interrupting, being annoying and other hacks and tricks. That's not marketing. Marketing is communication.
## How did you get your first customers?
Our first customers came through community engagement and the "build in public" movement. We shared our journey, steps taken and product development openly on our blog, social media and niche communities such as Indie Hackers. That's how we got the early beta users and some of those became our first subscribers too.
## What were the biggest challenges you faced while building and growing Plausible?
The first year was pretty challenging in terms of growth. Uku was working alone on Plausible trying to do both development and marketing. This is pretty much an impossible task. The growth was very slow and we made it to about 100 subscribers and $400 MRR some 14 months into the existence of Plausible.
That's when Uku decided to look for a marketing co-founder and that's how we found me. Being two co-founders helped us put more time and effort into marketing and communication. One of the first things we did when I joined was to change our positioning to make it crystal clear and easy to understand what we do, what we stand for and how we compare to Google Analytics (the biggest name in our market). And then we started publishing educational and informative content covering topics such as privacy, open source, bootstrapping and startup marketing .
I have written more about the changes we made in these early days [in this post](https://plausible.io/blog/blog-post-changed-my-startup).
## Which growth strategies have been the most effective?
We have a boring marketing strategy and we say no to all the growth hacks and other best marketing practices. Content marketing has been our most effective growth strategy. As an example, the first blog post that I published (Why You Should Remove Google Analytics from Your Site) went viral on Hacker News. It drove some good traffic to our site leading to an increase in brand awareness.
What matters is doing quality work and staying consistent with it over a longer period of time so we continued to publish multiple blog posts per week for over a year. Thanks to that work, we've been fortunate enough to achieve the viral moments on Hacker News multiple times over those first 2-3 years.
I have shared more about our early years, marketing steps we've taken, lessons we've learned and things we have achieved [in blog posts such as this one](https://plausible.io/blog/open-source-saas). Our analytics dashboard is open to the public so it's possible to see the progress we've made since day one [in our stats](https://plausible.io/plausible.io?period=all&keybindHint=A).
## What role has the community played in Plausible's growth? Have there been any surprising or particularly impactful contributions from the community?
The community has helped shape our product and spread the word about our mission.
We have an open roadmap and listen to the product feedback which determines our development prioritization. This is where feature requests and other feedback is very valuable to us. We pretty much pick the most upvoted feature and work on that.
As mentioned earlier, we don't do any traditional marketing as in we don't do any paid advertising nor pay anyone to recommend Plausible. This means that most of our growth comes from people who love using Plausible and who share their experiences with the world. Without people spreading the word about Plausible it would be difficult for us to do what we do. So that's why community contributions is vital for us.
<Image src={plausibleCommunity} alt="Plausible Community" loading="lazy" />
## What's next for Plausible? Are there any upcoming features or improvements you're particularly excited about?
We're focused on continuing to improve Plausible and making it even more useful and competitive while staying true to our mission and meeting rigorous standards for stability, security and privacy.
Our developers are currently working on the top two most upvoted feature requests from our public feedback board (scroll depth and saved segments) so that's very exciting. It would be great to release these two big features soon!
---
Just like Plausible, Wasp is an open-source project too! We'd appreciate it if you could [star Wasp on GitHub as a sign of support](https://github.com/wasp-lang/wasp)! ⭐️

View File

@ -30,10 +30,13 @@ These are the steps necessary for you to deploy your app. We recommend you follo
Each of these steps is covered in more detail below.
### Prerequisites
Make sure you've got all your API keys and environment variables set up before you deploy.
#### AWS S3 CORS configuration
If you're storing files in AWS S3, ensure you've listed your production domain
in the bucket's CORS configuration under `AllowedOrigins`. Check the [File
uploading guide](/guides/file-uploading/#change-the-cors-settings) for details.
#### Env Vars
Make sure you've got all your API keys and environment variables set up before you deploy.
##### Payment Processor Vars
In the [Payments Processor integration guide](/guides/payments-integration/), you set up your API keys using test keys and test product ids. You'll need to get the live/production versions of those keys. To get these, repeat the instructions in the [Integration Guide](/guides/payments-integration/) without being in test mode. Add the new keys to your deployed environment secrets.

View File

@ -11,6 +11,7 @@ import defaultSettings from '@assets/file-uploads/default-settings.png';
import newBucket from '@assets/file-uploads/new-bucket.png';
import permissions from '@assets/file-uploads/permissions.png';
import cors from '@assets/file-uploads/cors.png';
import corsExample from '@assets/file-uploads/cors-example.png';
import username from '@assets/file-uploads/username.png';
import keys from '@assets/file-uploads/keys.png';
@ -61,52 +62,62 @@ To do so, follow the steps in this external guide: [Creating IAM users and S3 bu
Once you are logged in with your IAM user, you'll need to create an S3 bucket to store your files.
1. Navigate to the S3 service in the AWS console
<Image src={findS3} alt="find s3" loading="lazy" />
<Image src={findS3} alt="find s3" loading="lazy" />
2. Click on the `Create bucket` button
<Image src={createBucket} alt="create bucket" loading="lazy" />
<Image src={createBucket} alt="create bucket" loading="lazy" />
3. Fill in the bucket name and region
4. **Leave all the settings as default** and click `Create bucket`
<Image src={defaultSettings} alt="bucket settings" loading="lazy" />
<Image src={defaultSettings} alt="bucket settings" loading="lazy" />
### Change the CORS settings
Now we need to change some permissions on the bucket to allow for file uploads from your app.
1. Click on the bucket you just created
<Image src={newBucket} alt="new bucket" loading="lazy" />
<Image src={newBucket} alt="new bucket" loading="lazy" />
2. Click on the `Permissions` tab
<Image src={permissions} alt="permissions" loading="lazy" />
<Image src={permissions} alt="permissions" loading="lazy" />
3. Scroll down to the `Cross-origin resource sharing (CORS)` section and click `Edit`
<Image src={cors} alt="cors" loading="lazy" />
5. Paste the following CORS configuration and click `Save changes`:
```json
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"PUT",
"GET"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]
```
<Image src={cors} alt="cors" loading="lazy" />
5. Insert the correct CORS configuration and click `Save changes`. You can
copy-paste most of the config below, but **you must edit the
`AllowedOrigins` field** to fit your app. Include `http://localhost:3000` for
local development, and `https://<your domain>` for production.
If you don't yet have a domain name, just list `http://localhost:3000` for
now. We'll remind you to add your domain before deploying to production in
the [Deployment docs](/guides/deploying/#aws-s3-cors-configuration).
```json {11,12}
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"PUT",
"GET"
],
"AllowedOrigins": [
"http://localhost:3000",
"https://<your-domain>"
],
"ExposeHeaders": []
}
]
```
As an example, here are the CORS permissions for this site - https://opensaas.sh:
<Image src={corsExample} alt="cors-example" loading="lazy" />
### Get your AWS S3 credentials
Now that you have your S3 bucket set up, you'll need to get your S3 credentials to use in your app.
1. Click on your username in the top right corner of the AWS console and select `Security Credentials`
<Image src={username} alt="username" loading="lazy" />
<Image src={username} alt="username" loading="lazy" />
2. Scroll down to the `Access keys` section
3. Click on `Create Access Key`
4. Select the `Application running on an AWS service` option and create the access key
<Image src={keys} alt="keys" loading="lazy" />
<Image src={keys} alt="keys" loading="lazy" />
5. Copy the `Access key ID` and `Secret access key` and paste them in your `src/app/.env.server` file:
```sh
AWS_S3_IAM_ACCESS_KEY=ACK...