ECS / Fargate Deployment¶
Run Rift beside your application in one ECS task, so the application's hard-coded HTTPS calls are answered by the intercept listener.
A reference, not a tested path. The task definition below is a starting point assembled from the container and intercept behaviour documented elsewhere. Nothing in this repository runs it on Fargate. The executable, CI-verified example of the same setup is the HTTPS intercept demo (Docker Compose). Have the definition reviewed against the ECS schema before relying on it.
Shape of the task¶
With awsvpc networking (mandatory on Fargate) every container in a task shares one network
namespace, so the app reaches Rift on localhost -- the same arrangement as the
Kubernetes sidecar.
riftruns the intercept listener, with its rules declared in a config file.appsetsHTTPS_PROXY=http://127.0.0.1:8080and trusts the intercept CA.- The CA pair lives in Secrets Manager. ECS injects a secret only as an environment variable, and
the config-file block reads it from there by name:
"caCertPemEnv": "INTERCEPT_CA_CERT","caKeyPemEnv": "INTERCEPT_CA_KEY". The variables are deliberately not namedRIFT_INTERCEPT_*: those are the flags' own variables, and setting one alongside a configinterceptblock is a startup error. - The app trusts the same certificate, so it must reach the app before the app opens its first
connection -- baked into the app image (below) or copied in by an init container. A JVM in
particular reads its truststore once, at startup. A JVM also ignores
HTTPS_PROXY: map it ontohttps.proxyHost/https.proxyPort(andNO_PROXYontohttp.nonProxyHosts) in the app.
Task definition¶
{
"family": "app-with-rift",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"name": "rift",
"image": "123456789012.dkr.ecr.eu-west-1.amazonaws.com/app-rift-mocks:latest",
"essential": true,
"command": ["--configfile", "/config/mock.json"],
"stopTimeout": 10,
"secrets": [
{ "name": "INTERCEPT_CA_CERT",
"valueFrom": "arn:aws:secretsmanager:eu-west-1:123456789012:secret:rift-intercept-ca-cert" },
{ "name": "INTERCEPT_CA_KEY",
"valueFrom": "arn:aws:secretsmanager:eu-west-1:123456789012:secret:rift-intercept-ca-key" }
],
"healthCheck": {
"command": ["CMD", "/usr/local/bin/rift", "healthcheck"],
"interval": 5,
"timeout": 3,
"retries": 10,
"startPeriod": 5
}
},
{
"name": "app",
"image": "123456789012.dkr.ecr.eu-west-1.amazonaws.com/app:latest",
"essential": true,
"dependsOn": [{ "containerName": "rift", "condition": "HEALTHY" }],
"environment": [
{ "name": "HTTPS_PROXY", "value": "http://127.0.0.1:8080" },
{ "name": "NO_PROXY", "value": "localhost,127.0.0.1" }
]
}
]
}
dependsOn: HEALTHY on the built-in rift healthcheck means the app starts only once the listener
is up with its rules installed. The intercept block in mock.json is the one shown under
Docker; give it
"host": "127.0.0.1" here, since only the task's own app connects, "port": 8080,
"caCertPemEnv": "INTERCEPT_CA_CERT" and "caKeyPemEnv": "INTERCEPT_CA_KEY". A multi-line PEM
survives intact: Secrets Manager stores it verbatim and Rift reads the variable as-is. No shell is
involved, so the -static image works too.
On a Rift release before caCertPemEnv existed, the block refuses the two keys. There, write the
secrets to files with a shell entrypoint ("entryPoint": ["sh", "-c"], "command": ["umask 077 &&
printf '%s' \"$INTERCEPT_CA_CERT\" > /tmp/ca-cert.pem && printf '%s' \"$INTERCEPT_CA_KEY\" >
/tmp/ca-key.pem && exec rift --configfile /config/mock.json"]) and point caCertPath/caKeyPath
at them; that needs the default (Debian-based) image.
Alternative: the flag path. Start the listener with --intercept-port 8080 and the secrets named
RIFT_INTERCEPT_CA_CERT_PEM / RIFT_INTERCEPT_CA_KEY_PEM, with no intercept block, and install
the rules with a one-shot PUT /intercept/rules against localhost:2525. That needs no shell, but
costs a bootstrap call and a window where the app's first calls can race the missing rules; with
caCertPemEnv the block path needs no shell either, so prefer it.
ECS reads the secrets with the execution role, not the task role, so the execution role needs
secretsmanager:GetSecretValue on both secrets.
Getting the config file in¶
--configfile takes a local file, so it has to be on the container's filesystem before Rift starts.
Two ways:
-
Bake it into a custom image (used above). Rules change by rebuilding and redeploying:
-
Fetch it from S3 with an init container. Add a non-essential-after-exit container that copies the file to a shared volume, and make
riftdepend on it with"condition": "SUCCESS":
with volumes: [{ "name": "config" }] on the task and the same mountPoints entry on rift,
plus "dependsOn": [{ "containerName": "fetch-config", "condition": "SUCCESS" }]. The task role
needs s3:GetObject. A running task can then pick up an edited file with POST /admin/reload on
its admin port (localhost:2525), which re-applies the file's intercept.rules. The listener
itself (host, port, auth, CA) is fixed at boot; an edit that also changes one of those applies
nothing from the block until a restart.
Putting the CA in the app image¶
Generate the CA once, outside the task, and store the two halves in Secrets Manager. In the CI job
that builds the app image, add the certificate to the image's truststore. Rift's offline
intercept-ca does both steps without starting
a server:
rift intercept-ca generate --out-dir ./ca
aws secretsmanager create-secret --name rift-intercept-ca-cert --secret-string file://ca/ca-cert.pem
aws secretsmanager create-secret --name rift-intercept-ca-key --secret-string file://ca/ca-key.pem
# JVM app: a truststore holding the CA plus the system roots
RIFT_TRUSTSTORE_PASSWORD=changeit \
rift intercept-ca export --cert ./ca/ca-cert.pem --format jks --out ./truststore.jks \
--merge-system-cas /etc/ssl/certs/ca-certificates.crt
Then COPY ca/ca-cert.pem /certs/ca.pem (or truststore.jks) into the app image and point the
runtime at it. Which variable adds to the default roots and which replaces them, and the JVM flags,
are under
Trusting the CA from the SUT.
Keep ca-key.pem out of the app image -- only Rift needs it.
Alternative: an init container. Rift serves the CA it is actually using at
GET /intercept/ca.pem on the admin port. A non-essential container that depends on rift with
"condition": "HEALTHY" can curl that file into a task volume shared with the app, and app
then depends on it with "condition": "SUCCESS" and mounts the volume read-only. The app still has
to turn the PEM into trust for its own runtime (a JVM needs a truststore). This layout needs no
shell in either image, so it works with a distroless Rift image and a distroless app.
Stopping the task¶
ECS sends SIGTERM and, after stopTimeout, SIGKILL. Rift handles SIGTERM itself, as PID 1,
and exits 0 within about three seconds, so a short stopTimeout is enough and no init process is
needed -- see Stopping the container.
Notes¶
forwardrules. Aforwardaction defaults to127.0.0.1:{port}, the Rift container's own loopback. An imposter in the same task is reachable there; one in another task or service needs"host"(and"scheme"): see Forward to one of your imposters.- One listener per task. Each task has its own listener and rules, as with Kubernetes replicas.
- Keep the listener private. Do not map port 8080 in a load balancer or open it in the security
group; an unauthenticated intercept proxy is open to anything that can reach it. To expose it
anyway, set the block's
auth(RIFT_INTERCEPT_AUTHis a flag-path variable and conflicts with a block).