Skip to content
Closed
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
70 changes: 20 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

Envoy WASM plugin that intercepts HTTP requests, reads a configurable header
(e.g. `x-example` set by someone after a successful authentication),
encrypts its value with **AES-256-CBC** (or sets a fixed value) and stores the
result as a `Set-Cookie` header in the response so downstream services
(e.g. Akamai) can read it.
encrypts its value with **AES-256-GCM** (AEAD) and stores the result as a `Set-Cookie` header
in the response so downstream services (e.g. Akamai) can read it.

Built on [`proxy-wasm-go-sdk`](https://github.com/proxy-wasm/proxy-wasm-go-sdk).
Module: `github.com/freepik-company/ceep`.
Expand All @@ -18,72 +17,43 @@ Module: `github.com/freepik-company/ceep`.
3. If the header is absent or empty, the plugin does nothing and continues.
4. It checks the request `:authority` header against the configured `cookie_domains` list.
If the authority is not in the list, the plugin skips and continues.
5. Depending on the configured mode it either:
- Encrypts the header value with AES-256-CBC (random or fixed IV), or
- Uses a static cookie value loaded from the `CEEP_STATIC_COOKIE_VALUE` environment variable.
5. It encrypts the header value with AES-256-GCM (random 12-byte nonce per request).
6. It injects a `Set-Cookie` header using the request authority as the cookie domain.

---

## Cookie value modes

The plugin supports three modes for generating the cookie value:
### Encrypted header (default)

### Random IV (default)

A random 16-byte IV is generated per request and prepended to the ciphertext.
Only `CEEP_ENCRYPTION_KEY` is required.

```
Set-Cookie: <cookie_name>=<base64(IV[16] || ciphertext)>; Path=/; Domain=<domain>; SameSite=Lax;
```

### Fixed IV

A shared 16-byte IV is loaded from `CEEP_ENCRYPTION_IV` at startup.
The IV is **not** included in the output. Set `iv_mode: "fixed"` in `pluginConfig`.

```
Set-Cookie: <cookie_name>=<base64(ciphertext)>; Path=/; Domain=<domain>; SameSite=Lax;
```

### Static cookie value

No encryption is performed. The cookie value is taken directly from the
`CEEP_STATIC_COOKIE_VALUE` environment variable. Set `set_static_cookie_value: true`
in `pluginConfig`.
Requires `CEEP_ENCRYPTION_KEY` (32 raw bytes). Each request uses a **new random 12-byte nonce** prepended to the GCM ciphertext and tag.

```
Set-Cookie: <cookie_name>=<CEEP_STATIC_COOKIE_VALUE>; Path=/; Domain=<domain>; SameSite=Lax;
Set-Cookie: <cookie_name>=<base64(nonce[12] || AES-256-GCM(header_value))>; Path=/; Domain=<domain>; SameSite=Lax; HttpOnly; Secure
```

### Encryption details (Random IV / Fixed IV modes)
| Field | Details |
| --------- | ----------------------------------------------------------------------- |
| Algorithm | AES-256-GCM (AEAD) |
| Key size | 32 bytes (raw, not base64) |
| Nonce | Random 12 bytes, prepended to the sealed payload |
| Tag | 16 bytes, appended by GCM and verified on decrypt |
| Encoding | Standard base64 (RFC 4648) |

| Field | Details |
| --------- | ----------------------------------------------------------------- |
| Algorithm | AES-256-CBC |
| Key size | 32 bytes (raw, not base64) |
| IV | Random 16 bytes prepended to ciphertext, or fixed 16 bytes shared |
| Padding | PKCS#7 |
| Encoding | Standard base64 (RFC 4648) |
On Akamai (or any consumer), read the nonce from the first 12 bytes after base64 decode; do **not** use a fixed nonce/IV for all cookies — that would break semantic security.

### Decryption pseudocode (e.g. Akamai EdgeWorkers)

Random IV:

```javascript
const raw = atob(cookieValue);
const iv = raw.slice(0, 16);
const payload = raw.slice(16);
const value = aes256cbcDecrypt(key, iv, payload);
const raw = atob(cookieValue); // base64-decode
const nonce = raw.slice(0, 12);
const sealed = raw.slice(12); // ciphertext || 16-byte tag
const value = aes256gcmOpen(key, nonce, sealed); // authentication error if tampered
```

Fixed IV:
### Static cookie value

```javascript
const payload = atob(cookieValue);
const value = aes256cbcDecrypt(key, sharedIV, payload);
```
With `set_static_cookie_value: true` and `CEEP_STATIC_COOKIE_VALUE`, the cookie value is set from env with no encryption (see samples in `docs/samples/`).

---

Expand Down
62 changes: 8 additions & 54 deletions docs/samples/istio-wasmplugin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# The plugin intercepts HTTP requests handled by oauth2-proxy, reads the header
# specified in header_to_encrypt_name (set by oauth2-proxy after successful auth),
# encrypts it with AES-256-CBC and injects the result as a Set-Cookie header in
# encrypts it with AES-256-GCM and injects the result as a Set-Cookie header in
# the response so Akamai can read it on subsequent requests.
#
# Prerequisites:
Expand All @@ -17,26 +17,15 @@
# - "something.example.com" matches exactly → Cookie Domain: something.example.com
#
# Akamai decryption:
# - Algorithm : AES-256-CBC
# - Algorithm : AES-256-GCM (random 12-byte nonce per cookie, prepended to the value)
Copy link
Copy Markdown
Member

@achetronic achetronic Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, hey, not possible in akamai to decrypt using all the algorithms you want. Just accepting two of them for AES256

Image

CC: @devploit

# - Key : same raw 32-byte key
# - IV : random (prepended to cookie) or fixed (shared, set via CEEP_ENCRYPTION_IV)
# - Ciphertext : base64-decoded cookie value (without IV prefix when using fixed IV)
# - Padding : PKCS#7
#
# IV modes (pluginConfig.iv_mode):
# - "random" (default): a random 16-byte IV is generated per request and
# prepended to the ciphertext: base64(IV || ciphertext).
# - "fixed": CEEP_ENCRYPTION_IV env var must be set (exactly 16 bytes). The IV is
# NOT included in the output: base64(ciphertext). Both sides share the IV.
#
# Static cookie mode (pluginConfig.set_static_cookie_value):
# - When true, the cookie value is taken directly from the CEEP_STATIC_COOKIE_VALUE
# environment variable without any encryption. No CEEP_ENCRYPTION_KEY or CEEP_ENCRYPTION_IV
# is required.
# - Nonce : first 12 bytes of the base64-decoded cookie value
# - Sealed : remaining bytes (ciphertext || 16-byte authentication tag)

---
# Option A: Random IV (default). Only CEEP_ENCRYPTION_KEY is required.
# iv_mode is "random" by default, so it can be omitted.
# Option A (production): key injected from a K8s Secret via secretKeyRef.
# Use External Secrets Operator (or Sealed Secrets) to sync the key from your
# secrets store into the Secret below.
apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
Expand Down Expand Up @@ -68,42 +57,7 @@ spec:
header_to_encrypt_name: "x-auth-request-email"

---
# Option B: Fixed IV. Set iv_mode to "fixed" and provide ENCRYPTION_IV.
# The IV is NOT included in the cookie output — both sides must share it.
apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
name: ceep
namespace: gateway
spec:
selector:
matchLabels:
app.kubernetes.io/instance: gateway

url: oci://ghcr.io/freepik-company/ceep:latest
imagePullPolicy: IfNotPresent

type: HTTP
phase: STATS

vmConfig:
env:
- name: CEEP_ENCRYPTION_KEY
valueFrom: HOST
- name: CEEP_ENCRYPTION_IV
valueFrom: HOST

pluginConfig:
iv_mode: "fixed"
cookie_name: "_extauth_signed_email"
cookie_domains:
- domain: "*.example.com"
wildcard_matches_base: true
- domain: "*.example.com"
- domain: "example.com"
header_to_encrypt_name: "x-auth-request-email"
---
# Option C: Fixed cookie value. Set set_static_cookie_value to "true" and provide CEEP_STATIC_COOKIE_VALUE.
# Option B: Fixed cookie value. Set set_static_cookie_value to "true" and provide CEEP_STATIC_COOKIE_VALUE.
apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
Expand Down
74 changes: 8 additions & 66 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
// This WASM plugin for Envoy is designed to intercept oauth2-proxy responses,
// read X-Auth-Request-Email header and inject it encrypted (AES-256-CBC) as the
// read X-Auth-Request-Email header and inject it encrypted (AES-256-GCM) as the
// _extauth_signed_email cookie so Akamai can use it to bypass rate-limit rules
// for authenticated users.
//
// The AES-256 encryption key is loaded from the ENCRYPTION_KEY environment variable,
// The AES-256 encryption key is loaded from the CEEP_ENCRYPTION_KEY environment variable,
// injected at runtime by Istio from a Kubernetes Secret (WasmPlugin.vmConfig.env),
// so the key never appears in any YAML file committed to the repository.
//
// Cookie value modes:
// - Random IV (default): base64(IV[16 bytes] || AES-256-CBC(email))
// - Fixed IV (ENCRYPTION_IV set): base64(AES-256-CBC(email))
// - Static value (set_static_cookie_value: true): CEEP_STATIC_COOKIE_VALUE env var used as-is
// Encrypted cookie format: base64(nonce[12] || AES-256-GCM ciphertext || tag[16])
// Akamai decrypts using the nonce from the first 12 bytes; GCM Open verifies the tag.
//
// Ref: https://github.com/tetratelabs/proxy-wasm-go-sdk/blob/main/examples/http_headers/

Expand All @@ -33,11 +31,6 @@ const (
// Injected by WasmPlugin.vmConfig.env from a Kubernetes Secret.
EnvEncryptionKey = "CEEP_ENCRYPTION_KEY"

// EnvEncryptionIV is the optional environment variable holding a fixed 16-byte IV.
// When set, the IV is NOT prepended to the ciphertext (both sides already know it).
// When unset, a random IV is generated per request and prepended to the output.
EnvEncryptionIV = "CEEP_ENCRYPTION_IV"

// EnvStaticCookieValue is the environment variable holding a fixed cookie value.
// When set_static_cookie_value is true, this value is used directly as the cookie
// value without any encryption.
Expand Down Expand Up @@ -73,15 +66,9 @@ type pluginContext struct {
// Following fields are configured during OnPluginStart.

// encryptionKey is the AES-256 key (exactly 32 bytes).
// Loaded from the ENCRYPTION_KEY environment variable.
// Loaded from CEEP_ENCRYPTION_KEY when not using static cookie mode.
encryptionKey []byte

// encryptionIV is the fixed 16-byte IV (only used when ivMode is "fixed").
encryptionIV []byte

// ivMode is "random" or "fixed".
ivMode string

// setStaticCookieValue indicates whether to use a fixed cookie value from env
// instead of encrypting the header value dynamically.
setStaticCookieValue bool
Expand Down Expand Up @@ -167,27 +154,7 @@ func (p *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPlugi

p.encryptionKey = []byte(encryptionKey)

p.ivMode = "random"
if v := gjson.Get(string(data), "iv_mode").Str; v != "" {
p.ivMode = v
}

if p.ivMode == "fixed" {
encryptionIV := os.Getenv(EnvEncryptionIV)
if encryptionIV == "" {
proxywasm.LogCriticalf("%s environment variable is required when iv_mode is \"fixed\"", EnvEncryptionIV)
return types.OnPluginStartStatusFailed
}

if len(encryptionIV) != 16 {
proxywasm.LogCriticalf("%s must be exactly 16 bytes, got %d", EnvEncryptionIV, len(encryptionIV))
return types.OnPluginStartStatusFailed
}

p.encryptionIV = []byte(encryptionIV)
}

proxywasm.LogDebugf("plugin configured: cookie_name=%s cookie_domains=%v header_to_encrypt_name=%s iv_mode=%s", p.cookieName, p.cookieDomains, p.headerToEncryptName, p.ivMode)
proxywasm.LogDebugf("plugin configured: cookie_name=%s cookie_domains=%v header_to_encrypt_name=%s", p.cookieName, p.cookieDomains, p.headerToEncryptName)
return types.OnPluginStartStatusOK
}

Expand All @@ -201,12 +168,6 @@ type httpContext struct {
// encryptionKey is the AES-256 key used to encrypt the email value.
encryptionKey []byte

// encryptionIV is the fixed 16-byte IV (only used when ivMode is "fixed").
encryptionIV []byte

// ivMode is "random" or "fixed".
ivMode string

// setStaticCookieValue indicates whether to use a fixed cookie value from env.
setStaticCookieValue bool

Expand Down Expand Up @@ -234,8 +195,6 @@ func (p *pluginContext) NewHttpContext(contextID uint32) types.HttpContext {
return &httpContext{
contextID: contextID,
encryptionKey: p.encryptionKey,
encryptionIV: p.encryptionIV,
ivMode: p.ivMode,
setStaticCookieValue: p.setStaticCookieValue,
staticCookieValue: p.staticCookieValue,
cookieName: p.cookieName,
Expand Down Expand Up @@ -281,24 +240,7 @@ func (h *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) typ
return types.ActionContinue
}

var iv []byte
var prependIV bool

switch h.ivMode {
case "fixed":
iv = h.encryptionIV
prependIV = false
default:
var err error
iv, err = generateRandomIV()
if err != nil {
proxywasm.LogCriticalf("failed to generate IV: %v", err)
return types.ActionContinue
}
prependIV = true
}

encryptedHeaderValue, err := encryptAES256CBC(h.encryptionKey, iv, headerValue, prependIV)
encryptedHeaderValue, err := encryptAES256GCM(h.encryptionKey, headerValue)
if err != nil {
proxywasm.LogCriticalf("failed to encrypt email: %v", err)
return types.ActionContinue
Expand All @@ -320,7 +262,7 @@ func (h *httpContext) OnHttpResponseHeaders(numHeaders int, endOfStream bool) ty
return types.ActionContinue
}

cookieValue := fmt.Sprintf("%s=%s; Path=/; Domain=%s; SameSite=Lax;", h.cookieName, h.encryptedHeaderValue, h.authority)
cookieValue := fmt.Sprintf("%s=%s; Path=/; Domain=%s; SameSite=Lax; HttpOnly; Secure", h.cookieName, h.encryptedHeaderValue, h.authority)

if err := proxywasm.AddHttpResponseHeader("set-cookie", cookieValue); err != nil {
proxywasm.LogCriticalf("failed to add Set-Cookie header: %v", err)
Expand Down
Loading