opensearch-sdk-java
opensearch-sdk-java copied to clipboard
[Discuss] Method to determine if REST Request originates from an extension and, if so, which
What/Why
The overarching problem the security team is trying to solve is how to ensure that requests that originate from an extension identify the extension and original requester. The auth token provided to the extension should not be shared and used by other extensions.
What are you proposing?
I have a few ideas for identifying the extension that a request originates from, but would like some input on the approaches. See an example of a REST Request originating from an extension in the AD extension here: https://github.com/opensearch-project/anomaly-detection/blob/feature/extensions/src/main/java/org/opensearch/ad/rest/RestCreateDetectorAction.java#L94-L99
IndexRequest<AnomalyDetector> indexRequest = new IndexRequest.Builder<AnomalyDetector>()
.index(ANOMALY_DETECTORS_INDEX)
.document(detector)
.build();
IndexResponse indexResponse = sdkClient.index(indexRequest);
Assumptions: The token being created in core and sent to the extension to interact with the cluster is a JWT. The JWT is signed by a secret that is unique to the extension. @dbwiddis laid out details nicely in a comment in the security repo: https://github.com/opensearch-project/security/issues/1895#issuecomment-1402609039
Brief background on JWTs
JWTs have 3 parts: the header (metadata on the algorithm used), payload (JSON body of the token) and signature. The issuer of a JWT signs the header and payload with a secret known to the issuer that can be used in return on receipt of a token to verify the authenticity of the token. The payload and header are readable by everyone, but cannot be tampered with.
There are 2 signing methods for JWTs (See https://www.pingidentity.com/en/resources/blog/post/jwt-security-nobody-talks-about.html for greater detail):
-
Symmetric Encryption - With symmetric encryption there is a single secret signing key known to the issuer that can be used to sign and verify tokens. If another service needs to verify tokens the signing key would need to be shared
-
Asymmetric Encryption - In asymmetric encryption a public/private key pair is generated and the private key is used by the JWT issuer to sign tokens. Those tokens can be verified with the public key and any other service that needs to verify tokens can know the public key so that they can verify, but not issue tokens.
Approach 1
Pass an HTTP Header with each request originating from an extension with an extension identifier. It could be the more public extensionId used in the extension's routes or another id that is less publicly known than the extensionId.
Pros:
- Easy to implement
Cons
- Not the most secure as an extension could share its id with another extension making impersonation possible
Approach 2
Is it possible to prevent extensions communicating with one another? If extensions are unable to communicate with one another then the JWT can implicitly be trusted and the extension information embedded in the token as a claim.
Approach 3
Use asymmetric encryption and add a verification layer in the SDK. Generate a public/private key pair and have the public key known to the extension. Before any usage of the token passed from core, verify that the token is meant for this extension. Similar to approach 2 where the extension information can be embedded in the token if we can trust that the token comes from the source that it was intended for.
Other approaches?
Are there any other approaches not outlined here? A dedicated channel for communication that can identify the source of a request? Using other metadata from the request such as resolving an IP address and performing X-Forwarded-For resolution?
What problems are you trying to solve?
Ensuring that extensions interact with OpenSearch securely and preventing impersonation by sharing auth tokens amongst extensions.
Approach 1
Pass an HTTP Header with each request originating from an extension with an extension identifier. It could be the more public extensionId used in the extension's routes or another id that is less publicly known than the extensionId.
Pros:
- Easy to implement
Cons
- Not the most secure as an extension could share its id with another extension making impersonation possible
I don't particularly see this as a con. In fact, I see potential use cases for integrated extensions.
At a certain computing website you're no doubt familiar with, I participate in an group that helps target spam. Briefly:
- The site API includes querying content (read only, quota limited).
- The site API enables user interaction (in this case, flagging posts for moderator attention, per-user quota limited)
- There are multiple bots (read, extensions) targeting various aspects of the site (spam, rude/aggressive comments, plagiarism, etc.). These bots:
- generally run "as a user" (read, extension)
- are sometimes given permission by other users (like me) to take actions as that user (since per-user quotas are limited and spam is not the anti-spam bot allows users to opt-in to sharing an id key with the bot to enable it to send flags as if it were the user).
Consider the above example where each user is an "extension". Normally every extension operates independently. Sometimes one extension (me, wanting to fight spam) gives some limited API credential permission (but not login credentials) to the bot (with super-high read query quota) to act as me interacting with the API to flag spam. This is a good thing.
For an existing plugin application, the alerting plugin runs an anomaly detector as if it were the original user who created it, and we want to keep this sort of behavior somehow.
Approach 2
Is it possible to prevent extensions communicating with one another? If extensions are unable to communicate with one another then the JWT can implicitly be trusted and the extension information embedded in the token as a claim.
I don't think it's possible to prevent. I am of the philosophy that we should not build our design to require it, but we should design in a way that permits extensions to communicate with each other if they so choose.
TLDR here: we should assume any information we send to an extension can (and will) be shared.
Approach 3
Use asymmetric encryption and add a verification layer in the SDK. Generate a public/private key pair and have the public key known to the extension. Before any usage of the token passed from core, verify that the token is meant for this extension.
No objections to this, but I really don't see the need for it.
Other approaches?
Are there any other approaches not outlined here? A dedicated channel for communication that can identify the source of a request? Using other metadata from the request such as resolving an IP address and performing X-Forwarded-For resolution?
Any headers/metadata can/will be hacked. There is a transport later communication that could in theory be used internally that could somewhat try to validate this. The sequence would go something like this:
- It is assumed that the initiation of all requests starts with the OpenSearch RestController.
- Any request not known to be part of an existing workflow process should be assumed to be a new request and should carry its own authentication as if it were a user.
- When a REST request requires an extension to process it, it is sent over transport to the extension along with sanitized identity information assumed to be world-readable. It can/should also send a short-time-limited access token intended for that extension's REST requests.
- The extension may make multiple REST requests and gets responses using this information. OpenSearch would be aware that this request is currently "open" and would expect requests with those tokens.
- Eventually the extension returns its result via the transport layer and the "request" can be closed. After this point any future requests using this token would be considered invalid.
So our focus is on step 4, looking at REST requests that come in during the short time window that a request is awaiting a response over transport. We could:
- With every rest request include some random ID in a header that we also send separately over transport.
- Before the Rest Controller acts on a request with such ID it would wait for the transport call (which could have arrived before it) with that ID
- This would definitely impact performance as we'd have to wait for both requests to arrive, but it would gain some security as only the extension has that transport layer connection.
I think that on installation extension gets a blob encrypted with a server key that contains its public key, then signs all its requests with its public key that the server can verify subsequently upon every request.
- Server generates a private/public key pair on installation [s-pub, s-key].
- An extension generates a private/public key pair on installation [x-pub, x-key].
- During install, extension presents x-pub to the server, and receives an opaque token: e(s-key: [x-pub, permissions, expiry]).
- Extension presents [token, sig(x-key: token)] with every request, server decrypts token to get x-pub, checks that sig() is valid by using x-pub, checks permissions, expiry information. An alternative would be to store x-pub on the server, so that an extension can be uninstalled and its key revoked on the server side.
I don't know if replays with [token, sig(x-key: token)] are a concern. IMO the transport is secure, so it's not an issue, but I could be wrong.