Ecamo (embedded camo) is a HTTP reverse proxy heavily inspired by atmos/camo.
The original Camo aims to enable loading images behind cleartext HTTP server to avoid mixed content issue. In addition to that, Ecamo also aims to avoid using of third party cookies when loading images from other origins where require appropriate session cookies like company's internal screenshot server.
We've been using Camo in our internal wiki to serve some external images for long time, but due to recent movement around third-party cookies, we need a similar system to allow embedding internal screenshot services without using third-party cookie.
Basically, as like as Camo does, Ecamo receives URL data and retrives a resource on URL on behalf of user.
Unlike Camo, an origin must set up routing to Ecamo under a path with a predefined prefix of Ecamo. For instance, when a Ecamo prefix is set to /.ecamo/
, then any requests prefixed with /.ecamo/
on the origin should be routed to Ecamo server. Single Ecamo server works for multiple applications (= origins).
Ecamo is designed to be used as follows.
<!-- On origin https://wiki.corp.example.com -->
<img src="/.ecamo/v1/r/{URL_TOKEN}...">
In this example, an origin is wiki.corp.example.com
and is expected to set an authorisation cookie for Ecamo. And URL_TOKEN
specifies a URL of actual content. authorisation cookies and URL tokens are formatted in JWT and must be signed by origin owned P-256 private key.
Then Ecamo will redirect this URL to Ecamo's canonical origin with a short-lived token based on an authorisation cookie. A user will receive an actual content of URL specified from origin.
Configuration is done through environment variables.
ECAMO_BIND
(default:[::]:3000
): bind addressECAMO_CANONICAL_HOST
(required): HTTP Host header value of an canonical origin. Used to serve actual content. You may need to specify port number for non-standard ports.ECAMO_SERVICE_PUBLIC_KEYS
(required): JSON object where key is"${SERVICE_ORIGIN} ${kid}"
and value is JWK object, used by services for signing an authorisation cookie and an ecamo URL. Supports ES256 keys. e.g.{"https://service.test.invalid key_1": {"kid": "key_1", ...}}
ECAMO_PRIVATE_KEYS
(required): JSON object where key is tokenkid
and value is JWK object, used by Ecamo for signing an short-lived authorization token in URL and request header. Supports ES256 keys.ECAMO_SIGNING_KID
: Primary private keykid
to use; key element must be given through$ECAMO_PRIVATE_KEYS
.ECAMO_SERVICE_HOST_REGEXP
: Regexp to validate a service origin Host header. when unspecified, any origins work as a service origin.ECAMO_SOURCE_ALLOWED_REGEXP
: Regexp to validate a source URL. When specified, any unmatching source URL will be denied.ECAMO_SOURCE_BLOCKED_REGEXP
: Regexp to reject a source URL. When specified, any matching source URL will be denied.ECAMO_PRIVATE_SOURCE_ALLOWED_REGEXP
: Regexp to validate a source URL in case of a destination IP address resolved into a private IP address; URL might only include scheme, host and port. Any unmatching source URL connecting to a private IP address will be denied. When unspecified, any connection attempts to private IP address will be denied. Note that a URL has to be allowed also withECAMO_SOURCE_ALLOWED_REGEXP
.ECAMO_PREFIX
(default:.ecamo
): An ecamo prefix explained earlier. This is an URL prefix especially when embedded in a service origin, more exactly used when redirecting requests to a service origin from a canonical origin.ECAMO_MAX_REDIRECTS
(default:0
): Maximum number of HTTP redirections allowed during a single HTTP request to fetch a source URL. When allowed, any URLs will be allowed when following redirection (ECAMO_*_REGEXP
doesn't work butECAMO_PRIVATE_SOURCE_ALLOWED_REGEXP
works)ECAMO_MAX_LENGTH
: Maximum number ofContent-Length
allowed to be proxied from a source URL. If achunked
response exceeds the limit, such proxied response will be terminated (a client will see unexpected EOF)ECAMO_CONTENT_TYPE_ALLOWED
(default: common image/* types):Content-Type
allowed to be proxied. Specify in comma separeted values.ECAMO_TIMEOUT
: Timeout in seconds to fetch a source URL.ECAMO_AUTH_COOKIE
(default:__Host-ecamo_token
, when insecure mode=ecamo_token
): Cookie name to store an authorisation token.ECAMO_DEFUALT_CACHE_CONTROL
(default=public, max-age=3600
): cache-control header value to response when it is missing in a source URL responseECAMO_INSECURE
: When given, some features work on plain HTTP for development.
You can use sec-x-ecamo-service-host
request header to Ecamo server to explicitly specify a service origin Host header. This should work well especially if you put your Ecamo server behind reverse proxies and you need to set specific Host
(:authority
) header.
(In the other hands this is similar to X-Forwarded-Host
request header)
To use Ecamo, a service origin has to generate a URL token and an authorisation token.
Use the following format to make a request to Ecamo:
https://${SERVICE_HOST}/${PREFIX}/v1/r/${URL_TOKEN}
where:
SERVICE_HOST
: any string is permitted; but subject for validation of$ECAMO_SERVICE_HOST_REGEXP
PREFIX
: recommended to be identical to$ECAMO_PREFIX
but any string is permitted.URL_TOKEN
: JWT (JSON Web Token) with the following constraints- header:
alg
: Must beES256
kid
: Must be set to a one defined in$ECAMO_SIGNING_PUBLIC_KEYS
- claims:
iss
: Must be identical to a Web origin of service origin. (e.g.https://service.test.invalid
)ecamo:url
: source URLecamo:send-token
(optional): Set totrue
to send anonymous ID token to the source URL.
- header:
- Note that a URL token is indeterministic per signing action. If you're going to enable edge caching, make sure your application generates ecamo URL as infrequently as possible.
exp
andnbf
claims are not validated.
An authorisation cookie is a JWT signed by a key specified in $ECAMO_SIGNING_PUBLIC_KEYS
, with the following constraints. It should be stored to a cookie named a value of $ECAMO_AUTH_COOKIE
(default to __Host-ecamo_token
)
- Headers:
alg
: Must beES256
kid
: Must be set to a one defined in$ECAMO_SIGNING_PUBLIC_KEYS
- Claims:
exp
must be provided.iss
must be identical to an web origin of service origin. (e.g.https://service.test.invalid
)aud
must be set to an web origin of canonical origin. (e.g.https://$ECAMO_CANONICAL_HOST
)
- Recommendations:
- Align cookie expiration and token lifetime.
- Keep lifetime as short as possible. Using JWT in cookies is a terrible idea in general, but we assume an application is doing their authentication at their own and the
ecamo-token
is derived from its session store. Ifecamo-token
can be derived from other session tokens, then its lifetime can be short.
To allow user to recognise a canonical URL of a requested content, when a end user directly opened /.ecamo/...
URL, Ecamo redirects them directly to a source image (more exactly, when a request without an authorisation cookie or a request with Sec-Fetch-Dest=document).
If ecamo:send-token
of a URL token is set to true, Ecamo will set a ID token as a Bearer token (Authorization: Bearer ...
).
The token doesn't contain user information, for example:
{
"iss": "https://ecamo.test.invalid",
"sub": "anonymous",
"aud": "https://source.test.invalid",
"exp": ...,
"iat": ...,
"ecamo:svc": "https://service.test.invalid"
}
- Fastly Compute@Edge integration: see ./contrib/fastlyce
Due to reqwest's current API limitation, Ecamo launches a SOCKS5 proxy to restrict connection to private IP addresses when $ECAMO_PRIVATE_SOURCE_ALLOWED_REGEXP
is configured. The internal proxy is used only for requests not matching $ECAMO_PRIVATE_SOURCE_ALLOWED_REGEXP
. For such requests any attempts to the following addresses will be denied:
MIT License
Copyright 2021 Cookpad Inc.