From 59bf72dc49641906c85a9eddd1d7a49d9061d97f Mon Sep 17 00:00:00 2001 From: LPW Date: Mon, 2 Mar 2026 17:26:01 -0500 Subject: [PATCH] feat(helm): add Pipelock ConfigMap, scanning config, and consolidate compose (#1064) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .github/workflows/pipelock.yml | 4 +- charts/sure/CHANGELOG.md | 46 ++- charts/sure/templates/_env.tpl | 13 + charts/sure/templates/_helpers.tpl | 24 ++ charts/sure/templates/pipelock-configmap.yaml | 76 +++++ .../sure/templates/pipelock-deployment.yaml | 101 +++++++ charts/sure/templates/pipelock-service.yaml | 30 ++ charts/sure/values.yaml | 50 ++++ compose.example.ai.yml | 78 ++++- compose.example.pipelock.yml | 275 ------------------ pipelock.example.yaml | 36 +++ 11 files changed, 437 insertions(+), 296 deletions(-) create mode 100644 charts/sure/templates/pipelock-configmap.yaml create mode 100644 charts/sure/templates/pipelock-deployment.yaml create mode 100644 charts/sure/templates/pipelock-service.yaml delete mode 100644 compose.example.pipelock.yml create mode 100644 pipelock.example.yaml diff --git a/.github/workflows/pipelock.yml b/.github/workflows/pipelock.yml index dfcb866ec..3668c0a49 100644 --- a/.github/workflows/pipelock.yml +++ b/.github/workflows/pipelock.yml @@ -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/ diff --git a/charts/sure/CHANGELOG.md b/charts/sure/CHANGELOG.md index f0d636ba5..b2d44fe72 100644 --- a/charts/sure/CHANGELOG.md +++ b/charts/sure/CHANGELOG.md @@ -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 - -### [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 + ## Notes - Chart version and application version are kept in sync - Requires Kubernetes >= 1.25.0 diff --git a/charts/sure/templates/_env.tpl b/charts/sure/templates/_env.tpl index ccf0c1b69..a0b230ac1 100644 --- a/charts/sure/templates/_env.tpl +++ b/charts/sure/templates/_env.tpl @@ -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 }} diff --git a/charts/sure/templates/_helpers.tpl b/charts/sure/templates/_helpers.tpl index 436127959..d36105db9 100644 --- a/charts/sure/templates/_helpers.tpl +++ b/charts/sure/templates/_helpers.tpl @@ -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 -}} diff --git a/charts/sure/templates/pipelock-configmap.yaml b/charts/sure/templates/pipelock-configmap.yaml new file mode 100644 index 000000000..f840961e2 --- /dev/null +++ b/charts/sure/templates/pipelock-configmap.yaml @@ -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 }} diff --git a/charts/sure/templates/pipelock-deployment.yaml b/charts/sure/templates/pipelock-deployment.yaml new file mode 100644 index 000000000..f35db3e49 --- /dev/null +++ b/charts/sure/templates/pipelock-deployment.yaml @@ -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 }} diff --git a/charts/sure/templates/pipelock-service.yaml b/charts/sure/templates/pipelock-service.yaml new file mode 100644 index 000000000..01be758c7 --- /dev/null +++ b/charts/sure/templates/pipelock-service.yaml @@ -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 }} diff --git a/charts/sure/values.yaml b/charts/sure/values.yaml index 3ecd95f94..349e88a23 100644 --- a/charts/sure/values.yaml +++ b/charts/sure/values.yaml @@ -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://:/mcp + upstream: "" + # WebSocket proxy: bidirectional frame scanning for ws/wss connections. + # Runs on the same listener as the forward proxy at /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: {} diff --git a/compose.example.ai.yml b/compose.example.ai.yml index e711fc8f4..fb85a354a 100644 --- a/compose.example.ai.yml +++ b/compose.example.ai.yml @@ -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 diff --git a/compose.example.pipelock.yml b/compose.example.pipelock.yml deleted file mode 100644 index b70bbb916..000000000 --- a/compose.example.pipelock.yml +++ /dev/null @@ -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://: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 diff --git a/pipelock.example.yaml b/pipelock.example.yaml new file mode 100644 index 000000000..d53f11a13 --- /dev/null +++ b/pipelock.example.yaml @@ -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