feat(helm): add Pipelock ConfigMap, scanning config, and consolidate compose (#1064)

* feat(helm): add Pipelock ConfigMap, scanning config, and consolidate compose

- Add ConfigMap template rendering DLP, response scanning, MCP input/tool
  scanning, and forward proxy settings from values
- Mount ConfigMap as /etc/pipelock/pipelock.yaml volume in deployment
- Add checksum/config annotation for automatic pod restart on config change
- Gate HTTPS_PROXY/HTTP_PROXY env injection on forwardProxy.enabled (skip
  in MCP-only mode)
- Use hasKey for all boolean values to prevent Helm default swallowing false
- Single source of truth for ports (forwardProxy.port/mcpProxy.port)
- Pipelock-specific imagePullSecrets with fallback to app secrets
- Merge standalone compose.example.pipelock.yml into compose.example.ai.yml
- Add pipelock.example.yaml for Docker Compose users
- Add exclude-paths to CI workflow for locale file false positives

* Add CHANGELOG entry for Pipelock security proxy integration

* Missed v0.6.8 release

---------

Co-authored-by: Juan José Mata <jjmata@jjmata.com>
This commit is contained in:
LPW
2026-03-02 17:26:01 -05:00
committed by GitHub
parent 4db5737c9c
commit 59bf72dc49
11 changed files with 437 additions and 296 deletions

View File

@@ -20,5 +20,7 @@ jobs:
uses: luckyPipewrench/pipelock@v1
with:
scan-diff: 'true'
fail-on-findings: 'false'
fail-on-findings: 'true'
test-vectors: 'false'
exclude-paths: |
config/locales/views/reports/

View File

@@ -5,22 +5,25 @@ All notable changes to the Sure Helm chart will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### [0.0.0], [0.6.5]
## [0.6.9-alpha] - 2026-03-01
### Added
- **Pipelock security proxy** (`pipelock.enabled=true`): Separate Deployment + Service that provides two scanning layers
- **Forward proxy** (port 8888): Scans outbound HTTPS from Faraday-based clients (e.g. ruby-openai). Auto-injects `HTTPS_PROXY`/`HTTP_PROXY`/`NO_PROXY` env vars into app pods
- **MCP reverse proxy** (port 8889): Scans inbound MCP traffic for DLP, prompt injection, and tool poisoning. Auto-computes upstream URL via `sure.pipelockUpstream` helper
- **WebSocket proxy** configuration support (disabled by default, requires Pipelock >= 0.2.9)
- ConfigMap with scanning config (DLP, prompt injection detection, MCP input/tool scanning, response scanning)
- ConfigMap checksum annotation for automatic pod restart on config changes
- Helm helpers: `sure.pipelockImage`, `sure.pipelockUpstream`
- Health and readiness probes on the Pipelock deployment
- `imagePullSecrets` with fallback to app-level secrets
- Boolean safety: uses `hasKey` to prevent Helm's `default` from swallowing explicit `false`
- Configurable ports via `forwardProxy.port` and `mcpProxy.port` (single source of truth across Service, Deployment, and env vars)
- `pipelock.example.yaml` reference config for Docker Compose deployments
- First (nightly/test) releases via <https://we-promise.github.io/sure/index.yaml>
### [0.6.6] - 2025-12-31
### Added
- First version/release that aligns versions with monorepo
- CNPG: render `Cluster.spec.backup` from `cnpg.cluster.backup`.
- If `backup.method` is omitted and `backup.volumeSnapshot` is present, the chart will infer `method: volumeSnapshot`.
- For snapshot backups, `backup.volumeSnapshot.className` is required (template fails early if missing).
- Example-only keys like `backup.ttl` and `backup.volumeSnapshot.enabled` are stripped to avoid CRD warnings.
- CNPG: render `Cluster.spec.plugins` from `cnpg.cluster.plugins` (enables barman-cloud plugin / WAL archiver configuration).
### Changed
- Consolidated `compose.example.pipelock.yml` into `compose.example.ai.yml` — Pipelock now runs alongside Ollama in one compose file with health checks, config volume mount, and MCP env vars (`MCP_API_TOKEN`, `MCP_USER_EMAIL`)
- CI: Pipelock scan `fail-on-findings` changed from `false` to `true`; added `exclude-paths` for locale help text false positives
## [0.6.7-alpha] - 2026-01-10
@@ -33,6 +36,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Production-ready HA timeouts: 200ms connect, 1s read/write, 3 reconnection attempts
- Backward compatible with existing `REDIS_URL` deployments
### [0.6.6] - 2025-12-31
### Added
- First version/release that aligns versions with monorepo
- CNPG: render `Cluster.spec.backup` from `cnpg.cluster.backup`.
- If `backup.method` is omitted and `backup.volumeSnapshot` is present, the chart will infer `method: volumeSnapshot`.
- For snapshot backups, `backup.volumeSnapshot.className` is required (template fails early if missing).
- Example-only keys like `backup.ttl` and `backup.volumeSnapshot.enabled` are stripped to avoid CRD warnings.
- CNPG: render `Cluster.spec.plugins` from `cnpg.cluster.plugins` (enables barman-cloud plugin / WAL archiver configuration).
### [0.0.0], [0.6.5]
### Added
- First (nightly/test) releases via <https://we-promise.github.io/sure/index.yaml>
## Notes
- Chart version and application version are kept in sync
- Requires Kubernetes >= 1.25.0

View File

@@ -11,6 +11,7 @@ The helper always injects:
- optional Active Record Encryption keys (controlled by rails.encryptionEnv.enabled)
- optional DATABASE_URL + DB_PASSWORD (includeDatabase=true and helper can compute a DB URL)
- optional REDIS_URL + REDIS_PASSWORD (includeRedis=true and helper can compute a Redis URL)
- optional HTTPS_PROXY / HTTP_PROXY / NO_PROXY (pipelock.enabled=true)
- rails.settings / rails.extraEnv / rails.extraEnvVars
- optional additional per-workload env / envFrom blocks via extraEnv / extraEnvFrom.
*/}}
@@ -77,6 +78,18 @@ The helper always injects:
{{- end }}
{{- end }}
{{- end }}
{{- if and $ctx.Values.pipelock.enabled (ne (toString (dig "forwardProxy" "enabled" true $ctx.Values.pipelock)) "false") }}
{{- $proxyPort := 8888 -}}
{{- if $ctx.Values.pipelock.forwardProxy -}}
{{- $proxyPort = int ($ctx.Values.pipelock.forwardProxy.port | default 8888) -}}
{{- end }}
- name: HTTPS_PROXY
value: {{ printf "http://%s-pipelock.%s.svc.cluster.local:%d" (include "sure.fullname" $ctx) $ctx.Release.Namespace $proxyPort | quote }}
- name: HTTP_PROXY
value: {{ printf "http://%s-pipelock.%s.svc.cluster.local:%d" (include "sure.fullname" $ctx) $ctx.Release.Namespace $proxyPort | quote }}
- name: NO_PROXY
value: "localhost,127.0.0.1,.svc.cluster.local,.cluster.local"
{{- end }}
{{- range $k, $v := $ctx.Values.rails.settings }}
- name: {{ $k }}
value: {{ $v | quote }}

