Security Best Practices Guide¶
A consolidated security reference for ARC-1 operators. This guide covers hardening, authentication, authorization, and incident response. It references detailed setup guides where appropriate rather than duplicating their content.
1. Security Architecture Overview¶
ARC-1 enforces authorization at three stacked gates. All relevant gates must allow an operation for it to succeed.
| Layer | What it checks | Controlled by |
|---|---|---|
| Server ceiling | Positive opt-in flags such as SAP_ALLOW_WRITES, SAP_ALLOW_FREE_SQL, SAP_DENY_ACTIONS |
ARC-1 administrator |
| User permission | Scopes from XSUAA/OIDC JWTs or API-key profiles | BTP/IdP/ARC-1 administrator |
| SAP authorization | SAP authorization objects (S_DEVELOP, S_ADT_RES, package auth, etc.) |
SAP Basis / role admin |
Checks are additive: server ceiling AND user scope/profile AND SAP authorization must all pass. If any layer blocks, the operation fails. The server safety config acts as a hard ceiling - user scopes can only restrict further, never expand beyond server config.
For the full model, scope definitions, API-key profiles, and BTP role mapping, see Authorization & Roles.
2. Authentication Methods and When to Use Each¶
| Scenario | Transport | MCP Client Auth | Recommended Guide |
|---|---|---|---|
| Local development (single user) | stdio | None | Local Development |
| Shared server (single access level) | HTTP | One ARC1_API_KEYS entry |
API Key Setup |
| Team server (role-based access) | HTTP | Multiple API keys with profiles | API Key Setup |
| Enterprise (per-user identity) | HTTP | OIDC / JWT | OAuth / JWT Setup |
| Enterprise + SAP audit trail | HTTP | OIDC / JWT + Principal Propagation | OAuth / JWT + PP Setup |
| BTP Cloud Foundry | HTTP | XSUAA OAuth | XSUAA Setup |
When XSUAA is enabled, the verifier can chain XSUAA, OIDC, and configured API keys. Separate strict
PP and API-key instances are the recommended topology. A single mixed instance is also supported:
set SAP_PP_STRICT=false explicitly, so JWT calls use PP and API-key calls use the shared SAP identity.
For the full decision guide and common combinations, see Authentication Overview.
3. OIDC/JWT Configuration Checklist¶
When using an external identity provider (Entra ID, Okta, Keycloak, etc.), configure these environment variables or CLI flags:
| Variable / Flag | Required | Description |
|---|---|---|
SAP_OIDC_ISSUER / --oidc-issuer |
Yes | OIDC issuer URL. Must match the iss claim in tokens. |
SAP_OIDC_AUDIENCE / --oidc-audience |
Yes | Expected aud claim value. For Entra ID v2.0, this is the raw client ID GUID. For v1.0, it is api://{client-id}. |
SAP_OIDC_CLOCK_TOLERANCE / --oidc-clock-tolerance |
No | Clock skew tolerance in seconds for exp/nbf validation. Default: 0. Useful when server and IdP clocks drift. |
Verification checklist:
- [ ]
SAP_OIDC_ISSUERmatches theissclaim in your tokens exactly (trailing slashes matter). - [ ]
SAP_OIDC_AUDIENCEmatches theaudclaim in your tokens (decode a token at jwt.ms to verify). - [ ] The OIDC provider includes ARC-1 scopes (
read,write,data,sql,transports,git,admin) in thescopeorscpclaim. Tokens without scope claims default to read-only access. - [ ] OIDC discovery at
{issuer}/.well-known/openid-configurationand its advertisedjwks_uriare reachable from ARC-1. - [ ] The issuer and JWKS TLS certificates are trusted by Node.js.
--insecureaffects SAP connections, not OIDC; add an internal CA withNODE_EXTRA_CA_CERTSwhen needed.
ARC-1 validates tokens per the OAuth 2.0 Protected Resource model (RFC 9700): signature verification via JWKS, issuer match, audience match, and expiration check.
4. API Key Security¶
Key Generation¶
Always use cryptographically random keys with sufficient entropy:
Per-Key Profiles¶
Use --api-keys to assign each key a profile with specific scopes and safety restrictions. The old single --api-key mode was removed because it made the access level ambiguous.
Key Rotation¶
- Rotate keys on a regular schedule (quarterly recommended) and immediately upon suspected compromise.
- Multi-key mode allows rolling rotation: add the new key, distribute it, then remove the old key.
- Audit logs include the profile name (e.g.,
api-key:viewer) to identify which key was used, aiding in compromise investigation. - API key rotation requires updating all clients that use the affected key.
Limitations¶
API keys identify roles, not individuals. They do not support per-user SAP audit trails. For user-level identity, use OIDC or XSUAA.
For full setup instructions, see API Key Setup.
5. Safety Configuration Best Practices¶
All safety flags are positive opt-ins (default: false / restrictive). Enable only what you need.
SAP API Policy and data-access gates¶
SAP's API Policy restricts large-scale extraction and ungoverned autonomous AI call patterns, which is part of why these two capabilities are gated. See SAP API Policy & Architecture Alignment for the clause-by-clause treatment and ARC-1's position; validate your productive setup against SAP documentation, your SAP agreement, and internal governance.
Two ARC-1 capabilities can expose business data or execute ad-hoc SQL and require explicit env vars before they are reachable:
| Capability | Env var | Default | Policy note |
|---|---|---|---|
Named table content preview (SAPRead(type=TABLE_CONTENTS)) |
SAP_ALLOW_DATA_PREVIEW=true |
false (off) |
Can expose application-table data; keep off unless the use case is approved. |
Freestyle ABAP SQL (SAPQuery) |
SAP_ALLOW_FREE_SQL=true |
false (off) |
Executes ad-hoc ABAP SQL; keep off unless the use case is approved. |
| Exact table/CDS blocklist (experimental) | SAP_BLOCKED_DATA_SOURCES=USR02,PA0002 |
empty (off) | Defense-in-depth denial with live CDS/replacement lineage; unresolved requests fail closed. Not an allowlist or SAP authorization replacement. |
Recommended security-focused profile. Keep structured data access and remove caller-authored SQL, which is the largest parser and SQL-Console surface:
Blocklist + free SQL remains a supported advanced profile for installations that genuinely need
SAPQuery; it is the less restrictive choice and subjects callers to the strict static-SQL subset.
Either way, apply SAP Note 3772411 independently — this feature is default-off and remediates
nothing, and SAP_ALLOW_WRITES=false does not neutralize a database-side mutation reached through a
vulnerable SQL Console host expression.
Recommendation for productive systems: keep both flags at their defaults unless there is an approved use case. ARC-1 still covers the core developer-tooling surface — read source/metadata, search, navigate, lint, write/activate ABAP objects, manage transports, drive Git workflows. Turning either flag on can be appropriate, but should be a deliberate operator decision against the current SAP API Policy, the customer's SAP agreement, SAP authorizations, and internal data-protection rules.
Recommended production defaults¶
| Setting | Recommended | Rationale |
|---|---|---|
SAP_ALLOW_WRITES |
false unless writes are needed |
Blocks every mutation — object writes, activation, transport writes, git writes. |
SAP_ALLOW_FREE_SQL |
false on sensitive systems |
Blocks arbitrary SQL queries against the database via SAPQuery. |
SAP_ALLOW_DATA_PREVIEW |
false unless table preview is required |
Blocks named table content preview. |
SAP_BLOCKED_DATA_SOURCES |
Exact sensitive sources when data access is approved; keep data access disabled if the target lacks the required catalog metadata | Experimental, default-off emergency brake, slower by design (extra SAP metadata calls, no cache). Requires catalog access to DD02L and DDLDEPENDENCY under the caller identity; blocking either prevents table-lineage proof. Missing data-preview support returns DATA_POLICY_UNAVAILABLE. Fails closed on unsupported lineage but leaves every unlisted source eligible. Not an allowlist, not a DCL replacement, and not a remediation for SAP Note 3772411. |
SAP_ALLOWED_PACKAGES |
$TMP or Z*,Y*,$TMP |
Restricts writes to custom-code packages. Prefix wildcards (Z*), exact matches, and DEVCLASS subtree rules (ZFOO/** — ZFOO plus every transitive sub-package) are all supported; subtree resolution is fail-closed on SAP errors. Reads are never package-gated. |
SAP_ALLOW_TRANSPORT_WRITES |
false unless CTS needed |
Opt-in for transport mutations (SAPTransport.create/release/delete). |
SAP_ALLOW_GIT_WRITES |
false unless Git needed |
Opt-in for gated abapGit mutations and SAP-side Git egress. It does not enable gCTS mutations, which remain quarantined before HTTP; accepted abapGit mutations without an authoritative postcondition return incomplete. |
SAP_DENY_ACTIONS |
Use for fine-grained blocks | E.g. SAPWrite.delete,SAPManage.flp_* — overrides scope + flag checks. |
SAP_PP_STRICT |
Explicit true for production PP |
Keeps the PP instance JWT-only. JWT PP failures always fail closed; explicit true also rejects API-key / non-JWT requests. |
API-key profiles (non-BTP multi-user)¶
For HTTP-streamable deployments without XSUAA/OIDC, use ARC1_API_KEYS with per-key profile names. Each profile maps to a scope set AND a partial SafetyConfig intersected with the server ceiling.
| Profile | Scopes | Default allowedPackages |
|---|---|---|
viewer |
[read] |
— |
viewer-data |
[read, data] |
— |
viewer-sql |
[read, data, sql] |
— |
developer |
[read, write, transports, git] |
$TMP |
developer-data |
[read, write, data, transports, git] |
$TMP |
developer-sql |
[read, write, data, sql, transports, git] |
$TMP |
admin |
all 7 scopes (implies everything) | (unrestricted) |
A user's effective safety is always the intersection of (1) the server ceiling, (2) their profile's partial safety, and (3) their JWT scopes. Per-user config can only tighten, never widen.
Important: API-key developer* profiles are sandboxed to $TMP by design. If a key must write to transportable packages, use a tightly scoped admin key with a narrow server-side SAP_ALLOWED_PACKAGES, or use OIDC/XSUAA for per-user authorization.
Full authorization model: authorization.md.
6. Scope Implications¶
ARC-1 scopes have transitive grants that operators should understand:
| Scope assigned | Scopes effectively granted | Reason |
|---|---|---|
write |
write + read |
A developer who can write can also read |
sql |
sql + data |
A user who can run freestyle SQL can also preview tables |
admin |
all 7 scopes | Emergency/operator profile, still limited by server flags |
read |
read only |
No transitive grants |
data |
data only |
No transitive grants |
transports |
transports only |
Specialized CTS scope; mutations also need write |
git |
git only |
Specialized Git scope; mutations also need write |
This means:
- Assigning
writewithoutreadis unnecessary --writealready includesread. - Assigning
sqlwithoutdatais unnecessary --sqlalready includesdata. - Assigning
datadoes NOT grantread(source code access) -- these are independent dimensions. - Assigning only
transportsor onlygitis not enough for mutations -- grantwriteas well.
The scope model separates several dimensions: objects (source code: read/write), data (table contents: data/sql), and shared infrastructure (CTS and Git: transports/git). A developer may need full source code access without being able to query production data, and vice versa.
7. Reverse Proxy Requirements¶
When deploying ARC-1 behind a reverse proxy (nginx, HAProxy, Traefik, etc.) outside of Cloud Foundry:
| Requirement | Details |
|---|---|
| TLS termination | Terminate TLS at the proxy. ARC-1's HTTP listener does not handle TLS natively. |
| Header sanitization | Strip or overwrite X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Host from incoming requests to prevent spoofing. Only the proxy should set these. |
| Proxy headers | Forward Host, X-Real-IP, and X-Forwarded-For to ARC-1 for accurate logging. |
| Health check | Expose /health without authentication for load balancer probes. |
| Timeouts | Set proxy read/write timeouts to at least 120 seconds. Some ADT operations (activation, unit tests) can take 30-60 seconds. |
| Request size | MCP JSON bodies default to 100 KiB. Administrators can set ARC1_MAX_HTTP_REQUEST_BYTES up to 16 MiB; raising only the proxy limit has no effect. Oversized requests return 413. Align proxy limits and timeouts, and review the request admission and memory guidance before raising the limit. |
Example nginx configuration is provided in the API Key Setup guide.
8. BTP-Specific Security¶
XSUAA Role Collections¶
Scopes are assigned to BTP users via role templates and role collections in the BTP Cockpit. The seven scopes (read, write, data, sql, transports, git, admin) map to role templates (MCPViewer, MCPDeveloper, MCPDataViewer, MCPSqlUser, MCPAdmin) that are combined into role collections for assignment.
Principal Propagation¶
When SAP_PP_ENABLED=true, each MCP user's JWT identity flows through to SAP via BTP Destination Service. For on-premise systems this routes through Connectivity Service + Cloud Connector principal propagation; for BTP ABAP Environment it uses a cloud-to-cloud destination such as OAuth2UserTokenExchange. SAP sees the real user identity for authorization checks and audit logging. JWT PP failures always fail closed. Separate strict PP and API-key instances are recommended. With explicit SAP_PP_STRICT=false, one supported instance can accept both: JWT calls use PP and API-key calls use the shared technical SAP identity.
Experimental multi-target v1 enforces strict Principal Propagation for PP destinations. It also has a
separate, default-off BasicAuthentication exception for mutation-free on-premise targets. That
exception requires ARC1_MULTI_TARGET_ALLOW_BASIC_AUTH=true, an XSUAA-authenticated caller, one
non-rolling CF process, a least-privileged technical SAP user, and a principal-type-None Cloud
Connector mapping with internal HTTPS. SAP then sees only the shared technical user, so human
attribution comes from ARC-1 audit records rather than SAP. It is never PP fallback and must not be
used when per-user SAP authorization or horizontal scaling is required. See
ADR-0007
and Multi-System Setup.
Shared SAP login lifetime and credential rotation¶
Single-target HTTP deployments reuse the shared identity's SAP login cookies and CSRF token across tool calls (#871). Each call still gets fresh object/package caches and its own scope restrictions. JWT principal-propagation clients and multi-target clients do not receive this shared transport.
Transport renewal: once a transport is ten minutes old, a new HTTP request gets a replacement.
Requests already created retain the old one, so lock/save/unlock and late responses stay together.
The age is measured with a monotonic clock. Renewal can add authentication/CSRF requests and latency;
the amount depends on the workload. ARC1_MAX_CONCURRENT limits simultaneous SAP requests, not
logons per minute. Stdio's existing transport lifetime is unchanged.
A password change is not session revocation. A replacement discards runtime cookies and
re-reads SAP_COOKIE_FILE, but can reload the same still-valid ticket. SAP_COOKIE_STRING and
startup-resolved destination credentials do not hot-reload. SAP can accept configured session
cookies or SSO tickets without checking the accompanying Basic password; existing requests may
also outlive ten minutes. Renewal is therefore not a fixed credential-revocation deadline.
See SAP's HTTP security session documentation.
For planned technical-user credential rotation, update ARC-1's configured credentials or shared destination and restart every instance so subsequent calls use the new configuration. A still-valid session can hide outdated credentials until renewal; repeated rejected logons can then lock the technical account under SAP's configured policy. A normal request may retry authentication once, whereas a rejected CSRF bootstrap may stop at its first attempt.
For urgent revocation, first stop access through ARC-1 and have the SAP administrator terminate the affected HTTP security sessions (SM05) and address outstanding SSO tickets under the system's incident procedure. Restarting ARC-1 discards its in-memory cookies; it does not revoke tickets copied elsewhere. Replace configured cookie files/strings too if using the development SSO bridge. ARC-1's existing 401 recovery is not a revocation mechanism and does not guarantee automatic recovery of a write.
SAP recommends login/create_sso2_ticket=3 to issue assertion tickets without logon tickets, but
legacy SSO consumers may require 2. Have the SAP owner assess that landscape-wide setting
separately; changing issuance does not revoke existing tickets. See
SAP's ticket configuration guidance.
SAP sees the shared technical user across calls. Use ARC-1's per-call audit records for MCP-user attribution, or use principal propagation when SAP must authorize and audit each human separately.
Destination Service¶
BTP Destination Service centralizes SAP connection details and credentials. ARC-1 resolves the destination at runtime. Use SAP_BTP_DESTINATION for shared-user destinations or the BTP ABAP OAuth2UserTokenExchange per-user destination. Use SAP_BTP_PP_DESTINATION when an on-premise shared startup destination and PrincipalPropagation destination must be separate.
If Cloud Connector uses path-level allowlists, include non-ADT OData routes needed by ARC-1 features, especially:
- /sap/opu/odata/UI2/PAGE_BUILDER_CUST for FLP launchpad management (SAPManage FLP actions)
- /sap/opu/odata/UI5/ABAP_REPOSITORY_SRV for UI5 ABAP Repository metadata reads
For detailed setup instructions:
- XSUAA Setup -- role templates, role collections, xs-security.json
- Principal Propagation Setup -- Cloud Connector, CERTRULE, per-user destinations
- BTP Destination Setup -- Destination Service configuration
- Authorization & Roles: BTP XSUAA role templates -- role-to-scope mapping
8a. Layered rate limiting¶
ARC-1 ships three independent rate-limiting layers, each addressing a distinct threat:
- Layer 1 — HTTP edge (per-IP,
express-rate-limit). OAuth endpoints useARC1_AUTH_RATE_LIMIT(default20/min/IP). All MCP route styles share the MCP HTTP value: unset derivesmax(OAuth × 30, 600), whileARC1_MCP_HTTP_RATE_LIMITcan replace it or explicitly disable it with0. Middleware runs before bearer auth and returns HTTP429withRetry-After+ RFC 9331 headers. - Layer 2 — Per-user MCP quota (per-user token bucket,
rate-limiter-flexible). Applied at the top ofhandleToolCall. Prevents one developer's runaway LLM from monopolizing the shared semaphore. Returns an MCP tool error with structuredretryAfter(not HTTP 429) so the agent loop backs off correctly. Single env var:ARC1_RATE_LIMIT— off by default, multi-user deployments opt in (typical:60/min/user). - Layer 3 — SAP-bound shared semaphore (server-wide FIFO queue). One
Semaphorefor the whole process, shared across allAdtClientinstances including per-user PP clients. Caps concurrent SAP HTTP requests atARC1_MAX_CONCURRENT(default10) — true server-wide, not per-user. HonorsRetry-Afteron429/503from SAP / BTP gateways (single retry, clamped to 60 s). Excess requests wait in queue; no rejection.
All three layers are per-instance and in-memory. Multi-instance attackers cost N × limit for Layers 1 + 2 — acceptable trade-off for the stateless-deployment property.
For the full operator picture (threat model, sizing math against rdisp/wp_no_dia, troubleshooting decision tree, opt-out per layer), see the Rate Limiting Guide. Design rationale: ADR-0004.
9. Audit Logging¶
ARC-1 emits structured audit events through three sink types. Stderr prints events at or above
the resolved log level (ARC1_LOG_LEVEL, default info; SAP_VERBOSE=true forces debug).
The configured file sink receives all audit levels, and the BTP Audit Log sink forwards the
security/data categories described below independently of that level. If stderr is your only
audit destination, keep info to retain tool-call events.
| Sink | Activation | Output |
|---|---|---|
| Stderr | Always active | JSON lines to stderr |
| File | Set --log-file / ARC1_LOG_FILE |
JSON lines appended to a file |
| BTP Audit Log | Auto-detected from a complete X.509 auditlog premium binding |
Categorized security and data events sent to BTP Audit Log Service v2 API |
Audit Log requires X.509 on both the service instance and binding. Incomplete bindings log an
ERROR and are skipped; delivery failures log a WARN at most once a minute without failing the
tool call. See setup and
certificate rotation.
What Gets Logged¶
| Event | Description |
|---|---|
tool_call_start |
Tool name and centrally redacted arguments. |
tool_call_end |
Tool, duration, success/error status, error class, and result size/preview after central redaction. |
http_request |
SAP HTTP method, ADT path, status, and duration. Optional debug bodies/headers are centrally redacted; authentication response bodies are never logged. |
data_response_limited |
A successful or retry response crossed the configured data-preview byte ceiling. Includes tool, limit/observed bytes, endpoint family, queue wait, request ID, and selected target/identity when applicable; never SQL or response bodies. |
http_csrf_fetch |
Debug-level CSRF probe response: method, endpoint, HTTP status, duration, session mode and context-cookie presence. success means this response supplied a usable token; a failed probe may recover through a fallback. Presence includes reset markers; it does not establish a valid context. No token/cookie values or bodies. |
auth_scope_denied |
Tool, required scope, and caller's available scopes when authorization rejects a call. |
auth_pp_created |
Success or failure while creating a per-user Principal Propagation ADT client. |
auth_shared_created |
Successful shared technical-user authentication after the Basic canary. Includes tool and identity: "shared". |
target_resolution_failed |
Multi-target ID/registry resolution failed. Includes tool and safe errorCode. |
pp_exchange_failed |
Per-user destination/token exchange failed before the SAP call. Includes tool and safe errorCode. |
shared_auth_failed |
Shared Basic credential preparation or canary failed. Includes tool and safe errorCode. |
cloud_connector_access_denied |
Cloud Connector did not expose or allow the selected target. Includes tool and safe errorCode. |
sap_service_unavailable |
A required SAP/ICF service is inactive or unavailable. Includes tool and safe errorCode. |
sap_authentication_failed |
SAP rejected the selected per-user or shared identity. Includes tool and safe errorCode. |
sap_authorization_failed |
SAP authenticated the identity but denied the operation. Includes tool and safe errorCode. |
target_policy_denied |
Instance/target policy denied a selected-target operation. Includes tool and safe errorCode. |
data_source_policy_decision |
Experimental data-source policy allowed or denied a query. Includes decision ID, direct sources, policy fingerprint, and metadata-work counters; denials include a reason. |
safety_blocked |
Safety ceiling blocked an operation; includes the operation and safe reason. |
server_start |
Server version, transport, write ceiling, configured target URL indicator, and process ID where available. |
activation_preaudit_completed |
Two-phase SAP activation preaudit result, reference count, and phase durations. |
oauth_client_registered |
XSUAA only: a new DCR client_id was minted (/register). Includes id length and redirect-URI count. |
oauth_client_lookup_failed |
XSUAA only: a client_id failed to resolve. reason ∈ {unknown_prefix, malformed, bad_signature, invalid_payload, expired}. Useful for spotting forgery / probing. |
oauth_redirect_uri_registered |
XSUAA only: a redirect URI matched the manual-client policy at /authorize and was added to ARC-1's in-memory client metadata. The XSUAA service configuration is unchanged. |
oauth_redirect_uri_rejected |
XSUAA only: an unapproved redirect URI was rejected at /authorize; useful for detecting interception attempts or bad client configuration. |
cors_rejected |
A browser request was blocked because its Origin header is not in ARC1_ALLOWED_ORIGINS. Includes origin, method, path. Useful for spotting misconfigured browser clients or probing. |
auth_rate_limited |
Layer 1 rate-limit denial on OAuth or /mcp endpoint (per-IP). Includes endpoint, IP, limitPerMinute. See Rate Limiting Guide. |
mcp_rate_limited |
Layer 2 rate-limit denial on per-user MCP tool quota. Includes user, tool, limitPerMinute, retryAfterMs. The MCP client receives a tool error with retryAfter (not HTTP 429). |
Every entry has timestamp, level, and event. Events within one MCP tool call share a
requestId; authenticated calls add user and clientId when available. Selected multi-target
calls also add destination, public target, and identity (per-user or shared). Destination
credentials, bearer tokens, cookies, authorization headers, and other secret values are centrally
redacted before any sink write.
Retention¶
- File sink: Retention is the operator's responsibility. Implement log rotation (e.g., logrotate) for long-running deployments.
- BTP Audit Log: Retention is managed by the BTP Audit Log Service per the service plan.
- Stderr: Transient unless captured by a container runtime or log aggregator.
10. Secrets Management¶
The following files and values must never be committed to version control:
| Secret | Storage Recommendation |
|---|---|
.env files |
Listed in .gitignore. Use environment variables or mounted files in production. |
SAP passwords (SAP_PASSWORD) |
Inject via environment variable, secrets manager, or cf set-env. |
API keys (ARC1_API_KEYS) |
Store in a secrets manager (Vault, AWS Secrets Manager, Azure Key Vault). |
BTP service keys (SAP_BTP_SERVICE_KEY) |
Use only for local BTP ABAP service-key OAuth or for creating BTP destinations. Prefer mounted files over inline JSON when local automation needs it. |
Cookie files (cookies.txt) |
Listed in .gitignore. Ephemeral by nature. |
XSUAA/Destination service credentials (VCAP_SERVICES) |
Inject through BTP service bindings or a secrets manager; never commit copied service credentials. |
In containerized deployments, prefer mounted secrets (Kubernetes Secrets, CF user-provided services) over environment variables, as environment variables may appear in process listings or crash dumps.
11. Network Security¶
OAuth Callback Server¶
The OAuth callback server (used for BTP ABAP browser login) binds exclusively to 127.0.0.1. It is not reachable from the network. This prevents network-adjacent attackers from intercepting the OAuth authorization code.
HTTP Streamable Transport¶
When ARC-1 runs with --transport http-streamable, the default bind address is 0.0.0.0:8080. In production:
- Always place ARC-1 behind a TLS-terminating reverse proxy or load balancer.
- Restrict network access using firewall rules, security groups, or VPN.
- ARC-1 refuses to start HTTP transport without
--api-keys,--oidc-issuer, or--xsuaa-authunlessARC1_ALLOW_HTTP_NO_AUTH=true/--allow-http-no-auth trueis set explicitly for local/dev use. - With
--xsuaa-auth, ARC-1 also refuses to start when the XSUAA binding cannot be loaded — it never falls back to an unauthenticated/mcp.
HTTP Security Headers (helmet)¶
When --transport http-streamable is active, every HTTP response (including /health, /mcp, OAuth endpoints) carries a curated set of browser security headers via helmet. These are always-on; there's no flag to disable them. Native MCP clients ignore these — they exist to harden the server when a browser ever reaches it.
| Header | Default value | Purpose |
|---|---|---|
Strict-Transport-Security |
max-age=15552000; includeSubDomains |
Force HTTPS for the host and its subdomains (180 days). |
Content-Security-Policy |
default-src 'self'; script-src 'self'; style-src 'self' https: 'unsafe-inline'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests; … |
Helmet's standard CSP. When CORS is enabled, only style-src is widened to allow inline styles for browser UIs; every other directive is preserved via useDefaults: true. |
Cross-Origin-Opener-Policy |
(not set) | Disabled. Microsoft Copilot Studio uses popup-based OAuth and relies on window.open() / postMessage to receive the redirect result. Any non-default COOP on /authorize (including same-origin-allow-popups) puts the popup in a separate browsing context group, severs the parent's window reference, and surfaces as "consent pop-up window has been closed unexpectedly". Helmet's stock same-origin has the same effect. ARC-1 renders no JS UI that would benefit from cross-origin isolation, so dropping COOP costs nothing. |
Cross-Origin-Resource-Policy |
same-origin (default) / cross-origin (when CORS is enabled) |
Auto-relaxed when ARC1_ALLOWED_ORIGINS is set so browser clients can read responses cross-origin. |
X-Content-Type-Options |
nosniff |
Prevents MIME-type confusion attacks. |
X-Frame-Options |
SAMEORIGIN |
Clickjacking guard for older browsers without CSP support. |
Referrer-Policy |
no-referrer |
Strips Referer on outbound navigations. |
Origin-Agent-Cluster |
?1 |
Asks browsers to isolate this origin's agent cluster. |
X-DNS-Prefetch-Control, X-Download-Options, X-Permitted-Cross-Domain-Policies, X-XSS-Protection |
(helmet defaults) | Legacy / browser-quirk hardening. |
To verify the headers on a running deployment:
curl -sI https://<your-app-url>/health | \
grep -iE 'strict-transport|content-security|cross-origin|x-content-type|x-frame|referrer'
Read-only UI surface¶
ARC1_UI is experimental and off by default. When enabled, ARC-1 serves static UI assets at /ui and read-only JSON endpoints under /ui/api/*. The endpoints expose sanitized config, safety/auth state, feature status, cache counts/source metadata, and recent sanitized audit events. They do not expose mutation controls, cached ABAP source bodies, request/response bodies, OAuth client IDs, or secrets.
In HTTP mode, ARC-1 refuses ARC1_UI=web unless an admin API key, OIDC, or XSUAA auth is configured, and the whole /ui/* subtree is mounted behind bearer auth with the admin scope. When ARC1_UI=off, no UI routes are mounted. On BTP CF, browser users should enter through the optional arc1-ui-router AppRouter (mta-ui-approuter.mtaext), which performs interactive XSUAA login and forwards the user JWT to ARC-1. In stdio mode, ARC1_UI=local binds only to loopback (127.0.0.1/localhost) and rejects non-loopback addresses.
CORS for browser-based MCP clients (opt-in)¶
CORS is off by default. The four MCP clients shipped with the project — Claude Desktop, Cursor, VS Code Copilot, Copilot Studio — use native HTTP, not the browser fetch API, and never trigger CORS. Only enable CORS when a browser UI (custom playground, embedded client, internal dashboard) calls /mcp directly:
cf set-env arc1-mcp-server ARC1_ALLOWED_ORIGINS "https://your-ui.example.com,https://other.example.com"
cf restage arc1-mcp-server
Configuration rules:
- Comma-separated, exact match. Wildcard patterns (
*,https://*.example.com) do not expand: they are treated as literal strings and do not match normal browser origins. - Pairs with
credentials: true. ARC-1 sendsAccess-Control-Allow-Origin: <reflected origin>(never*) andAccess-Control-Allow-Credentials: true. The wildcard form is incompatible with credentialed requests by browser policy. - Allowed methods:
GET,POST,DELETE,OPTIONS. Allowed request headers:Content-Type,Authorization,mcp-session-id,mcp-protocol-version,last-event-id. Exposed response headers:mcp-session-id. The 2026-07-28 modern-era headers (Mcp-Method/Mcp-Name/Mcp-Param-*) are deliberately absent — see ADR-0006. - Disallowed origins are silently dropped by the browser, but ARC-1 emits a
cors_rejectedaudit event server-side so misconfigured browser clients are observable. See §9 Audit Logging. - Browser-based DCR clients (rare) hitting
POST /registerorPOST /authorizefrom a foreign origin must be in the allowlist for the same reason native browser fetches are. See Stateless DCR for the OAuth flow.
To verify CORS on a running deployment:
# Allowed origin → 204 + Allow-Origin reflected
curl -sI -X OPTIONS \
-H "Origin: https://your-ui.example.com" \
-H "Access-Control-Request-Method: POST" \
https://<your-app-url>/mcp | \
grep -i 'access-control\|vary'
# Disallowed origin → no CORS headers (and a cors_rejected audit event)
curl -sI -X OPTIONS \
-H "Origin: https://evil.example.com" \
-H "Access-Control-Request-Method: POST" \
https://<your-app-url>/mcp | \
grep -i 'access-control' # expect: empty
SAP Connection¶
- Use HTTPS for the SAP connection (
SAP_URL=https://...) whenever possible. - Avoid
--insecure/SAP_INSECURE=truein production. If SAP uses an internal CA, configure it at the OS/Node.js level (NODE_EXTRA_CA_CERTSenvironment variable). - BTP on-premise deployments route through Cloud Connector; BTP ABAP Environment deployments use HTTPS directly to the ABAP Environment endpoint through the Destination service.
12. Incident Response¶
API Key Compromise¶
- Rotate immediately: Remove the compromised key from
ARC1_API_KEYSand restart ARC-1. - Review audit logs: Search for events with the compromised key's profile to assess the blast radius. Look for
tool_call_startandtool_call_endevents. - Generate a new key:
openssl rand -base64 32. Distribute to legitimate users. - Check for damage: If the key had write access, review recent transport requests and object modifications in the SAP system (SM21, STMS).
BTP Service Key Compromise¶
- Regenerate in BTP Cockpit: Delete the compromised service key and create a new one.
- Update dependent config: If the key is used locally, replace the service-key file and restart. If it was used to create an
OAuth2UserTokenExchangedestination, update that destination's client ID/secret. - Review BTP audit logs: Check for unauthorized access via the compromised credentials.
JWT / OIDC Token Compromise¶
- Revoke at the IdP: Disable the compromised user account or rotate the signing keys at the identity provider.
- Short-lived tokens limit exposure: JWT tokens typically expire in minutes to hours. Verify your IdP's token lifetime configuration.
- Check ARC-1 audit logs: Correlate the user's identity across
tool_call_startevents. - If PP was active: The attacker may have acted as the user in SAP. Check SAP security audit log (SM20) for the user's actions.
Cloud Connector PP Trust Compromise¶
This is the most critical on-premise PP compromise scenario -- a compromised Cloud Connector principal-propagation trust chain can assert SAP users.
- Revoke the CA immediately: Remove the CA certificate from SAP STRUST.
- Generate a new CA: Create a new key pair and import the new certificate into STRUST.
- Update Cloud Connector: Rotate the Cloud Connector PP certificates and verify subject-pattern rules.
- Audit all SAP activity: Review SM20 for all users during the compromise window.
13. Dependency & Supply-Chain Security¶
ARC-1 ships as an npm package and a Docker image consumed by enterprise customers running on regulated landscapes (banks, government, defense, pharma). Customers will run their own image scanners (Aqua, Prisma Cloud, Microsoft Defender) against the published image and reject vulnerable artifacts. ARC-1 layers its own supply-chain controls on top of GitHub-native primitives so issues are caught upstream of those scanners.
What runs in CI¶
| Control | Workflow | Severity gate |
|---|---|---|
| Dependabot — root npm + BTP AppRouter npm + GitHub Actions + Docker | .github/dependabot.yml |
weekly + same-day security advisories |
npm audit PR gates via audit-ci |
.github/workflows/test.yml (root + btp/approuter) |
fail on high / critical, subject to the expiring advisory exception below |
| GitHub Dependency Review (PR diff) | .github/workflows/dependency-review.yml |
fails on high; license allow/deny lists |
| CodeQL SAST (JavaScript/TypeScript) | GitHub Default Setup | findings on Security tab; PR check fails on High or higher |
| Trivy container scan — dev push | .github/workflows/docker.yml |
non-gating; SARIF uploaded to Security tab |
| Trivy container scan — scheduled | .github/workflows/security-scan.yml |
gating amd64 + arm64 maintenance signal on HIGH / CRITICAL; SARIF uploaded |
| Trivy container scan — release | .github/workflows/release.yml |
scan and SARIF upload are non-gating; neither can strand the Docker artifact |
Workflow-level permissions: contents: read |
all workflows | minimum GITHUB_TOKEN scope |
| Third-party action SHA pinning | googleapis/release-please-action, docker/*, aquasecurity/trivy-action |
mitigates the tj-actions/changed-files 2024 supply-chain compromise class |
| npm provenance | .github/workflows/release.yml (npm publish --provenance) |
every release tarball is Sigstore-attested |
| npm production SBOM | .github/workflows/release.yml (npm sbom --package-lock-only --omit=dev) |
best-effort, non-gating CycloneDX JSON release asset |
SECURITY.md policy |
repo root | private vulnerability reporting + severity-tiered response SLAs |
Docker BuildKit does not automatically invalidate a cached RUN apk upgrade when Alpine's
package repository changes. ARC-1 therefore names the final Dockerfile stage runtime and every
CI image build uses no-cache-filters: runtime plus pull: true. The comparatively expensive
native-module builder stage stays cached, while the runtime package upgrade is re-executed and can
pick up newly published OS security fixes.
Temporary node-forge audit exception¶
audit-ci.json temporarily accepts only
GHSA-86w9-cpqp-85rv, the RSA
signature-verification advisory affecting node-forge through 1.4.0 with no published patch as of
7 October 2026. The exception expires automatically at 00:00 Europe/Berlin on 21 October 2026.
It lets unrelated dependency maintenance proceed; it does not fix the vulnerability or establish
that ARC-1 cannot be affected. Remove the exception when the affected dependency paths are fixed,
or reassess it before expiry in a separate reviewed change.
Run npm run audit:ci and npm run audit:ci -- --directory btp/approuter to reproduce the PR
gates. Both retain the high/critical threshold, development dependencies, and the existing optional
dependency omission. Other advisories, including future node-forge advisories, still fail. The
accepted advisory remains visible in the output; after expiry it blocks again. Plain npm audit,
Dependabot alerts and update PRs, GitHub Dependency Review, and container scans keep their existing
behavior. The exception is specific to these two PR audit steps.
GitHub-native security features (verified enabled)¶
These toggles live on the repo’s Settings → Code security page. REST-exposed scanning/update settings were rechecked 2026-10-04; UI-only grouped-update and malware-alert settings below retain the 2026-05-08 observation and need an administrator to reconfirm.
| Feature | API verification | Status |
|---|---|---|
| Dependabot alerts | gh api repos/arc-mcp/arc-1/vulnerability-alerts -i \| head -1 → HTTP/2.0 204 |
✅ enabled |
| Dependabot security updates | gh api repos/arc-mcp/arc-1 --jq '.security_and_analysis.dependabot_security_updates.status' → "enabled" |
✅ enabled |
| Dependabot version updates | reads .github/dependabot.yml (in repo root) — toggled on at the same time as security updates; verify activity in Insights → Dependency graph → Dependabot |
✅ enabled |
| Dependabot grouped security updates | toggled on in Settings → Code security; no public REST field — verify by inspecting any auto-opened security PR (groups multiple advisories per ecosystem into one PR) | ✅ enabled |
| Dependabot malware alerts | toggled on in Settings → Code security; no public REST field — verify only via the Security tab when an alert fires | ✅ enabled |
| Secret scanning | gh api repos/arc-mcp/arc-1 --jq '.security_and_analysis.secret_scanning.status' → "enabled" |
✅ enabled |
| Push protection | gh api repos/arc-mcp/arc-1 --jq '.security_and_analysis.secret_scanning_push_protection.status' → "enabled" |
✅ enabled |
| Private vulnerability reporting | gh api repos/arc-mcp/arc-1/private-vulnerability-reporting --jq .enabled → true |
✅ enabled |
Optional toggles not enabled (deliberate — listed here so the absence is documented, not silent):
secret_scanning_non_provider_patterns— custom regex patterns. Off by default; only worth turning on if we need to scan for project-specific secret formats (we don't).secret_scanning_validity_checks— asks the upstream provider whether a leaked token is still valid. Off because the noise/value tradeoff doesn't justify it for a project our size; revisit if the validity API stabilizes and a customer asks.
User-account-level recommendation (cannot be enforced via repo settings): the project maintainer should also enable push protection at user level, which catches secrets pushed to any repo the maintainer commits to (including private forks of arc-1).
Verifying the chain as an operator¶
# 1. npm package — verify the published tarball was built from this repo
npm install arc-1
npm audit signatures arc-1
# Expected: "audited <N> packages — verified <N> packages with Sigstore"
# 2. npm package — download and inspect the production dependency SBOM
VERSION=<version>
gh release download "v${VERSION}" \
--repo arc-mcp/arc-1 \
--pattern "arc-1-${VERSION}-sbom.cdx.json"
jq -e --arg version "$VERSION" '
.bomFormat == "CycloneDX" and
.metadata.component.name == "arc-1" and
.metadata.component.version == $version and
.metadata.component.type == "application"
' "arc-1-${VERSION}-sbom.cdx.json"
# Expected: true
# 3. Installed npm dependency graph — review current advisories
npm audit --audit-level=high
# Nonzero exit means advisories at or above the threshold need review.
# 4. Docker image — scan locally with the same scanner CI uses
trivy image ghcr.io/arc-mcp/arc-1:<version> \
--severity HIGH,CRITICAL \
--exit-code 1
# Nonzero exit means HIGH/CRITICAL findings need review.
# 5. View the full advisory history for the project
open https://github.com/arc-mcp/arc-1/security/advisories
The release SBOM describes the production npm graph resolved from the root package-lock.json.
It does not inventory Alpine packages in the Docker image, the assembled MCPB contents, or
dynamically loaded extensions. Those artifacts need their own build-output SBOMs; do not use the
npm SBOM as evidence for their full contents.
SBOM publication is deliberately non-gating. A generation, validation, or GitHub upload error
remains visible in the publish-npm-sbom job, but continue-on-error: true prevents it from failing
or blocking the npm, Docker, MCPB, or MCP Registry release. Maintainers can regenerate and attach a
missing asset later.
Reporting a vulnerability¶
See SECURITY.md. Preferred channel is GitHub Private Vulnerability Reporting; fallback is email. Do not open a public issue or post on the SAP Community before the maintainers acknowledge the report — that bypasses coordinated disclosure and can put deployed instances at risk.
Roadmap¶
This section corresponds to roadmap entry SEC-11 (Tier 1: Foundation). Future tiers extend the chain:
- Tier 2 (Attestation) — the production npm CycloneDX release asset is complete. Image/MCPB SBOM coverage, Cosign keyless image signing, and OpenSSF Scorecard remain in
docs/plans/2026-05-08-dependency-security-tier2-attestation.md. - Tier 3 (Active Defense) — Socket.dev PR review, vulnerability triage runbook, formal non-adoption decisions for Renovate / Snyk / SLSA L3. Plan in
docs/plans/2026-05-08-dependency-security-tier3-defense.md.