You will be provided with a partial code base and an issue statement explaining a problem to resolve.
<issue>
S3Publisher should set Cache Control Headers
The S3Publisher is currently not setting Cache Control Headers. We should cache-control: public, maxage=N

N depends on the type of file:
- for hours index files, it should be rather low (5 minutes)
- for days index files, it should be higher (2 hours)
- for all generated aggregate files, it should be 24 hours

E-Tag should already been set, but should also be verified.

</issue>

<code>
[start of README.md]
1 <!-- markdownlint-disable MD041 -->
2 <h1 align="center">
3     Corona-Warn-App Server
4 </h1>
5 
6 <p align="center">
7     <a href="https://github.com/corona-warn-app/cwa-server/commits/" title="Last Commit"><img src="https://img.shields.io/github/last-commit/corona-warn-app/cwa-server?style=flat"></a>
8     <a href="https://github.com/corona-warn-app/cwa-server/issues" title="Open Issues"><img src="https://img.shields.io/github/issues/corona-warn-app/cwa-server?style=flat"></a>
9     <a href="https://circleci.com/gh/corona-warn-app/cwa-server" title="Build Status"><img src="https://circleci.com/gh/corona-warn-app/cwa-server.svg?style=shield&circle-token=4ab059989d10709df19eb4b98ab7c121a25e981a"></a>
10         <a href="https://sonarcloud.io/dashboard?id=corona-warn-app_cwa-server" title="Quality Gate"><img src="https://sonarcloud.io/api/project_badges/measure?project=corona-warn-app_cwa-server&metric=alert_status"></a>
11         <a href="https://sonarcloud.io/component_measures?id=corona-warn-app_cwa-server&metric=Coverage&view=list" title="Coverage"><img src="https://sonarcloud.io/api/project_badges/measure?project=corona-warn-app_cwa-server&metric=coverage"></a>
12     <a href="https://github.com/corona-warn-app/cwa-server/blob/master/LICENSE" title="License"><img src="https://img.shields.io/badge/License-Apache%202.0-green.svg?style=flat"></a>
13 </p>
14 
15 <p align="center">
16   <a href="#development">Development</a> •
17   <a href="#service-apis">Service APIs</a> •
18   <a href="#documentation">Documentation</a> •
19   <a href="#support-and-feedback">Support</a> •
20   <a href="#how-to-contribute">Contribute</a> •
21   <a href="#contributors">Contributors</a> •
22   <a href="#repositories">Repositories</a> •
23   <a href="#licensing">Licensing</a>
24 </p>
25 
26 The goal of this project is to develop the official Corona-Warn-App for Germany based on the exposure notification API from [Apple](https://www.apple.com/covid19/contacttracing/) and [Google](https://www.google.com/covid19/exposurenotifications/). The apps (for both iOS and Android) use Bluetooth technology to exchange anonymous encrypted data with other mobile phones (on which the app is also installed) in the vicinity of an app user's phone. The data is stored locally on each user's device, preventing authorities or other parties from accessing or controlling the data. This repository contains the **implementation of the server for encryption keys** for the Corona-Warn-App. This implementation is still a **work in progress**, and the code it contains is currently alpha-quality code.
27 
28 In this documentation, Corona-Warn-App services are also referred to as CWA services.
29 
30 ## Architecture Overview
31 
32 You can find the architecture overview [here](/docs/architecture-overview.md), which will give you
33 a good starting point in how the backend services interact with other services, and what purpose
34 they serve.
35 
36 ## Development
37 
38 After you've checked out this repository, you can run the application in one of the following ways:
39 
40 * As a [Docker](https://www.docker.com/)-based deployment on your local machine. You can run either:
41   * Single components using the respective Dockerfile or
42   * The full backend using the Docker Compose (which is considered the most convenient way)
43 * As a [Maven](https://maven.apache.org)-based build on your local machine.
44   If you want to develop something in a single component, this approach is preferable.
45 
46 ### Docker-Based Deployment
47 
48 If you want to use Docker-based deployment, you need to install Docker on your local machine. For more information about downloading and installing Docker, see the [official Docker documentation](https://docs.docker.com/get-docker/).
49 
50 #### Running the Full CWA Backend Using Docker Compose
51 
52 For your convenience, a full setup including the generation of test data has been prepared using [Docker Compose](https://docs.docker.com/compose/reference/overview/). To build the backend services, run ```docker-compose build``` in the repository's root directory. A default configuration file can be found under ```.env```in the root folder of the repository. The default values for the local Postgres and Zenko Cloudserver should be changed in this file before docker-compose is run.
53 
54 Once the services are built, you can start the whole backend using ```docker-compose up```.
55 The distribution service runs once and then finishes. If you want to trigger additional distribution runs, run ```docker-compose run distribution```.
56 
57 The docker-compose contains the following services:
58 
59 Service           | Description | Endpoint and Default Credentials
60 ------------------|-------------|-----------
61 submission        | The Corona-Warn-App submission service                                                      | `http://localhost:8000` <br> `http://localhost:8006` (for actuator endpoint)
62 distribution      | The Corona-Warn-App distribution service                                                    | NO ENDPOINT
63 postgres          | A [postgres] database installation                                                          | `postgres:8001` <br> Username: postgres <br> Password: postgres
64 pgadmin           | A [pgadmin](https://www.pgadmin.org/) installation for the postgres database                | `http://localhost:8002` <br> Username: user@domain.com <br> Password: password
65 cloudserver       | [Zenko CloudServer] is a S3-compliant object store  | `http://localhost:8003/` <br> Access key: accessKey1 <br> Secret key: verySecretKey1
66 verification-fake | A very simple fake implementation for the tan verification.                                 | `http://localhost:8004/version/v1/tan/verify` <br> The only valid tan is "b69ab69f-9823-4549-8961-c41sa74b2f36"
67 
68 ##### Known Limitation
69 
70 In rare cases the docker-compose runs into a timing issue if the distribution service starts before the bucket of the objectstore was created. This is not a big issue as you can simply run ```docker-compose run distribution``` to trigger additional distribution runs after the objectstore was initialized.
71 
72 #### Running Single CWA Services Using Docker
73 
74 If you would like to build and run a single CWA service, it's considered easiest to run them in a Docker environment. You can do this using the script provided in the respective CWA service directory. The Docker script first builds the CWA service and then creates an image for the runtime, which means that there are no additional dependencies for you to install.
75 
76 To build and run the distribution service, run the following command:
77 
78 ```bash
79 ./services/distribution/build_and_run.sh
80 ```
81 
82 To build and run the submission service, run the following command:
83 
84 ```bash
85 ./services/submission/build_and_run.sh
86 ```
87 
88 The submission service is available on [localhost:8080](http://localhost:8080 ).
89 
90 ### Maven-Based Build
91 
92 If you want to actively develop in one of the CWA services, the Maven-based runtime is most suitable.
93 To prepare your machine to run the CWA project locally, we recommend that you first ensure that you've installed the following:
94 
95 * Minimum JDK Version 11: [OpenJDK](https://openjdk.java.net/) / [SapMachine](https://sap.github.io/SapMachine/)
96 * [Maven 3.6](https://maven.apache.org/)
97 * [Postgres]
98 * [Zenko CloudServer]
99 
100 #### Configure
101 
102 After you made sure that the specified dependencies are running, configure them in the respective configuration files.
103 
104 * Configure the Postgres connection in the [submission config](./services/submission/src/main/resources/application.yaml) and in the [distribution config](./services/distribution/src/main/resources/application.yaml)
105 * Configure the S3 compatible object storage in the [distribution config](./services/distribution/src/main/resources/application.yaml)
106 * Configure the private key for the distribution service, the path need to be prefixed with `file:`
107   * `VAULT_FILESIGNING_SECRET` should be the path to the private key, example available in `<repo-root>/docker-compose-test-secrets/private.pem`
108 
109 #### Build
110 
111 After you've checked out the repository, to build the project, run ```mvn install``` in your base directory.
112 
113 #### Run
114 
115 Navigate to the service you want to start and run the spring-boot:run target. The configured Postgres and the configured S3 compliant object storage are used as default. When you start the submission service, the endpoint is available on your local port 8080.
116 
117 If you want to start the submission service, for example, you start it as follows:
118 
119 ```bash
120   cd services/submission/
121   mvn spring-boot:run
122 ```
123 
124 #### Debugging
125 
126 To enable the `DEBUG` log level, you can run the application using the Spring `dev` profile.
127 
128 ```bash
129 mvn spring-boot:run -Dspring-boot.run.profiles=dev
130 ```
131 
132 To be able to set breakpoints (e.g. in IntelliJ), it may be necessary to use the ```-Dspring-boot.run.fork=false``` parameter.
133 
134 ## Service APIs
135 
136 The API that is being exposed by the backend services is documented in an [OpenAPI](https://www.openapis.org/) specification. The specification files are available at the following locations:
137 
138 Service      | OpenAPI Specification
139 -------------|-------------
140 Submission Service        | [services/submission/api_v1.json](https://github.com/corona-warn-app/cwa-server/raw/master/services/submission/api_v1.json)
141 Distribution Service      | [services/distribution/api_v1.json)](https://github.com/corona-warn-app/cwa-server/raw/master/services/distribution/api_v1.json)
142 
143 ## Spring Profiles
144 
145 ### Distribution
146 
147 Profile      | Effect
148 -------------|-------------
149 `dev`        | Turns the log level to `DEBUG` and sets the app package ID in the export packages' signature info to `de.rki.coronawarnapp-dev` so that test certificates (instead of production certificates) will be used for client-side validation.
150 `cloud`      | Removes default values for the `datasource` and `objectstore` configurations.
151 `demo`       | Includes incomplete days and hours into the distribution run, thus creating aggregates for the current day and the current hour (and including both in the respective indices). When running multiple distributions in one hour with this profile, the date aggregate for today and the hours aggregate for the current hour will be updated and overwritten.
152 `testdata`   | Causes test data to be inserted into the database before each distribution run. By default, around 1000 random diagnosis keys will be generated per hour. If there are no diagnosis keys in the database yet, random keys will be generated for every hour from the beginning of the retention period (14 days ago at 00:00 UTC) until one hour before the present hour. If there are already keys in the database, the random keys will be generated for every hour from the latest diagnosis key in the database (by submission timestamp) until one hour before the present hour (or none at all, if the latest diagnosis key in the database was submitted one hour ago or later).
153 
154 ### Submission
155 
156 Profile      | Effect
157 -------------|-------------
158 `dev`        | Turns the log level to `DEBUG`.
159 `cloud`      | Removes default values for the `datasource` configuration.
160 
161 ## Documentation
162 
163 The full documentation for the Corona-Warn-App can be found in the [cwa-documentation](https://github.com/corona-warn-app/cwa-documentation) repository. The documentation repository contains technical documents, architecture information, and whitepapers related to this implementation.
164 
165 ## Support and Feedback
166 
167 The following channels are available for discussions, feedback, and support requests:
168 
169 | Type                     | Channel                                                |
170 | ------------------------ | ------------------------------------------------------ |
171 | **General Discussion**   | <a href="https://github.com/corona-warn-app/cwa-documentation/issues/new/choose" title="General Discussion"><img src="https://img.shields.io/github/issues/corona-warn-app/cwa-documentation/question.svg?style=flat-square"></a> </a>   |
172 | **Concept Feedback**    | <a href="https://github.com/corona-warn-app/cwa-documentation/issues/new/choose" title="Open Concept Feedback"><img src="https://img.shields.io/github/issues/corona-warn-app/cwa-documentation/architecture.svg?style=flat-square"></a>  |
173 | **Backend Issue**    | <a href="https://github.com/corona-warn-app/cwa-server/issues/new/choose" title="Open Backend Issue"><img src="https://img.shields.io/github/issues/corona-warn-app/cwa-server?style=flat-square"></a>  |
174 | **Other Requests**    | <a href="mailto:corona-warn-app.opensource@sap.com" title="Email CWA Team"><img src="https://img.shields.io/badge/email-CWA%20team-green?logo=mail.ru&style=flat-square&logoColor=white"></a>   |
175 
176 ## How to Contribute
177 
178 Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](./CONTRIBUTING.md). By participating in this project, you agree to abide by its [Code of Conduct](./CODE_OF_CONDUCT.md) at all times.
179 
180 ## Contributors
181 
182 The German government has asked SAP and Deutsche Telekom to develop the Corona-Warn-App for Germany as open source software. Deutsche Telekom is providing the network and mobile technology and will operate and run the backend for the app in a safe, scalable and stable manner. SAP is responsible for the app development, its framework and the underlying platform. Therefore, development teams of SAP and Deutsche Telekom are contributing to this project. At the same time our commitment to open source means that we are enabling -in fact encouraging- all interested parties to contribute and become part of its developer community.
183 
184 ## Repositories
185 
186 The following public repositories are currently available for the Corona-Warn-App:
187 
188 | Repository          | Description                                                           |
189 | ------------------- | --------------------------------------------------------------------- |
190 | [cwa-documentation] | Project overview, general documentation, and white papers            |
191 | [cwa-server]        | Backend implementation for the Apple/Google exposure notification API|
192 | [cwa-verification-server] | Backend implementation of the verification process|
193 
194 [cwa-documentation]: https://github.com/corona-warn-app/cwa-documentation
195 [cwa-server]: https://github.com/corona-warn-app/cwa-server
196 [cwa-verification-server]: https://github.com/corona-warn-app/cwa-verification-server
197 [Postgres]: https://www.postgresql.org/
198 [HSQLDB]: http://hsqldb.org/
199 [Zenko CloudServer]: https://github.com/scality/cloudserver
200 
201 ## Licensing
202 
203 Copyright (c) 2020 SAP SE or an SAP affiliate company.
204 
205 Licensed under the **Apache License, Version 2.0** (the "License"); you may not use this file except in compliance with the License.
206 
207 You may obtain a copy of the License at <https://www.apache.org/licenses/LICENSE-2.0>.
208 
209 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the [LICENSE](./LICENSE) for the specific language governing permissions and limitations under the License.
210 
[end of README.md]
[start of /dev/null]
1 
[end of /dev/null]
[start of services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreAccess.java]
1 /*
2  * Corona-Warn-App
3  *
4  * SAP SE and all other contributors /
5  * copyright owners license this file to you under the Apache
6  * License, Version 2.0 (the "License"); you may not use this
7  * file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  * http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing,
13  * software distributed under the License is distributed on an
14  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15  * KIND, either express or implied.  See the License for the
16  * specific language governing permissions and limitations
17  * under the License.
18  */
19 
20 package app.coronawarn.server.services.distribution.objectstore;
21 
22 import app.coronawarn.server.services.distribution.config.DistributionServiceConfig;
23 import app.coronawarn.server.services.distribution.config.DistributionServiceConfig.ObjectStore;
24 import app.coronawarn.server.services.distribution.objectstore.publish.LocalFile;
25 import io.minio.MinioClient;
26 import io.minio.PutObjectOptions;
27 import io.minio.Result;
28 import io.minio.errors.InvalidEndpointException;
29 import io.minio.errors.InvalidPortException;
30 import io.minio.errors.MinioException;
31 import io.minio.messages.DeleteError;
32 import io.minio.messages.Item;
33 import java.io.IOException;
34 import java.security.GeneralSecurityException;
35 import java.util.ArrayList;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.stream.Collectors;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41 import org.springframework.stereotype.Component;
42 
43 /**
44  * <p>Grants access to the S3 compatible object storage hosted by Telekom in Germany, enabling
45  * basic functionality for working with files.</p>
46  * <br>
47  * Make sure the following properties are available on the env:
48  * <ul>
49  * <li>services.distribution.objectstore.endpoint</li>
50  * <li>services.distribution.objectstore.bucket</li>
51  * <li>services.distribution.objectstore.accessKey</li>
52  * <li>services.distribution.objectstore.secretKey</li>
53  * <li>services.distribution.objectstore.port</li>
54  * </ul>
55  */
56 @Component
57 public class ObjectStoreAccess {
58 
59   private static final Logger logger = LoggerFactory.getLogger(ObjectStoreAccess.class);
60 
61   private static final String DEFAULT_REGION = "eu-west-1";
62 
63   private final boolean isSetPublicReadAclOnPutObject;
64 
65   private final String bucket;
66 
67   private final MinioClient client;
68 
69   /**
70    * Constructs an {@link ObjectStoreAccess} instance for communication with the specified object store endpoint and
71    * bucket.
72    *
73    * @param distributionServiceConfig The config properties
74    * @throws IOException              When there were problems creating the S3 client
75    * @throws GeneralSecurityException When there were problems creating the S3 client
76    * @throws MinioException           When there were problems creating the S3 client
77    */
78   ObjectStoreAccess(DistributionServiceConfig distributionServiceConfig)
79       throws IOException, GeneralSecurityException, MinioException {
80     this.client = createClient(distributionServiceConfig.getObjectStore());
81 
82     this.bucket = distributionServiceConfig.getObjectStore().getBucket();
83     this.isSetPublicReadAclOnPutObject = distributionServiceConfig.getObjectStore().isSetPublicReadAclOnPutObject();
84 
85     if (!this.client.bucketExists(this.bucket)) {
86       throw new IllegalArgumentException("Supplied bucket does not exist " + bucket);
87     }
88   }
89 
90   private MinioClient createClient(ObjectStore objectStore)
91       throws InvalidPortException, InvalidEndpointException {
92     if (isSsl(objectStore)) {
93       return new MinioClient(
94           objectStore.getEndpoint(),
95           objectStore.getPort(),
96           objectStore.getAccessKey(), objectStore.getSecretKey(),
97           DEFAULT_REGION,
98           true
99       );
100     } else {
101       return new MinioClient(
102           objectStore.getEndpoint(),
103           objectStore.getPort(),
104           objectStore.getAccessKey(), objectStore.getSecretKey()
105       );
106     }
107   }
108 
109   private boolean isSsl(ObjectStore objectStore) {
110     return objectStore.getEndpoint().startsWith("https://");
111   }
112 
113   /**
114    * Stores the target file on the S3.
115    *
116    * @param localFile the file to be published
117    */
118   public void putObject(LocalFile localFile)
119       throws IOException, GeneralSecurityException, MinioException {
120     String s3Key = localFile.getS3Key();
121     PutObjectOptions options = createOptionsFor(localFile);
122 
123     logger.info("... uploading {}", s3Key);
124     this.client.putObject(bucket, s3Key, localFile.getFile().toString(), options);
125   }
126 
127   /**
128    * Deletes objects in the object store, based on the given prefix (folder structure).
129    *
130    * @param prefix the prefix, e.g. my/folder/
131    */
132   public void deleteObjectsWithPrefix(String prefix)
133       throws MinioException, GeneralSecurityException, IOException {
134     List<String> toDelete = getObjectsWithPrefix(prefix)
135         .stream()
136         .map(S3Object::getObjectName)
137         .collect(Collectors.toList());
138 
139     logger.info("Deleting {} entries with prefix {}", toDelete.size(), prefix);
140     var deletionResponse = this.client.removeObjects(bucket, toDelete);
141 
142     List<DeleteError> errors = new ArrayList<>();
143     for (Result<DeleteError> deleteErrorResult : deletionResponse) {
144       errors.add(deleteErrorResult.get());
145     }
146 
147     if (!errors.isEmpty()) {
148       throw new MinioException("Can't delete files, number of errors: " + errors.size());
149     }
150   }
151 
152   /**
153    * Fetches the list of objects in the store with the given prefix.
154    *
155    * @param prefix the prefix, e.g. my/folder/
156    * @return the list of objects
157    */
158   public List<S3Object> getObjectsWithPrefix(String prefix)
159       throws IOException, GeneralSecurityException, MinioException {
160     var objects = this.client.listObjects(bucket, prefix, true);
161 
162     var list = new ArrayList<S3Object>();
163     for (Result<Item> item : objects) {
164       list.add(S3Object.of(item.get()));
165     }
166 
167     return list;
168   }
169 
170   private PutObjectOptions createOptionsFor(LocalFile file) {
171     var options = new PutObjectOptions(file.getFile().toFile().length(), -1);
172 
173     if (this.isSetPublicReadAclOnPutObject) {
174       options.setHeaders(Map.of("x-amz-acl", "public-read"));
175     }
176 
177     return options;
178   }
179 
180 }
181 
[end of services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreAccess.java]
</code>

Here is an example of a patch file. It consists of changes to the code base. It specifies the file names, the line numbers of each change, and the removed and added lines. A single patch file can contain changes to multiple files.
<patch>
--- a/file.py
+++ b/file.py
@@ -1,27 +1,35 @@
 def euclidean(a, b):
-    while b:
-        a, b = b, a % b
-    return a
+    if b == 0:
+        return a
+    return euclidean(b, a % b)
 
 
 def bresenham(x0, y0, x1, y1):
     points = []
     dx = abs(x1 - x0)
     dy = abs(y1 - y0)
-    sx = 1 if x0 < x1 else -1
-    sy = 1 if y0 < y1 else -1
-    err = dx - dy
+    x, y = x0, y0
+    sx = -1 if x0 > x1 else 1
+    sy = -1 if y0 > y1 else 1
 
-    while True:
-        points.append((x0, y0))
-        if x0 == x1 and y0 == y1:
-            break
-        e2 = 2 * err
-        if e2 > -dy:
+    if dx > dy:
+        err = dx / 2.0
+        while x != x1:
+            points.append((x, y))
             err -= dy
-            x0 += sx
-        if e2 < dx:
-            err += dx
-            y0 += sy
+            if err < 0:
+                y += sy
+                err += dx
+            x += sx
+    else:
+        err = dy / 2.0
+        while y != y1:
+            points.append((x, y))
+            err -= dx
+            if err < 0:
+                x += sx
+                err += dy
+            y += sy
 
+    points.append((x, y))
     return points
</patch>

I need you to solve the provded issue by generating a single patch file that I can apply directly to this repository using git apply. Please respond with a single patch file in the format shown above.
Respond below:


S3Publisher should set Cache Control Headers
The S3Publisher is currently not setting Cache Control Headers. We should cache-control: public, maxage=N

N depends on the type of file:
- for hours index files, it should be rather low (5 minutes)
- for days index files, it should be higher (2 hours)
- for all generated aggregate files, it should be 24 hours

E-Tag should already been set, but should also be verified.

For the sake of simplicity, I think we can get away with just setting everything to 5 minutes. In the worst case, we need to expect requests for all available files by all 4 CDN master cache servers every 5 minutes. We serve a peak total of 356‬ files (1 version index, 1 version OpenAPI JSON, 2 country indices for diagnosis keys and parameters, 1 date index, 14 date aggregates, 14 hour indices, 14*24 hour aggregates, 1 parameter configuration). If we set everything to 5 minutes, this would lead to an average of:
* 1424‬ requests per 5 minutes
* ~285 requests per minute
* ~5 requests per second

I think the object storage should easily be able to handle that, even if for some reason the CDN edge cache servers decided to request stuff themselves.
5 Minutes is OK for now - let's keep it simple in the first iteration.
<patch>
diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreAccess.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreAccess.java
--- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreAccess.java
+++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreAccess.java
@@ -20,19 +20,17 @@
 package app.coronawarn.server.services.distribution.objectstore;
 
 import app.coronawarn.server.services.distribution.config.DistributionServiceConfig;
-import app.coronawarn.server.services.distribution.config.DistributionServiceConfig.ObjectStore;
 import app.coronawarn.server.services.distribution.objectstore.publish.LocalFile;
 import io.minio.MinioClient;
 import io.minio.PutObjectOptions;
 import io.minio.Result;
-import io.minio.errors.InvalidEndpointException;
-import io.minio.errors.InvalidPortException;
 import io.minio.errors.MinioException;
 import io.minio.messages.DeleteError;
 import io.minio.messages.Item;
 import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -58,7 +56,11 @@ public class ObjectStoreAccess {
 
   private static final Logger logger = LoggerFactory.getLogger(ObjectStoreAccess.class);
 
-  private static final String DEFAULT_REGION = "eu-west-1";
+  /**
+   * Specifies the default maximum amount of time in seconds that a published resource can be considered "fresh" when
+   * held in a cache.
+   */
+  public static final int DEFAULT_MAX_CACHE_AGE = 300;
 
   private final boolean isSetPublicReadAclOnPutObject;
 
@@ -71,14 +73,14 @@ public class ObjectStoreAccess {
    * bucket.
    *
    * @param distributionServiceConfig The config properties
+   * @param minioClient               The client used for interaction with the object store
    * @throws IOException              When there were problems creating the S3 client
    * @throws GeneralSecurityException When there were problems creating the S3 client
    * @throws MinioException           When there were problems creating the S3 client
    */
-  ObjectStoreAccess(DistributionServiceConfig distributionServiceConfig)
+  ObjectStoreAccess(DistributionServiceConfig distributionServiceConfig, MinioClient minioClient)
       throws IOException, GeneralSecurityException, MinioException {
-    this.client = createClient(distributionServiceConfig.getObjectStore());
-
+    this.client = minioClient;
     this.bucket = distributionServiceConfig.getObjectStore().getBucket();
     this.isSetPublicReadAclOnPutObject = distributionServiceConfig.getObjectStore().isSetPublicReadAclOnPutObject();
 
@@ -87,38 +89,25 @@ public class ObjectStoreAccess {
     }
   }
 
-  private MinioClient createClient(ObjectStore objectStore)
-      throws InvalidPortException, InvalidEndpointException {
-    if (isSsl(objectStore)) {
-      return new MinioClient(
-          objectStore.getEndpoint(),
-          objectStore.getPort(),
-          objectStore.getAccessKey(), objectStore.getSecretKey(),
-          DEFAULT_REGION,
-          true
-      );
-    } else {
-      return new MinioClient(
-          objectStore.getEndpoint(),
-          objectStore.getPort(),
-          objectStore.getAccessKey(), objectStore.getSecretKey()
-      );
-    }
-  }
-
-  private boolean isSsl(ObjectStore objectStore) {
-    return objectStore.getEndpoint().startsWith("https://");
+  /**
+   * Stores the target file on the S3 and sets cache control headers according to the default maximum age value.
+   *
+   * @param localFile The file to be published.
+   */
+  public void putObject(LocalFile localFile) throws IOException, GeneralSecurityException, MinioException {
+    putObject(localFile, DEFAULT_MAX_CACHE_AGE);
   }
 
   /**
-   * Stores the target file on the S3.
+   * Stores the target file on the S3 and sets cache control headers according to the specified maximum age value.
    *
-   * @param localFile the file to be published
+   * @param localFile The file to be published.
+   * @param maxAge    A cache control parameter that specifies the maximum amount of time in seconds that a resource can
+   *                  be considered "fresh" when held in a cache.
    */
-  public void putObject(LocalFile localFile)
-      throws IOException, GeneralSecurityException, MinioException {
+  public void putObject(LocalFile localFile, int maxAge) throws IOException, GeneralSecurityException, MinioException {
     String s3Key = localFile.getS3Key();
-    PutObjectOptions options = createOptionsFor(localFile);
+    PutObjectOptions options = createOptionsFor(localFile, maxAge);
 
     logger.info("... uploading {}", s3Key);
     this.client.putObject(bucket, s3Key, localFile.getFile().toString(), options);
@@ -167,12 +156,14 @@ public List<S3Object> getObjectsWithPrefix(String prefix)
     return list;
   }
 
-  private PutObjectOptions createOptionsFor(LocalFile file) {
+  private PutObjectOptions createOptionsFor(LocalFile file, int maxAge) {
     var options = new PutObjectOptions(file.getFile().toFile().length(), -1);
 
+    Map<String, String> headers = new HashMap<>(Map.of("cache-control", "public,max-age=" + maxAge));
     if (this.isSetPublicReadAclOnPutObject) {
-      options.setHeaders(Map.of("x-amz-acl", "public-read"));
+      headers.put("x-amz-acl", "public-read");
     }
+    options.setHeaders(headers);
 
     return options;
   }
diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreClientConfig.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreClientConfig.java
new file mode 100644
--- /dev/null
+++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreClientConfig.java
@@ -0,0 +1,66 @@
+/*
+ * Corona-Warn-App
+ *
+ * SAP SE and all other contributors /
+ * copyright owners license this file to you under the Apache
+ * License, Version 2.0 (the "License"); you may not use this
+ * file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package app.coronawarn.server.services.distribution.objectstore;
+
+import app.coronawarn.server.services.distribution.config.DistributionServiceConfig;
+import app.coronawarn.server.services.distribution.config.DistributionServiceConfig.ObjectStore;
+import io.minio.MinioClient;
+import io.minio.errors.InvalidEndpointException;
+import io.minio.errors.InvalidPortException;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Manages the instantiation of the {@link MinioClient} bean.
+ */
+@Configuration
+public class ObjectStoreClientConfig {
+
+  private static final String DEFAULT_REGION = "eu-west-1";
+
+  @Bean
+  public MinioClient createMinioClient(DistributionServiceConfig distributionServiceConfig)
+      throws InvalidPortException, InvalidEndpointException {
+    return createClient(distributionServiceConfig.getObjectStore());
+  }
+
+  private MinioClient createClient(ObjectStore objectStore)
+      throws InvalidPortException, InvalidEndpointException {
+    if (isSsl(objectStore)) {
+      return new MinioClient(
+          objectStore.getEndpoint(),
+          objectStore.getPort(),
+          objectStore.getAccessKey(), objectStore.getSecretKey(),
+          DEFAULT_REGION,
+          true
+      );
+    } else {
+      return new MinioClient(
+          objectStore.getEndpoint(),
+          objectStore.getPort(),
+          objectStore.getAccessKey(), objectStore.getSecretKey()
+      );
+    }
+  }
+
+  private boolean isSsl(ObjectStore objectStore) {
+    return objectStore.getEndpoint().startsWith("https://");
+  }
+}

</patch>
diff --git a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreAccessUnitTest.java b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreAccessUnitTest.java
new file mode 100644
--- /dev/null
+++ b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreAccessUnitTest.java
@@ -0,0 +1,113 @@
+/*
+ * Corona-Warn-App
+ *
+ * SAP SE and all other contributors /
+ * copyright owners license this file to you under the Apache
+ * License, Version 2.0 (the "License"); you may not use this
+ * file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package app.coronawarn.server.services.distribution.objectstore;
+
+import static java.util.Map.entry;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import app.coronawarn.server.services.distribution.config.DistributionServiceConfig;
+import app.coronawarn.server.services.distribution.objectstore.publish.LocalFile;
+import io.minio.MinioClient;
+import io.minio.PutObjectOptions;
+import java.io.File;
+import java.nio.file.Path;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.test.context.ConfigFileApplicationContextInitializer;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+@EnableConfigurationProperties(value = DistributionServiceConfig.class)
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration(classes = {MinioClient.class}, initializers = ConfigFileApplicationContextInitializer.class)
+class ObjectStoreAccessUnitTest {
+
+  private static final String EXP_S3_KEY = "fooS3Key";
+  private static final String EXP_FILE_CONTENT = "barFileContent";
+
+  private final DistributionServiceConfig distributionServiceConfig;
+  private final String expBucketName;
+  private LocalFile testLocalFile;
+  private ObjectStoreAccess objectStoreAccess;
+
+  @MockBean
+  private MinioClient minioClient;
+
+  @Autowired
+  public ObjectStoreAccessUnitTest(DistributionServiceConfig distributionServiceConfig) {
+    this.distributionServiceConfig = distributionServiceConfig;
+    this.expBucketName = distributionServiceConfig.getObjectStore().getBucket();
+  }
+
+  @BeforeEach
+  public void setUpMocks() throws Exception {
+    when(minioClient.bucketExists(any())).thenReturn(true);
+    this.objectStoreAccess = new ObjectStoreAccess(distributionServiceConfig, minioClient);
+    this.testLocalFile = setUpLocalFileMock();
+  }
+
+  private LocalFile setUpLocalFileMock() {
+    var testLocalFile = mock(LocalFile.class);
+    var testPath = mock(Path.class);
+
+    when(testLocalFile.getS3Key()).thenReturn(EXP_S3_KEY);
+    when(testLocalFile.getFile()).thenReturn(testPath);
+    when(testPath.toFile()).thenReturn(mock(File.class));
+    when(testPath.toString()).thenReturn(EXP_FILE_CONTENT);
+
+    return testLocalFile;
+  }
+
+  @Test
+  void testPutObjectSetsDefaultCacheControlHeader() throws Exception {
+    ArgumentCaptor<PutObjectOptions> options = ArgumentCaptor.forClass(PutObjectOptions.class);
+    var expHeader = entry("cache-control", "public,max-age=" + ObjectStoreAccess.DEFAULT_MAX_CACHE_AGE);
+
+    objectStoreAccess.putObject(testLocalFile);
+
+    verify(minioClient, atLeastOnce())
+        .putObject(eq(expBucketName), eq(EXP_S3_KEY), eq(EXP_FILE_CONTENT), options.capture());
+    Assertions.assertThat(options.getValue().headers()).contains(expHeader);
+  }
+
+  @Test
+  void testPutObjectSetsSpecifiedCacheControlHeader() throws Exception {
+    ArgumentCaptor<PutObjectOptions> options = ArgumentCaptor.forClass(PutObjectOptions.class);
+    var expMaxAge = 1337;
+    var expHeader = entry("cache-control", "public,max-age=" + expMaxAge);
+
+    objectStoreAccess.putObject(testLocalFile, expMaxAge);
+
+    verify(minioClient, atLeastOnce())
+        .putObject(eq(expBucketName), eq(EXP_S3_KEY), eq(EXP_FILE_CONTENT), options.capture());
+    Assertions.assertThat(options.getValue().headers()).contains(expHeader);
+  }
+}