View File

@@ -157,3 +157,27 @@ true
{{- default "redis-password" .Values.redis.passwordKey -}}
{{- end -}}
{{- end -}}
{{/* Pipelock image string */}}
{{- define "sure.pipelockImage" -}}
{{- $repo := "ghcr.io/luckypipewrench/pipelock" -}}
{{- $tag := "latest" -}}
{{- if .Values.pipelock.image -}}
{{- $repo = .Values.pipelock.image.repository | default $repo -}}
{{- $tag = .Values.pipelock.image.tag | default $tag -}}
{{- end -}}
{{- printf "%s:%s" $repo $tag -}}
{{- end -}}
{{/* Pipelock MCP upstream URL (auto-compute or explicit override) */}}
{{- define "sure.pipelockUpstream" -}}
{{- $upstream := "" -}}
{{- if .Values.pipelock.mcpProxy -}}
{{- $upstream = .Values.pipelock.mcpProxy.upstream | default "" -}}
{{- end -}}
{{- if $upstream -}}
{{- $upstream -}}
{{- else -}}
{{- printf "http://%s:%d/mcp" (include "sure.fullname" .) (int (.Values.service.port | default 80)) -}}
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,76 @@
{{- if .Values.pipelock.enabled }}
{{- $fwdEnabled := true -}}
{{- $fwdMaxTunnel := 300 -}}
{{- $fwdIdleTimeout := 60 -}}
{{- if .Values.pipelock.forwardProxy -}}
{{- if hasKey .Values.pipelock.forwardProxy "enabled" -}}
{{- $fwdEnabled = .Values.pipelock.forwardProxy.enabled -}}
{{- end -}}
{{- $fwdMaxTunnel = int (.Values.pipelock.forwardProxy.maxTunnelSeconds | default 300) -}}
{{- $fwdIdleTimeout = int (.Values.pipelock.forwardProxy.idleTimeoutSeconds | default 60) -}}
{{- end -}}
{{- $wsEnabled := false -}}
{{- $wsMaxMsg := 1048576 -}}
{{- $wsMaxConns := 128 -}}
{{- $wsScanText := true -}}
{{- $wsAllowBinary := false -}}
{{- $wsForwardCookies := false -}}
{{- $wsMaxConnSec := 3600 -}}
{{- $wsIdleTimeout := 300 -}}
{{- $wsOriginPolicy := "rewrite" -}}
{{- if .Values.pipelock.websocketProxy -}}
{{- if hasKey .Values.pipelock.websocketProxy "enabled" -}}
{{- $wsEnabled = .Values.pipelock.websocketProxy.enabled -}}
{{- end -}}
{{- $wsMaxMsg = int (.Values.pipelock.websocketProxy.maxMessageBytes | default 1048576) -}}
{{- $wsMaxConns = int (.Values.pipelock.websocketProxy.maxConcurrentConnections | default 128) -}}
{{- if hasKey .Values.pipelock.websocketProxy "scanTextFrames" -}}
{{- $wsScanText = .Values.pipelock.websocketProxy.scanTextFrames -}}
{{- end -}}
{{- if hasKey .Values.pipelock.websocketProxy "allowBinaryFrames" -}}
{{- $wsAllowBinary = .Values.pipelock.websocketProxy.allowBinaryFrames -}}
{{- end -}}
{{- if hasKey .Values.pipelock.websocketProxy "forwardCookies" -}}
{{- $wsForwardCookies = .Values.pipelock.websocketProxy.forwardCookies -}}
{{- end -}}
{{- $wsMaxConnSec = int (.Values.pipelock.websocketProxy.maxConnectionSeconds | default 3600) -}}
{{- $wsIdleTimeout = int (.Values.pipelock.websocketProxy.idleTimeoutSeconds | default 300) -}}
{{- $wsOriginPolicy = .Values.pipelock.websocketProxy.originPolicy | default "rewrite" -}}
{{- end }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "sure.fullname" . }}-pipelock
labels:
{{- include "sure.labels" . | nindent 4 }}
data:
pipelock.yaml: |
forward_proxy:
enabled: {{ $fwdEnabled }}
max_tunnel_seconds: {{ $fwdMaxTunnel }}
idle_timeout_seconds: {{ $fwdIdleTimeout }}
websocket_proxy:
enabled: {{ $wsEnabled }}
max_message_bytes: {{ $wsMaxMsg }}
max_concurrent_connections: {{ $wsMaxConns }}
scan_text_frames: {{ $wsScanText }}
allow_binary_frames: {{ $wsAllowBinary }}
forward_cookies: {{ $wsForwardCookies }}
strip_compression: true
max_connection_seconds: {{ $wsMaxConnSec }}
idle_timeout_seconds: {{ $wsIdleTimeout }}
origin_policy: {{ $wsOriginPolicy }}
dlp:
scan_env: true
response_scanning:
enabled: true
action: warn
mcp_input_scanning:
enabled: true
action: block
on_parse_error: block
mcp_tool_scanning:
enabled: true
action: warn
detect_drift: true
{{- end }}

