Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create security page and document request identity #431

Merged
merged 8 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions code_snippets/java/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies {
implementation("dev.restate:sdk-http-vertx:$restateVersion")
implementation("dev.restate:sdk-lambda:$restateVersion")
implementation("dev.restate:sdk-serde-jackson:$restateVersion")
implementation("dev.restate:sdk-request-identity:$restateVersion")

// Jackson parameter names
// https://github.com/FasterXML/jackson-modules-java8/tree/2.14/parameter-names
Expand Down
19 changes: 19 additions & 0 deletions code_snippets/java/src/main/java/develop/ServingIdentity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package develop;

// <start_here>
import dev.restate.sdk.auth.signing.RestateRequestIdentityVerifier;
import dev.restate.sdk.http.vertx.RestateHttpEndpointBuilder;

class MySecureApp {
public static void main(String[] args) {
RestateHttpEndpointBuilder.builder()
.bind(new MyService())
.bind(new MyVirtualObject())
.bind(new MyWorkflow())
.withRequestIdentityVerifier(
RestateRequestIdentityVerifier.fromKeys(
"publickeyv1_w7YHemBctH5Ck2nQRQ47iBBqhNHy4FV7t2Usbye2A6f"))
.buildAndListen();
}
}
// <end_here>
1 change: 1 addition & 0 deletions code_snippets/kotlin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies {
implementation("dev.restate:sdk-common:$restateVersion")
implementation("dev.restate:sdk-http-vertx:$restateVersion")
implementation("dev.restate:sdk-lambda:$restateVersion")
implementation("dev.restate:sdk-request-identity:$restateVersion")

implementation("org.apache.logging.log4j:log4j-core:2.20.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
Expand Down
19 changes: 19 additions & 0 deletions code_snippets/kotlin/src/main/kotlin/develop/ServingIdentity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package develop

// <start_here>
import dev.restate.sdk.auth.signing.RestateRequestIdentityVerifier
import dev.restate.sdk.http.vertx.RestateHttpEndpointBuilder

fun main() {
RestateHttpEndpointBuilder.builder()
.bind(MyService())
.bind(MyVirtualObject())
.bind(MyWorkflow())
.withRequestIdentityVerifier(
RestateRequestIdentityVerifier.fromKeys(
jackkleeman marked this conversation as resolved.
Show resolved Hide resolved
"publickeyv1_w7YHemBctH5Ck2nQRQ47iBBqhNHy4FV7t2Usbye2A6f"
)
)
.buildAndListen()
}
// <end_here>
10 changes: 10 additions & 0 deletions code_snippets/ts/src/develop/serving.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,13 @@ const http2Handler = restate
const httpServer = http2.createServer(http2Handler);
httpServer.listen();
// <end_custom_endpoint>

// <start_identity>
restate
.endpoint()
.bind(myService)
.bind(myVirtualObject)
.bind(myWorkflow)
.withIdentityV1("publickeyv1_w7YHemBctH5Ck2nQRQ47iBBqhNHy4FV7t2Usbye2A6f")
.listen();
// <end_identity>
25 changes: 24 additions & 1 deletion docs/develop/java/serving.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,27 @@ CODE_LOAD::kotlin/src/main/kotlin/develop/ServingVirtualThreads.kt

</TabItem>
</Tabs>
</details>
</details>

jackkleeman marked this conversation as resolved.
Show resolved Hide resolved
## Validating request identity

SDKs can validate that incoming requests come from a particular Restate
instance. You can find out more about request identity in the
[Security docs](/operate/security#locking-down-service-access). You will need
to use the request identity dependency, for example in Gradle:
```kotlin
implementation("dev.restate:sdk-request-identity:VAR::JAVA_SDK_VERSION")
```

<Tabs groupId="sdk" queryString>
<TabItem value="java" label="Java">
```java
CODE_LOAD::java/src/main/java/develop/ServingIdentity.java
```
</TabItem>
<TabItem value="kotlin" label="Kotlin">
```kotlin
CODE_LOAD::kotlin/src/main/kotlin/develop/ServingIdentity.kt
```
</TabItem>
</Tabs>
9 changes: 9 additions & 0 deletions docs/develop/ts/serving.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,12 @@ for guidance on how to deploy your services on AWS Lambda.
<Admonition type="tip" title={"Run on Lambda without handler changes"}>
The implementation of your services and handlers remains the same for both deployment options.
</Admonition>

## Validating request identity

SDKs can validate that incoming requests come from a particular Restate
instance. You can find out more about request identity in the [Security docs](/operate/security#locking-down-service-access)

```typescript
CODE_LOAD::ts/src/develop/serving.ts#identity
```
24 changes: 5 additions & 19 deletions docs/operate/registration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -170,22 +170,8 @@ This will forcefully overwrite the existing service deployment with the same URI
Forcing a deployment registration is a feature designed to simplify local Restate service development, and should never be used in a production Restate deployment, as it potentially breaks all the in-flight invocations to that deployment.
</Admonition>

## Private services
<CH.Section>
When registering an endpoint, every service is by default reachable via HTTP requests.

You can configure a service as `private`, such that you can't invoke it over HTTP, through the [Admin API](focus://1[15:28]) ([docs](/references/admin-api#tag/service/operation/modify_service)):

```shell
curl -X PATCH localhost:9070/services/MyService \
-H 'content-type: application/json' \
-d '{"public": false}'
```

You can revert it back to public with [`{"public": true}`](focus://3).

<Admonition type="info">
Private services can still be invoked by other handlers via the SDK.
</Admonition>

</CH.Section>
<Admonition type="info">
After registration, services can also be marked as 'private' to prevent them
from being invoked through the ingress. See the
[Security docs](/operate/security#private-services) for more information.
</Admonition>
99 changes: 99 additions & 0 deletions docs/operate/security.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
title: "Security"
sidebar_position: 6
description: "Restrict access to Restate services"
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import Admonition from '@theme/Admonition';

# Security

## Private services
<CH.Section>
When registering an endpoint, every service is by default reachable via HTTP requests to the ingress.

You can configure a service as `private`, such that you can't invoke it over HTTP, through the [Admin API](focus://1[15:28]) ([docs](/references/admin-api#tag/service/operation/modify_service)):

```shell
curl -X PATCH localhost:9070/services/MyService \
-H 'content-type: application/json' \
-d '{"public": false}'
```

You can revert it back to public with [`{"public": true}`](focus://3).

<Admonition type="info">
Private services can still be invoked by other handlers via the SDK.
</Admonition>
</CH.Section>

## Locking down service access
Only Restate needs to be able to make requests to your services - requests from
other services or from the ingress will always go via the Restate runtime. It is
therefore advisable to ensure that only Restate can reach your service.
Unrestricted access to the services is dangerous. If you're working with multiple Restate instances, you also may want to check that requests are
coming from the right instance.

To make this easier, Restate has a native request identity feature which can be
used in the SDK to cryptographically verify that requests have come from a
particular Restate instance.

To get started, you need an ED25519 private key, which you can generate using
openssl as follows:
```bash
openssl genpkey -algorithm ed25519 -outform pem -out private.pem
```

You can provide the path to the key to Restate on startup using an environment
variable: `RESTATE_REQUEST_IDENTITY_PRIVATE_KEY_PEM_FILE=private.pem`.

On start, Restate will log out the public key in a convenient compact
format:
```
INFO restate_service_client::request_identity::v1
Loaded request identity key
kid: "publickeyv1_w7YHemBctH5Ck2nQRQ47iBBqhNHy4FV7t2Usbye2A6f"
path: private.pem
```

You can also obtain the public key in this format without running Restate using
the following script:
<details className="grey-details">
<summary>generate.sh</summary>
```shell
#!/usr/bin/env bash
set -euo pipefail

# generate private key
openssl genpkey -algorithm ed25519 -outform pem -out private.pem
echo "Wrote private key to private.pem"

base58_chars="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
# encode public key
encoded=$(openssl ec -in private.pem -inform pem -pubout -outform der -out /dev/stdout 2>/dev/null |
tail -c +13 |
basenc --base16 "${1:-/dev/stdin}" -w0 |
if
read
[[ $REPLY =~ ^((00)*)(([[:xdigit:]]{2})*) ]]
echo -n "${BASH_REMATCH[1]//00/1}" # leading 0s -> 1
(( ${#BASH_REMATCH[3]} > 0 ))
then
dc -e "16i0${BASH_REMATCH[3]^^} Ai[58~rd0<x]dsxx+f" | # hex bytes to indexes into the char string
while read -r
do echo -n "${base58_chars:REPLY:1}"
done
fi)

echo -n "publickeyv1_${encoded}" > public-key
echo "Wrote publickeyv1_${encoded} to public-key"
```
</details>

The string `publickeyv1_w7YHemBctH5Ck2nQRQ47iBBqhNHy4FV7t2Usbye2A6f` does not
need to be kept secret and can be safely included in your code when providing to the
SDK. To learn how to provide the public key to the SDK, see the serving docs
for [TypeScript](/develop/ts/serving#validating-request-identity)
and [Java](/develop/ts/serving#validating-request-identity)
4 changes: 2 additions & 2 deletions docs/operate/versioning.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ You cannot remove a single service, but you can remove the deployment containing
1. Make sure that there are no other handlers with business logic that calls this service.
2. If several services are bundled in the same deployment, you can't remove only one of them. You have to remove the whole deployment.
So make sure that you first deploy the services you want to keep in a separate new deployment.
4. [Make the service private](/operate/registration#private-services) to avoid accepting new HTTP requests.
4. [Make the service private](/operate/security#private-services) to avoid accepting new HTTP requests.
5. Check whether the service has pending invocations via _`restate services status`_, and wait until the service is drained (i.e. no ongoing invocations).

**When all prerequisites are fulfilled**, you can remove the deployment containing the service via:
Expand All @@ -189,4 +189,4 @@ curl -X DELETE localhost:9070/deployments/dp_14LsPzGz9HBxXIeBoH5wYUh?force=true
```

</TabItem>
</Tabs>
</Tabs>
Loading