Serverless Single-Serving Secrets
Published on Sunday, January 12, 2020A few weeks ago, a colleague showed me a small Sinatra application they had written to share small bits of sensitive text without any preconditions on the recipient. The application, which is very nicely branded as Onceler, will accept a small text snippet, store it in memory, and give the uploader a unique URL from which the secret can be downloaded. The recipient then visits the URL, which causes the secret to be downloaded, displayed, and deleted from the server’s memory.
The secret can only be retrieved once, so the service offers some protection for communication channels with limited or non-existent forward secrecy, and the secret URL contains a 120-bit random identifier, which effectively prevents attackers from scanning the whole keyspace looking for uncollected goodies. You’re still effectively publishing sensitive data at a publicly accessible URL, but texting someone a single-use link to retrieve a temporary password is a step up from just texting them the password, especially if you know the recipient will visit the link relatively quickly. The sender and receiver don’t need to have accounts on the same end-to-end encrypted communications service, they don’t need to send each other the public halves of their PGP keys, and they don’t need to have previously agreed on a shared secret (any of which would offer greater security at the expense of convenience).
While I loved the idea, especially for scenarios that would normally have you send something mildly sensitive over SMS or email, I didn’t love the idea of running a persistent server. The implementation uses a plain ruby map to store data, so it relies on locks to prevent concurrent access to a given secret, requires the sender and recipient to interact with the same server process, and would provide interesting heap dumps. I thought it might be viable to implement the same idea using API Gateway and DynamoDB, as the latter supports an atomic get-and-remove operation through the DeleteItem
operation’s ReturnValues
parameter.
My first stab at this was to use API Gateway’s AWS
integration type, which will translate incoming requests to calls to an AWS service using a velocity template. At first glance, this is an attractive option, since you don’t have to write any code or pay for any Lambda invocations or execution time. But in practice, I didn’t like the idea of embedding all routing and processing logic in the OpenAPI definition and nested Velocity templates. The full CloudFormation template ended up doing way too much in YAML configuration for my taste:
|
|
I also didn’t want to limit the storage to a particular content type (or worse, hand-code templates for every new content type I wished to add).
Luckily, API Gateway just released HTTP APIs as a public beta. These are a pared-down version of their stalwart REST API offering with a reduced featureset offered at a significantly reduced cost. AWS
integrations are not available on HTTP APIs, which can only defer to a Lambda or HTTP backend, but even with Lambda costs factored in, the HTTP API is still likely to be a better deal. Assuming Lambda invocations will terminate within Lambda’s minimum billable duration of 100ms, it would cost $1.408 to handle one million requests with an HTTP API (with $1.00 going to API Gateway and $0.408 going to Lambda). With a REST API with AWS
integrations, you only pay for API Gateway’s per-request charge, but this is still over twice as expensive at $3.50.
HTTP APIs actually end up being cheaper than using the slightly clunkier CloudFront + Lambda@Edge setup described in Serverless client-side telemetry aggregation given Lambda@Edge’s higher costs compared to Lambda. The same million requests that the HTTP API could handle for $1.408 will cost $1.9125 when handled by Lambda@Edge.
HTTP APIs are still in beta, but the only buggy behavior I encountered was in the service’s OpenAPI import, which does not yet appear to support many of API Gateway’s OpenAPI extensions. There is also not yet any support for API Gateway’s HTTP or WebSocket APIs in Terraform, so I ended up explicitly defining routes and integrations as CloudFormation resources, then having Terraform deploy a small CloudFormation stack. My labor to get everything working with an HTTP API was definitely worth more than the $2.00/month I’ll save if a million people use the API, but it was a learning experience.
Using the API
The API for storing and retrieving secrets has no authentication, so you can use it from any HTTP client. For example, the following bash command will save a message and return the secret’s ID using cURL
and jq
:
curl -s --data 'my secret' https://onceler.toomanywords.io | jq .id
Any content-type header specified for the request body will be returned when the secret is retrieved.
Retrieving the secret just requires the id:
curl https://onceler.toomanywords.io/{id}
The API also allows CORS access, so it can be called directly from JavaScript. You can view examples of how to use the service with text, small files, and client-side encryption here.