View File

@@ -0,0 +1,101 @@
{{- if .Values.pipelock.enabled }}
{{- $fwdPort := 8888 -}}
{{- $mcpPort := 8889 -}}
{{- $pullPolicy := "IfNotPresent" -}}
{{- if .Values.pipelock.forwardProxy -}}
{{- $fwdPort = int (.Values.pipelock.forwardProxy.port | default 8888) -}}
{{- end -}}
{{- if .Values.pipelock.mcpProxy -}}
{{- $mcpPort = int (.Values.pipelock.mcpProxy.port | default 8889) -}}
{{- end -}}
{{- if .Values.pipelock.image -}}
{{- $pullPolicy = .Values.pipelock.image.pullPolicy | default "IfNotPresent" -}}
{{- end }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "sure.fullname" . }}-pipelock
labels:
{{- include "sure.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.pipelock.replicas | default 1 }}
selector:
matchLabels:
app.kubernetes.io/component: pipelock
{{- include "sure.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
app.kubernetes.io/component: pipelock
{{- include "sure.selectorLabels" . | nindent 8 }}
annotations:
checksum/config: {{ include (print $.Template.BasePath "/pipelock-configmap.yaml") . | sha256sum }}
{{- with .Values.pipelock.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
{{- $plSecrets := coalesce .Values.pipelock.image.imagePullSecrets .Values.image.imagePullSecrets }}
{{- if $plSecrets }}
imagePullSecrets:
{{- toYaml $plSecrets | nindent 8 }}
{{- end }}
volumes:
- name: config
configMap:
name: {{ include "sure.fullname" . }}-pipelock
containers:
- name: pipelock
image: {{ include "sure.pipelockImage" . }}
imagePullPolicy: {{ $pullPolicy }}
args:
- "run"
- "--config"
- "/etc/pipelock/pipelock.yaml"
- "--listen"
- "0.0.0.0:{{ $fwdPort }}"
- "--mode"
- {{ .Values.pipelock.mode | default "balanced" | quote }}
- "--mcp-listen"
- "0.0.0.0:{{ $mcpPort }}"
- "--mcp-upstream"
- {{ include "sure.pipelockUpstream" . | quote }}
volumeMounts:
- name: config
mountPath: /etc/pipelock
readOnly: true
ports:
- name: proxy
containerPort: {{ $fwdPort }}
protocol: TCP
- name: mcp
containerPort: {{ $mcpPort }}
protocol: TCP
livenessProbe:
httpGet:
path: /health
port: proxy
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: proxy
initialDelaySeconds: 3
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
resources:
{{- toYaml (.Values.pipelock.resources | default dict) | nindent 12 }}
nodeSelector:
{{- toYaml (.Values.pipelock.nodeSelector | default dict) | nindent 8 }}
affinity:
{{- toYaml (.Values.pipelock.affinity | default dict) | nindent 8 }}
tolerations:
{{- toYaml (.Values.pipelock.tolerations | default list) | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,30 @@
{{- if .Values.pipelock.enabled }}
{{- $fwdPort := 8888 -}}
{{- $mcpPort := 8889 -}}
{{- if .Values.pipelock.forwardProxy -}}
{{- $fwdPort = int (.Values.pipelock.forwardProxy.port | default 8888) -}}
{{- end -}}
{{- if .Values.pipelock.mcpProxy -}}
{{- $mcpPort = int (.Values.pipelock.mcpProxy.port | default 8889) -}}
{{- end }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "sure.fullname" . }}-pipelock
labels:
{{- include "sure.labels" . | nindent 4 }}
spec:
type: {{ (.Values.pipelock.service).type | default "ClusterIP" }}
selector:
app.kubernetes.io/component: pipelock
{{- include "sure.selectorLabels" . | nindent 4 }}
ports:
- name: proxy
port: {{ $fwdPort }}
targetPort: proxy
protocol: TCP
- name: mcp
port: {{ $mcpPort }}
targetPort: mcp
protocol: TCP
{{- end }}

View File

@@ -465,3 +465,53 @@ hpa:
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
# Pipelock: AI agent security proxy (optional)
# Provides forward proxy (outbound HTTPS scanning) and MCP reverse proxy
# (inbound MCP traffic scanning for prompt injection, DLP, tool poisoning).
# More info: https://github.com/luckyPipewrench/pipelock
pipelock:
enabled: false
image:
repository: ghcr.io/luckypipewrench/pipelock
tag: "0.2.7"
pullPolicy: IfNotPresent
imagePullSecrets: []
replicas: 1
# Pipelock run mode: strict, balanced, audit
mode: balanced
forwardProxy:
enabled: true
port: 8888
maxTunnelSeconds: 300
idleTimeoutSeconds: 60
mcpProxy:
port: 8889
# Auto-computed when empty: http://<fullname>:<service.port>/mcp
upstream: ""
# WebSocket proxy: bidirectional frame scanning for ws/wss connections.
# Runs on the same listener as the forward proxy at /ws?url=<ws-url>.
# Requires Pipelock >= 0.2.9 (or current dev build).
websocketProxy:
# Requires image.tag >= 0.2.9. Update pipelock.image.tag before enabling.
enabled: false
maxMessageBytes: 1048576 # 1MB per message
maxConcurrentConnections: 128
scanTextFrames: true # DLP + injection scanning on text frames
allowBinaryFrames: false # block binary frames by default
forwardCookies: false
maxConnectionSeconds: 3600 # 1 hour max connection lifetime
idleTimeoutSeconds: 300 # 5 min idle timeout
originPolicy: rewrite # rewrite, forward, or strip
service:
type: ClusterIP
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 128Mi
podAnnotations: {}
nodeSelector: {}
tolerations: []
affinity: {}

View File

@@ -1,21 +1,33 @@
# ===========================================================================
# Example Docker Compose file with additional Ollama service for LLM tools
# Example Docker Compose file with Ollama (local LLM) and Pipelock (agent
# security proxy)
# ===========================================================================
#
# Purpose:
# --------
#
# This file is an example Docker Compose configuration for self hosting
# Sure with Ollama on your local machine or on a cloud VPS.
# This file extends the standard Sure setup with two optional capabilities:
#
# The configuration below is a "standard" setup that works out of the box,
# but if you're running this outside of a local network, it is recommended
# to set the environment variables for extra security.
# Pipelock — agent security proxy (always runs)
# - Forward proxy (port 8888): scans outbound HTTPS from Faraday-based
# clients (e.g. ruby-openai). NOT covered: SimpleFin, Coinbase, or
# anything using Net::HTTP/HTTParty directly. HTTPS_PROXY is
# cooperative; Docker Compose has no egress network policy.
# - MCP reverse proxy (port 8889): scans inbound AI traffic (DLP,
# prompt injection, tool poisoning, tool call policy). External AI
# clients should connect to Pipelock on port 8889 rather than
# directly to Sure's /mcp endpoint. Note: /mcp is still reachable
# on web port 3000 (auth token required); Pipelock adds scanning
# but Docker Compose cannot enforce network-level routing.
#
# Ollama + Open WebUI — local LLM inference (optional, --profile ai)
# - Only starts when you run: docker compose --profile ai up
#
# Setup:
# ------
#
# To run this, you should read the setup guide:
# 1. Copy pipelock.example.yaml alongside this file (or customize it).
# 2. Read the full setup guide:
#
# https://github.com/we-promise/sure/blob/main/docs/hosting/docker.md
#
@@ -41,6 +53,17 @@ x-rails-env: &rails_env
DB_HOST: db
DB_PORT: 5432
REDIS_URL: redis://redis:6379/1
# MCP server endpoint — enables /mcp for external AI assistants (e.g. Claude, GPT).
# Set both values to activate. MCP_USER_EMAIL must match an existing user's email.
# External AI clients should connect via Pipelock (port 8889) for scanning.
MCP_API_TOKEN: ${MCP_API_TOKEN:-}
MCP_USER_EMAIL: ${MCP_USER_EMAIL:-}
# Route outbound HTTPS through Pipelock for clients that respect HTTPS_PROXY.
# Covered: OpenAI API (ruby-openai/Faraday). NOT covered: SimpleFin, Coinbase (Net::HTTP).
HTTPS_PROXY: "http://pipelock:8888"
HTTP_PROXY: "http://pipelock:8888"
# Skip proxy for internal Docker network services (including ollama for local LLM calls)
NO_PROXY: "db,redis,pipelock,ollama,localhost,127.0.0.1"
AI_DEBUG_MODE: "true" # Useful for debugging, set to "false" in production
# Ollama using OpenAI API compatible endpoints
OPENAI_ACCESS_TOKEN: token-can-be-any-value-for-ollama
@@ -50,6 +73,39 @@ x-rails-env: &rails_env
# OPENAI_ACCESS_TOKEN: ${OPENAI_ACCESS_TOKEN}
services:
pipelock:
image: ghcr.io/luckypipewrench/pipelock:latest # pin to a specific version (e.g., :0.2.7) for production
container_name: pipelock
hostname: pipelock
restart: unless-stopped
volumes:
- ./pipelock.example.yaml:/etc/pipelock/pipelock.yaml:ro
command:
- "run"
- "--config"
- "/etc/pipelock/pipelock.yaml"
- "--listen"
- "0.0.0.0:8888"
- "--mode"
- "balanced"
- "--mcp-listen"
- "0.0.0.0:8889"
- "--mcp-upstream"
- "http://web:3000/mcp"
ports:
# MCP reverse proxy — external AI assistants connect here
- "${MCP_PROXY_PORT:-8889}:8889"
# Uncomment to expose forward proxy endpoints (/health, /metrics, /stats):
# - "8888:8888"
healthcheck:
test: ["CMD", "/pipelock", "healthcheck", "--addr", "127.0.0.1:8888"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
networks:
- sure_net
# Note: You still have to download models manually using the ollama CLI or via Open WebUI
ollama:
profiles:
@@ -106,6 +162,10 @@ services:
volumes:
- app-storage:/rails/storage
ports:
# Web UI for browser access. Note: /mcp is also reachable on this port,
# bypassing Pipelock's MCP scanning (auth token is still required).
# For hardened deployments, use `expose: [3000]` instead and front
# the web UI with a separate reverse proxy.
- ${PORT:-3000}:3000
restart: unless-stopped
environment:
@@ -115,6 +175,8 @@ services:
condition: service_healthy
redis:
condition: service_healthy
pipelock: # Remove this block and unset HTTPS_PROXY/HTTP_PROXY to run without Pipelock
condition: service_healthy
dns:
- 8.8.8.8
- 1.1.1.1
@@ -132,6 +194,8 @@ services:
condition: service_healthy
redis:
condition: service_healthy
pipelock: # Remove this block and unset HTTPS_PROXY/HTTP_PROXY to run without Pipelock
condition: service_healthy
dns:
- 8.8.8.8
- 1.1.1.1

View File

@@ -1,275 +0,0 @@
# ===========================================================================
# Example Docker Compose file with Pipelock agent security proxy
# ===========================================================================
#
# Purpose:
# --------
#
# This file adds Pipelock (https://github.com/luckyPipewrench/pipelock)
# as a security proxy for Sure, providing two layers of protection:
#
# 1. Forward proxy (port 8888) — routes outbound HTTPS through Pipelock
# for clients that respect the HTTPS_PROXY environment variable.
#
# 2. MCP reverse proxy (port 8889) — scans inbound MCP traffic from
# external AI assistants bidirectionally (DLP, prompt injection,
# tool poisoning, tool call policy).
#
# Forward proxy coverage:
# -----------------------
#
# Covered (Faraday-based clients respect HTTPS_PROXY automatically):
# - OpenAI API calls (ruby-openai gem)
# - Market data providers using Faraday
#
# NOT covered (these clients ignore HTTPS_PROXY):
# - SimpleFin (HTTParty / Net::HTTP)
# - Coinbase (HTTParty / Net::HTTP)
# - Any code using Net::HTTP or HTTParty directly
#
# For covered traffic, Pipelock provides:
# - Domain allowlisting (only known-good external APIs can be reached)
# - SSRF protection (blocks connections to private/internal IPs)
# - DLP scanning on connection targets (detects exfiltration patterns)
# - Rate limiting per domain
# - Structured JSON audit logging of all outbound connections
#
# MCP reverse proxy coverage:
# ---------------------------
#
# External AI assistants connect to Pipelock on port 8889 instead of
# directly to Sure's /mcp endpoint. Pipelock scans all traffic:
#
# Request scanning (client → Sure):
# - DLP detection (blocks credential/secret leakage in tool arguments)
# - Prompt injection detection in tool call parameters
# - Tool call policy enforcement (blocks dangerous operations)
#
# Response scanning (Sure → client):
# - Prompt injection detection in tool response content
# - Tool poisoning / drift detection (tool definitions changing)
#
# The MCP endpoint on Sure (port 3000/mcp) should NOT be exposed directly
# to the internet. Route all external MCP traffic through Pipelock.
#
# Limitations:
# ------------
#
# HTTPS_PROXY is cooperative. Docker Compose has no egress network policy,
# so any code path that doesn't check the env var can connect directly.
# For hard enforcement, deploy with network-level controls that deny all
# egress except through the proxy. Example for Kubernetes:
#
# # NetworkPolicy: deny all egress, allow only proxy + DNS
# egress:
# - to:
# - podSelector:
# matchLabels:
# app: pipelock
# ports:
# - port: 8888
# - ports:
# - port: 53
# protocol: UDP
#
# Monitoring:
# -----------
#
# Pipelock logs every connection and MCP request as structured JSON to stdout.
# View logs with: docker compose logs pipelock
#
# Forward proxy endpoints (port 8888):
# http://localhost:8888/health - liveness check
# http://localhost:8888/metrics - Prometheus metrics
# http://localhost:8888/stats - JSON summary
#
# More info: https://github.com/luckyPipewrench/pipelock
#
# Setup:
# ------
#
# 1. Copy this file to compose.yml (or use -f flag)
# 2. Set your environment variables (OPENAI_ACCESS_TOKEN, MCP_API_TOKEN, etc.)
# 3. docker compose up
#
# Pipelock runs both proxies in a single container:
# - Port 8888: forward proxy for outbound HTTPS (internal only)
# - Port 8889: MCP reverse proxy for external AI assistants
#
# External AI clients connect to http://<host>:8889 as their MCP endpoint.
# Pipelock scans the traffic and forwards clean requests to Sure's /mcp.
#
# Customization:
# --------------
#
# Requires Pipelock with MCP HTTP listener support (--mcp-listen flag).
# See: https://github.com/luckyPipewrench/pipelock/releases
#
# Edit the pipelock command to change the mode:
# --mode strict Block unknown domains (recommended for production)
# --mode balanced Warn on unknown domains, block known-bad (default)
# --mode audit Log everything, block nothing (for evaluation)
#
# For a custom config, mount a file and use --config instead of --mode:
# volumes:
# - ./config/pipelock.yml:/etc/pipelock/config.yml:ro
# command: ["run", "--config", "/etc/pipelock/config.yml",
# "--mcp-listen", "0.0.0.0:8889", "--mcp-upstream", "http://web:3000/mcp"]
#
x-db-env: &db_env
POSTGRES_USER: ${POSTGRES_USER:-sure_user}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-sure_password}
POSTGRES_DB: ${POSTGRES_DB:-sure_production}
x-rails-env: &rails_env
<<: *db_env
SECRET_KEY_BASE: ${SECRET_KEY_BASE:-a7523c3d0ae56415046ad8abae168d71074a79534a7062258f8d1d51ac2f76d3c3bc86d86b6b0b307df30d9a6a90a2066a3fa9e67c5e6f374dbd7dd4e0778e13}
SELF_HOSTED: "true"
RAILS_FORCE_SSL: "false"
RAILS_ASSUME_SSL: "false"
DB_HOST: db
DB_PORT: 5432
REDIS_URL: redis://redis:6379/1
# NOTE: enabling OpenAI will incur costs when you use AI-related features in the app (chat, rules). Make sure you have set appropriate spend limits on your account before adding this.
OPENAI_ACCESS_TOKEN: ${OPENAI_ACCESS_TOKEN}
# MCP server endpoint — enables /mcp for external AI assistants (e.g. Claude, GPT).
# Set both values to activate. MCP_USER_EMAIL must match an existing user's email.
# External AI clients connect via Pipelock (port 8889), not directly to /mcp.
MCP_API_TOKEN: ${MCP_API_TOKEN:-}
MCP_USER_EMAIL: ${MCP_USER_EMAIL:-}
# Route outbound HTTPS through Pipelock for clients that respect HTTPS_PROXY.
# See "Forward proxy coverage" section above for which clients are covered.
HTTPS_PROXY: "http://pipelock:8888"
HTTP_PROXY: "http://pipelock:8888"
# Skip proxy for internal Docker network services
NO_PROXY: "db,redis,pipelock,localhost,127.0.0.1"
services:
pipelock:
image: ghcr.io/luckypipewrench/pipelock:latest
container_name: pipelock
hostname: pipelock
restart: unless-stopped
command:
- "run"
- "--listen"
- "0.0.0.0:8888"
- "--mode"
- "balanced"
- "--mcp-listen"
- "0.0.0.0:8889"
- "--mcp-upstream"
- "http://web:3000/mcp"
ports:
# MCP reverse proxy — external AI assistants connect here
- "${MCP_PROXY_PORT:-8889}:8889"
# Uncomment to expose forward proxy endpoints (/health, /metrics, /stats):
# - "8888:8888"
healthcheck:
test: ["CMD", "/pipelock", "healthcheck", "--addr", "127.0.0.1:8888"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
networks:
- sure_net
web:
image: ghcr.io/we-promise/sure:stable
volumes:
- app-storage:/rails/storage
ports:
# Web UI for browser access. Note: /mcp is also reachable on this port,
# bypassing Pipelock's MCP scanning (auth token is still required).
# For hardened deployments, use `expose: [3000]` instead and front
# the web UI with a separate reverse proxy.
- ${PORT:-3000}:3000
restart: unless-stopped
environment:
<<: *rails_env
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
pipelock:
condition: service_healthy
networks:
- sure_net
worker:
image: ghcr.io/we-promise/sure:stable
command: bundle exec sidekiq
volumes:
- app-storage:/rails/storage
restart: unless-stopped
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
pipelock:
condition: service_healthy
environment:
<<: *rails_env
networks:
- sure_net
db:
image: postgres:16
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
<<: *db_env
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" ]
interval: 5s
timeout: 5s
retries: 5
networks:
- sure_net
backup:
profiles:
- backup
image: prodrigestivill/postgres-backup-local
restart: unless-stopped
volumes:
- /opt/sure-data/backups:/backups # Change this path to your desired backup location on the host machine
environment:
- POSTGRES_HOST=db
- POSTGRES_DB=${POSTGRES_DB:-sure_production}
- POSTGRES_USER=${POSTGRES_USER:-sure_user}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-sure_password}
- SCHEDULE=@daily # Runs once a day at midnight
- BACKUP_KEEP_DAYS=7 # Keeps the last 7 days of backups
- BACKUP_KEEP_WEEKS=4 # Keeps 4 weekly backups
- BACKUP_KEEP_MONTHS=6 # Keeps 6 monthly backups
depends_on:
- db
networks:
- sure_net
redis:
image: redis:latest
restart: unless-stopped
volumes:
- redis-data:/data
healthcheck:
test: [ "CMD", "redis-cli", "ping" ]
interval: 5s
timeout: 5s
retries: 5
networks:
- sure_net
volumes:
app-storage:
postgres-data:
redis-data:
networks:
sure_net:
driver: bridge

36
pipelock.example.yaml Normal file
View File

@@ -0,0 +1,36 @@
# Pipelock configuration for Docker Compose
# See https://github.com/luckyPipewrench/pipelock for full options.
forward_proxy:
enabled: true
max_tunnel_seconds: 300
idle_timeout_seconds: 60
websocket_proxy:
enabled: false
max_message_bytes: 1048576
max_concurrent_connections: 128
scan_text_frames: true
allow_binary_frames: false
forward_cookies: false
strip_compression: true
max_connection_seconds: 3600
idle_timeout_seconds: 300
origin_policy: rewrite
dlp:
scan_env: true
response_scanning:
enabled: true
action: warn
mcp_input_scanning:
enabled: true
action: block
on_parse_error: block
mcp_tool_scanning:
enabled: true
action: warn
detect_drift: true