Back to product

ChefStackZ Connector for Zammad

Bidirectional Jira Cloud and Zammad sync — complete documentation

Version 1.0.0 GA · Updated April 2026

On this page

1. Introduction

ChefStackZ Connector for Zammad is the bridge between Jira Cloud and a self-hosted Zammad helpdesk. Engineering teams stay in Jira; support agents stay in Zammad; the same ticket appears on both sides with status, priority, title, assignee, reporter, comments and attachments kept in sync within roughly five seconds.

The product is designed for organisations that already separate engineering and customer support tooling and need a stable, auditable link between the two without consolidating support data into Atlassian Cloud.

Who it's for

  • Engineering teams on Jira Cloud who need visibility into the support tickets that triggered a bug or feature request, without leaving Jira.
  • Support teams on self-hosted Zammad who need to hand a customer issue to engineering and watch its lifecycle without re-keying anything.
  • Privacy-sensitive workloads where regulatory or contractual obligations require that ticket bodies, customer PII and attachments never leave the customer-controlled Zammad host. Privacy mode is vertical-agnostic and applies equally to legal, financial, HR, education and public-sector deployments.
  • Operators who want a five-minute install with no manual schema work in Zammad and no infrastructure beyond the existing docker-compose stack.

What customers see at a glance

  • In Jira, every issue gains a Zammad panel showing the linked support ticket: number, live title, bucketed status (Open / Pending / Closed) and a click-through link.
  • In Zammad, every ticket sidebar gains four custom fields plus a one-click Search Jira to link deep-link that opens an in-network agent UI for searching and linking by click. Bulk-link, create-new-Jira-from-Zammad and identity reconciliation all live in the same UI.
  • Both directions stay in sync within ~5 seconds. Editing a Jira issue's status updates Zammad; editing a Zammad ticket's Jira link updates the Jira panel; comments posted on either side appear on the other; attachments transfer in both directions up to 4 MB per file.

2. How it works

ChefStackZ ships in two parts: a Forge app on the Atlassian Marketplace (Jira side) and a customer-installed Docker sidecar (Zammad side). The two halves talk to each other over signed HTTPS. The sidecar always initiates outbound calls; the Forge runtime never reaches into the customer's network.

2.1 Architecture overview

The Forge half holds three pieces of state per install: a TTL'd link cache (ticket ID + URL + bucketed status), the field-mapping configuration, and an append-only audit log. None of those carry ticket bodies, customer emails or attachments. Everything customer-facing lives in Zammad.

The sidecar half is a Fastify HTTP server that runs alongside Zammad in the same docker-compose network. It exposes only the docker-network port (8787 by default); there is no inbound port from outside the customer network. Zammad reaches it via the compose network; agents reach the linking UI from their browser via an nginx location block on the existing Zammad hostname.

Traffic flows in two directions:

  • Zammad to Jira (Z to J). Zammad fires an outbound webhook on every ticket change. The sidecar verifies the X-Hub-Signature HMAC, enriches the payload, signs the result with HMAC-SHA256 plus timestamp and nonce, then POSTs to a Forge web trigger URL. Forge dedupes on (installId, ticketId, changedAt), updates the link cache, and the next render of the Jira panel reflects the change.
  • Jira to Zammad (J to Z). The Forge app subscribes to Jira issue update and comment events, enqueues the diffs, and the sidecar long-polls a pull-pending web trigger every ~5 seconds (default FORGE_PULL_INTERVAL_MS=5000) to drain them. Each drained item is applied via Zammad's REST API.

2.2 Trust boundaries

HopDirectionAuthenticationReplay protection
Zammad to sidecarinbound webhookHMAC-SHA1 (X-Hub-Signature)Logical dedup on (installId, ticketId, changedAt) upstream
Sidecar to Forgeoutbound HTTPSHMAC-SHA256 over install_id|timestamp|nonce|body_sha2565-minute timestamp window + 6-minute nonce TTL
Forge to sidecarnever happensn/an/a
Sidecar to Zammadoutbound RESTZammad personal access token (ZAMMAD_API_TOKEN)n/a (same network)
Browser to sidecar UIinboundSame-origin guard on state-changing routesPer-route rate limits

2.3 No external.fetch.backend permission

The Forge manifest at forge/manifest.yml deliberately omits external.fetch.backend. Atlassian's Forge platform refuses to allowlist arbitrary https://* targets for the egress surface, so the v1.0 architecture is sidecar-only: every cross-boundary call originates inside the customer network. A future v2 may introduce an opt-in Direct mode that calls Zammad directly from Forge for installs that whitelist a per-install URL; v1.0 does not need it.

2.4 What persists where

DataLives inRetention
Ticket bodies, customer emails, attachments, agent notesCustomer's ZammadCustomer's own retention policy
Link cache: ticket id, number, url, bucketed statusForge storage.app24-hour TTL per entry
Ticket title (default mode only)Forge storage.app24-hour TTL per entry
Field-mapping configurationForge storage.appLifetime of the install
Identity mappings (Jira accountId to Zammad email)Forge storage.appLifetime of the install
Privacy mode flagForge storage.appLifetime of the install
Audit logForge storage.app (append-only, bounded)Last N entries
Sidecar provisioning sentinelSidecar named volume chefstackz-dataUntil manually deleted
HMAC noncesIn-memory TTL'd map on the sidecar and on Forge6 minutes

3. Install (five minutes)

The install path is designed so that a Jira admin and a Zammad operator together can be live in five minutes. The Forge admin page generates a fully populated .env file containing the install ID, the shared HMAC secret, the admin secret and all five web-trigger URLs. The only value the operator types by hand is the Zammad API token.

3.1 Prerequisites

  • An existing Zammad install running via docker-compose (the upstream zammad-docker-compose layout works out of the box; other layouts work too, with a network-name adjustment).
  • Shell access to the Zammad host with permissions to run docker and edit the Zammad nginx config.
  • The Forge app already installed in your Jira Cloud site from the Atlassian Marketplace.
  • The Manage apps permission on the Jira site (the admin page enforces this on top of the Atlassian platform gate via the requireJiraAdmin() resolver guard).

3.2 Install bundle contents

FileWhat it is
chefstackz-connector-X.Y.Z.tarThe sidecar Docker image, ready to docker load
docker-compose.ymlDrop-in service entry for your Zammad compose stack
.env.exampleEnv template — fallback only, see step 3
nginx-snippet.confLocation blocks to add to your Zammad nginx config
INSTALL.mdFull step-by-step install guide bundled with the artefact

3.3 Step 1 — Load the Docker image

docker load -i chefstackz-connector-X.Y.Z.tar

Verify with docker images | grep chefstackz. You should see one row tagged chefstackz-zammad-connector:X.Y.Z.

3.4 Step 2 — Create the Zammad API token

The sidecar needs a Zammad personal access token with four scopes. In Zammad:

  1. Click your profile avatar (top-right) then Profile.
  2. In the sidebar: Token Access then + New Personal Access Token.
  3. Name: anything (for example, ChefStackZ sidecar).
  4. Permissions — tick all four:
    • ticket.agent (read/write tickets and custom attributes)
    • admin.object (create the four jira_* ticket attributes)
    • admin.trigger (create the outbound webhook trigger)
    • admin.macro (create the Clear Jira link one-click macro)
  5. Click Create. Copy the token (you will not see it again).

3.5 Step 3 — Download the .env from the Forge admin page

In your Jira Cloud site go to Settings → Apps → Zammad Connector. Click Download .env. Your browser saves chefstackz.env. Move it next to your docker-compose.yml:

mv ~/Downloads/chefstackz.env ./.env

Open the file and paste your Zammad API token (from step 2) on the ZAMMAD_API_TOKEN= line. Save. Eight other values — install id, shared secret, admin secret and five web-trigger URLs — are already filled in.

Fallback: the same admin section has an Or copy individual values (advanced) disclosure with one Copy button per field. Use this if your browser blocks downloads. You'll need to copy seven values plus the generated ADMIN_SECRET hidden in the disclosure into your local .env file.

3.6 Step 4 — Start the sidecar

If your Zammad runs with the upstream zammad-docker-compose layout:

docker compose \
  -f /path/to/zammad-docker-compose/docker-compose.yml \
  -f docker-compose.yml \
  --env-file .env \
  up -d

Otherwise, drop the services: block from docker-compose.yml into your existing compose file and run docker compose up -d as you usually do.

The sidecar will:

  1. Wait for Zammad to respond (exponential backoff up to 60s if Zammad is still booting in the same compose stack).
  2. Auto-provision Zammad — creating jira_issue_key, jira_issue_url, jira_issue_status and jira_link_search ticket attributes, the outbound webhook, two triggers, and the Clear Jira link macro.
  3. Write a sentinel at /data/.provisioned so subsequent restarts skip provisioning.

Watch it land:

docker logs -f chefstackz-zammad-connector

You're looking for auto-provision: succeeded within ~10 seconds.

3.7 Step 5 — Verify

Refresh the Forge admin page. The Sidecar status badge should flip to connected within ~30 seconds.

3.8 Step 6 — Expose the agent UI (recommended)

Without this step, agents can't click the Search Jira to link deep-link from inside Zammad — it would point at the docker-network hostname which their browsers cannot reach.

  1. Open nginx-snippet.conf from the bundle.
  2. Paste both location blocks INSIDE the server { ... } block of your Zammad nginx config (typically zammad-nginx.conf).
  3. Reload nginx: docker exec zammad-nginx nginx -s reload (or your equivalent).
  4. Edit .env: set SIDECAR_AGENT_URL=https://YOUR-ZAMMAD-HOST/jira-link/
  5. docker compose restart zammad-jira-sidecar

Now in Zammad, open any ticket. The right sidebar's Search Jira to link field shows a clickable URL ending in /jira-link/?ticket=<id>. Clicking it opens the agent linking UI on the same hostname as Zammad — no separate hostname, no extra TLS certificate.

4. Daily use

4.1 Engineer workflow (in Jira)

An engineer working a Jira issue sees the Zammad panel on every issue page. The panel renders one row per linked Zammad ticket showing:

  • Ticket number (#54321), clickable to open in Zammad.
  • Live ticket title, fetched on render and cached for at most 24 hours.
  • Bucketed state pill: Open, Pending or Closed.
  • Optional requester block (name, email, organisation), suppressed under Privacy mode.
  • Last-updated timestamp.

The engineer takes no action to receive updates — changing the Jira issue's status or comments propagates back to Zammad automatically. Removing the link is done from the panel's overflow menu.

4.2 Agent workflow (in Zammad)

An agent working a Zammad ticket sees four custom fields in the right sidebar:

  • jira_issue_key — the linked Jira issue key (for example PAY-1234). Editable.
  • jira_issue_url — the canonical URL to the linked Jira issue. Auto-computed.
  • jira_issue_status — the linked Jira issue's bucketed status. Read-only; updates via the puller.
  • jira_link_search — a clickable URL that opens the agent linking UI scoped to the current ticket.

Clicking the jira_link_search link takes the agent to the sidecar UI at SIDECAR_AGENT_URL/?ticket=<id>. From there:

  • Search — the agent enters a JQL fragment or natural-language query, picks a project from the dropdown, and the UI returns matching Jira issues with already-linked badges.
  • Link — one click writes the chosen issue key onto the Zammad ticket. The Zammad sidebar refreshes within seconds.
  • Bulk link — select multiple search results and link them all in one action.
  • Create new — if the Jira side doesn't yet have an issue, the agent picks a project and issue type, supplies a summary, and a new Jira issue is created and auto-linked.
  • Clear link — the bundled Clear Jira link macro removes the link in one click from the Zammad ticket's macro menu.

4.3 Field updates flow both ways

When either side edits a mapped field (status, priority, title, assignee, reporter), the change propagates to the other side within ~5 seconds. The five-field mapping defaults are seeded on first read. Any conflict (both sides change at once) is resolved by last-writer-wins, with the rejected change preserved in the audit log.

5. Privacy mode

Privacy mode is the headline differentiator and a one-click toggle in the Forge admin page. It is vertical-agnostic: the same setting protects legal, financial, HR, education, public-sector and any other privacy-sensitive workload equally. Privacy mode is positioned as regulation-proof boundary minimisation, not as a certification.

5.1 What it does

With Privacy mode enabled, every identifying free-text field is stripped before any payload crosses into the Atlassian boundary:

  • Ticket title: removed at the sidecar boundary. The Jira panel renders #NNN, a state pill and an Open in Zammad link. Title is also purged from any cached link entries on the OFF-then-ON transition (otherwise titles cached during the 24-hour TTL would leak until they aged out).
  • Comment bodies: replaced with a metadata breadcrumb of the form [Comment by jane@acme.example at 14:32 — see Zammad ticket #54321].
  • Attachments: replaced with a filename + size stub (report.pdf (2.3 MB) — file in Zammad ticket #N). Bytes never cross.
  • Requester block: name, email and organisation are absent from the panel.
  • Title field mapping: cannot be enabled while Privacy mode is on. The admin page enforces this both on the read overlay and on the persist step (defence in depth).

With Privacy mode disabled (the default), the panel shows the live ticket title, comments mirror in full, and attachments transfer up to 4 MB per file.

5.2 How it is enforced

Three independent layers enforce Privacy mode:

  1. Sidecar boundary. The sidecar's PRIVACY_STRICT_MODE env var (default false) gates the outbound enrichment. When true, ticket titles and article bodies are replaced with metadata stubs before the HMAC signature is computed — the bytes never reach the wire.
  2. Forge schema validator. forge/src/schemas/webhook-event.ts enforces a strict subset under Privacy mode. A misbehaving sidecar that includes a title field under Privacy mode is rejected at the boundary, audited, and the event is dropped.
  3. Forge render-time guard. The issue panel reads the Privacy flag from storage.app and refuses to render any title field, comment body or attachment metadata even if it somehow ended up in the cache.

5.3 Honest scope

Privacy mode is an architectural mitigation: ChefStackZ minimises what crosses to the Atlassian boundary. It is not a substitute for hosting customer data on infrastructure that meets your own regulatory obligations. Customer data continues to live on your own Zammad host (which you are free to run on whichever certified or non-certified hosting you need). The Privacy mode label refers exclusively to ChefStackZ behaviour at the Atlassian boundary.

5.5 Default off

Privacy mode is off by default in fresh installs. New customers can evaluate the connector with full title and content sync, then enable Privacy mode whenever procurement, audit or contractual requirements call for it. The toggle takes effect on the next outbound event from the sidecar; cached titles are purged immediately on the OFF-to-ON transition.

6. Field mapping

The five-field mapping defines how mutable fields propagate between Jira and Zammad. Defaults are seeded on first read; admins customise them in the Forge admin page under Field mapping.

6.1 The five mapped fields

FieldZammad sourceJira targetDirectionPrivacy mode
statusstate (open / pending / closed)statusCategorybothallowed (bucketed enum only)
prioritypriorityprioritybothallowed
titletitlesummarybothlocked off
assigneeownerassigneebothallowed via identity reconciliation
reportercustomerreporterbothallowed via identity reconciliation

6.2 Identity reconciliation

Assignee and reporter mapping requires resolving Zammad users to Atlassian accountIds. The admin page hosts an identity-mapping table where each row pairs a jiraAccountId with a zammadEmail. Three ways to populate it:

  • Auto-match by email. The admin:autoMatchIdentities resolver walks the customer's Zammad user list and queries Jira's /rest/api/3/user/search for each email. Exact-email matches are inserted automatically; mismatches surface as unmatched rows for the admin to fill manually.
  • Manual entry. The admin types a Zammad email and picks the Atlassian account from a dropdown.
  • Override. An auto-matched row can be overridden manually if the admin wants to map a Zammad user to a different Jira account.

The Atlassian scope read:jira-user is requested specifically for the auto-match flow; it is read-only and never mutates Jira users.

6.3 Disable a mapping

Each row in the mapping table has an enabled toggle. Disabling a row stops propagation in both directions for that field; existing values are left in place. Restore defaults at any time with the Restore default mappings button.

6.4 Conflicts

If both sides change a mapped field within the ~5-second sync window, the later writer wins. The earlier change is preserved in the audit log with verb mapping.failed so an admin can reconcile manually if needed.

7. Comments & attachments

7.1 Comment sync

Comments propagate in both directions on every linked ticket pair within ~5 seconds.

  • Z to J. Zammad agent articles fire on the outbound webhook. The sidecar forwards them to the FORGE_COMMENT_URL web trigger, which writes them as Jira issue comments via api.asApp().requestJira(...). Internal Zammad articles map to internal Jira comments; public articles map to public Jira comments.
  • J to Z. The Forge trigger handler subscribes to avi:jira:commented:issue. On each Jira comment, it filters to issues with a Zammad link, then enqueues a PendingCommentEntry in Forge storage. The sidecar's pull-pending loop drains the queue and writes each comment as a Zammad article via the REST API.

7.2 Visibility mapping

Zammad article typeJira comment visibility
Public (internal=false)Public (no restriction)
Internal (internal=true)Restricted (visible to project role only)

7.3 Attachment sync

  • Attachments transfer in both directions up to 4 MB per file (the Forge function-runtime body cap).
  • Files larger than 4 MB are skipped with a metadata-only audit entry pointing the operator at the source ticket.
  • Under Privacy mode, attachment bytes are never transferred. Both directions degrade to a filename-and-size stub written as a comment on the receiving side: report.pdf (2.3 MB) — file in Zammad ticket #N.

7.4 Required Atlassian scope

Comment and attachment writes require the write:jira-work scope. This was added in the v0.9.0 bridge release with one re-consent prompt; subsequent additive features (create-from-Zammad, attachments) reuse the same scope so customers see no further re-consent.

9. Compliance artefacts

Two PDF artefacts are downloadable from the Forge admin page on demand. Both are generated client-side via jspdf from a server-side JSON payload, so the Forge function never holds a PDF in memory and stays well inside the 25-second function timeout.

9.1 Data Residency Report

A one-page PDF that an admin can hand to an auditor or procurement team. It enumerates exactly what data persists on which side, the retention policies, the trust boundaries and the active Privacy mode state. Generated by admin:downloadResidencyReport; the structured data builder lives in buildResidencyReportData.

Contents include:

  • Install ID and app version.
  • Privacy mode state (on/off) at generation time.
  • Per-data-category storage location and retention.
  • Sub-processor list (Atlassian Cloud, customer's Zammad).
  • HMAC algorithms in use on each hop.

9.2 DPA addendum

A two-page addendum to the Marketplace EULA that names the customer (legal name) and asserts the data-handling commitments described in the report. The customer legal name is persisted in Forge storage so admins don't retype it on every download. The addendum download fails clearly if the legal name is unset, with a pointer back to the admin page Compliance section.

9.3 Audit log export

The Forge admin page exposes the most recent N audit entries via admin:listAuditEntries. Each entry has a sequence number, install ID, verb, ticket ID, Jira key, source (sidecar / forge / jira) and timestamp. Sorted newest-first for display. See section 13 for the full audit-log schema.

10. Configuration reference

All sidecar configuration is via environment variables, validated with Zod at startup. Missing or invalid values fail-fast in production.

10.1 Required variables

VariableWhat
ZAMMAD_URLBase URL of the customer's Zammad (for example http://zammad-railsserver:3000). Must be HTTPS in production.
ZAMMAD_API_TOKENZammad API token with ticket.agent, admin.object, admin.trigger and admin.macro scopes.
INSTALL_IDPer-install identity issued by the Forge admin page. Production guard catches the missing case at boot.
FORGE_SHARED_SECRETPer-install shared secret. The sidecar exits non-zero in NODE_ENV=production if empty.
FORGE_WEBHOOK_URLForge web trigger for ticket events.
FORGE_PULL_URLForge web trigger to drain Jira-to-Zammad updates.
FORGE_SEARCH_URLForge web trigger for JQL search.
ADMIN_SECRETGates /admin/provision. Generated by Forge and baked into the downloaded .env; rotate with openssl rand -hex 32.

10.2 Optional Forge web triggers

VariableWhat happens if unset
FORGE_PROJECTS_URLProject dropdown is empty; agents type project clauses by hand into JQL.
FORGE_LINKED_STATUS_URLNo already linked badges render in search results.
FORGE_COMMENT_URLComment sync is silently disabled. Status sync still works.
FORGE_CREATE_ISSUE_URLThe Create new tab in the agent UI silently disappears.
FORGE_ISSUE_TYPES_URLThe issue-type dropdown stays empty and the create form blocks with a no issue types available message.

10.3 Operational tunables

VariableDefaultWhat
PRIVACY_STRICT_MODEfalseStrip ticket titles, article bodies and attachment bytes at the sidecar boundary before any payload reaches Forge. See section 5.
PROVISION_ON_BOOTtrueAuto-run /admin/provision on first boot when the sentinel is absent.
PROVISION_SENTINEL_PATH/data/.provisionedFile whose existence marks already provisioned.
SIDECAR_PUBLIC_URLhttp://chefstackz-zammad-connector:8787URL Zammad uses to reach the sidecar over the docker network.
SIDECAR_AGENT_URLfalls back to SIDECAR_PUBLIC_URLURL agents use to reach /ui/ from their browser. Different from SIDECAR_PUBLIC_URL when containers and browsers are on different networks (e.g. dev: http://localhost:8787; prod: https://YOUR-ZAMMAD-HOST/jira-link/).
FORGE_PULL_INTERVAL_MS5000Base interval between pull-pending polls. Backoff multiplies this on failure.
HOST0.0.0.0Bind address.
PORT8787Bind port.
LOG_LEVELinfoOne of fatal, error, warn, info, debug, trace.
NODE_ENVdevelopmentSet to production for JSON logs and fail-fast on missing secrets.

11. Admin page reference

The Forge admin page lives at Jira Settings → Apps → Zammad Connector and is gated by the Atlassian platform's manage apps permission, with the requireJiraAdmin() resolver guard at forge/src/auth.ts as defence in depth. Every admin action is one resolver call.

11.1 Status section

  • Sidecar status badge (admin:getSidecarHealth) — connected when the sidecar has POSTed within the last hour; shows time-since-last-seen and 24-hour event count.
  • Test connection button (admin:testConnection) — probes the configured transport.

11.2 Install secrets section

  • Download .env (admin:downloadEnv) — returns a fully populated env file: install id, shared secret, admin secret and all five web-trigger URLs.
  • Download install bundle — links to the latest sidecar tarball on chefstackz.com.
  • Or copy individual values (advanced) — per-field copy buttons via admin:getInstallSecrets.
  • Rotate shared secret (admin:rotateSharedSecret) — generates a new FORGE_SHARED_SECRET with one click. Customer pastes the new value into .env and restarts.

11.3 Privacy mode section

  • Privacy mode toggle (admin:setPrivacyConfig) — on/off. Transitioning OFF to ON triggers a one-shot walker that strips ticket titles from cached link entries (purgeTitlesFromLinkCache) so cached entries don't leak titles for up to 24 hours.

11.4 Field mapping section

  • Field mappings table (admin:listFieldMappings, admin:upsertFieldMapping, admin:deleteFieldMapping, admin:restoreDefaultMappings).
  • Identity mappings table (admin:listIdentityMappings, admin:upsertIdentityMapping, admin:deleteIdentityMapping).
  • Auto-match by email (admin:autoMatchIdentities).

11.5 Compliance section

  • Customer legal name (admin:getCustomerLegalName, admin:setCustomerLegalName).
  • Download Data Residency Report (admin:downloadResidencyReport).
  • Download DPA addendum (admin:downloadDpaAddendum) — errors with a clear message if legal name is unset.

11.6 Audit log section

  • Recent audit entries (admin:listAuditEntries) — newest-first, default limit 50.

12. Auto-provisioning

The sidecar configures Zammad on first boot so the operator never has to curl into Zammad's REST API by hand.

12.1 What gets provisioned

  • Four ticket attributes: jira_issue_key, jira_issue_url, jira_issue_status, jira_link_search.
  • One outbound webhook pointing at the sidecar's /zammad-events endpoint, signed with the configured ZAMMAD_WEBHOOK_SECRET.
  • Two triggers that fire the webhook on ticket create and ticket update.
  • One macro: Clear Jira link, which removes the link in one click from the Zammad ticket macro menu.

12.2 Lifecycle

  1. On every boot the sidecar loads .env, validates with Zod, and fails fast in production if FORGE_SHARED_SECRET or INSTALL_ID is empty.
  2. Starts the HTTP server.
  3. If PROVISION_ON_BOOT=true (default), ADMIN_SECRET is set, AND the sentinel at PROVISION_SENTINEL_PATH (default /data/.provisioned) is absent: pings Zammad's /users/me with exponential backoff up to 60s, then runs the provisioning logic.
  4. On success: writes the sentinel with a JSON timestamp. Subsequent boots see the sentinel and skip.
  5. On failure: retries indefinitely with exponential backoff. The operator can fix Zammad and the sidecar catches up without restart.

12.3 Idempotent

The provisioning logic is idempotent. Anything already in Zammad is left alone; only missing pieces are created. Force a re-run by removing the sentinel:

docker exec chefstackz-zammad-connector rm /data/.provisioned
docker compose restart zammad-jira-sidecar

12.4 Disable

Set PROVISION_ON_BOOT=false in .env and restart. Run provisioning manually:

docker exec -it chefstackz-zammad-connector \
  curl -X POST http://localhost:8787/admin/provision \
       -H "X-Admin-Secret: $ADMIN_SECRET"

13. Audit log

Every cross-system event is recorded in an append-only audit log, scoped per install and bounded to the last N entries. The log is the canonical source of truth for "what did the connector do for this install".

13.1 What is logged

FieldContents
Sequence numberMonotonically increasing per install
Install IDForge install identifier
Verblink.upsert, link.invalidate, notify.sent, comment.bridged, attachment.uploaded, schema.warn, schema.reject, cache.purge, mapping.failed, etc.
Sourcesidecar, forge or jira
Zammad ticket IDID only, no title, no body
Jira issue keyKey only (e.g. PAY-1234)
TimestampISO-8601 UTC

13.2 What is NOT logged

  • Ticket titles or bodies.
  • Comment contents (full or truncated).
  • Customer or agent email addresses.
  • Attachment bytes or filenames.
  • Jira or Zammad authentication tokens.
  • Any HTTP request body that has not first passed through redactSecretsFromBody.

13.3 Property test

A property test in forge/src/__tests__/ asserts that no input payload substring (titles, bodies, customer emails) ever appears in a serialised audit row even when the input deliberately contains it. The test is part of the CI gate on every PR.

14. Troubleshooting

"auto-provision skipped — ADMIN_SECRET not set"

.env isn't being picked up. Make sure you ran docker compose --env-file .env up -d or that .env is in the same directory as docker-compose.yml.

"FORGE_SHARED_SECRET is empty in production"

Same root cause. The sidecar will not start without FORGE_SHARED_SECRET in NODE_ENV=production; restart it after fixing .env.

Sidecar starts but admin page never says "connected"

Zammad isn't firing the trigger. Check docker exec zammad-railsserver tail log/production.log for trigger errors. Most common cause: the API token is missing the admin.trigger scope. Re-create the token with all four scopes (see section 3.4).

Agents see "deep link unreachable" when clicking from the Zammad sidebar

Either step 6 of the install (nginx snippet) isn't done, or SIDECAR_AGENT_URL doesn't match what nginx serves. Check it from your own browser first; the sidecar's /healthz endpoint should return {"ok":true}.

Status pill is stale in the Jira panel

The link cache has a 24-hour TTL on the cached entry but live status is fetched on render. If the panel shows stale data, hard-refresh the Jira issue page; the next render will fetch live. If the problem persists, the puller may be backed off — check docker logs chefstackz-zammad-connector for pull-pending errors.

HMAC signature mismatch on every event

The FORGE_SHARED_SECRET in .env doesn't match the one Forge stored. Re-download the .env from the admin page (it always returns the persisted value) or rotate the secret via Rotate shared secret, paste the new value into .env and restart.

Comments are not propagating

Check three things: (a) FORGE_COMMENT_URL is set in .env (it's included by default in the downloaded file); (b) the write:jira-work scope was granted at the v0.9.0 re-consent prompt — regrant by re-installing if missed; (c) the audit log shows comment.bridged entries. If verb schema.reject appears, the sidecar payload failed schema validation — look at the rejection reason in the entry.

Attachments fail silently for large files

The Forge function-runtime body cap is 4 MB. Files larger than that are skipped with an audit entry pointing the operator at the source ticket; the attachment stays in the originating system and is not lost.

Force re-provisioning

The sentinel file is what tells the sidecar Zammad already set up. Delete it and restart:

docker exec chefstackz-zammad-connector rm /data/.provisioned
docker compose restart zammad-jira-sidecar

Healthcheck endpoints

  • http://localhost:8787/healthz — liveness; returns {"ok":true} as long as the process is up.
  • http://localhost:8787/healthz/zammad — Zammad reachability + token validity. Returns 503 with the underlying Zammad error if the token is wrong or Zammad is unreachable.

15. Security model

Every externally reachable surface is layered with defence-in-depth controls. The full security posture is documented at chefstackz.com/security and in the Marketplace security questionnaire.

15.1 Authentication on every hop

HopMechanism
Sidecar to ForgeHMAC-SHA256 over install_id|timestamp|nonce|body_sha256, base64. Verified in constant time.
Zammad to sidecarHMAC-SHA1 (X-Hub-Signature, GitHub scheme). Refuses to silently downgrade to unsigned in production.
Forge admin actionsAtlassian platform session + requireJiraAdmin() guard.
Browser to /link-ticket and /search-jiraSame-origin guard — Origin header must match request Host. Browser fetch always sends Origin; curl/script attempts fail closed.

15.2 Replay protection

  • Sidecar to Forge: 5-minute timestamp window plus 6-minute nonce TTL. Replay outside the window is a 401; replay inside the window with a seen nonce is a 401.
  • Logical dedup on (installId, ticketId, changedAt) at the Forge web trigger handler protects against the same logical event arriving via two paths.

15.3 Cryptography in use

UseAlgorithm
Sidecar to Forge envelope HMACHMAC-SHA256
Zammad to sidecar webhook HMACHMAC-SHA1 (Zammad's X-Hub-Signature scheme)
Body integritySHA-256 (body hash bound into the signed envelope)
Constant-time comparisoncrypto.timingSafeEqual
Secret generationcrypto.randomBytes (32-byte for shared secret and admin secret)

HMAC-SHA1 on the Zammad inbound is acceptable because it is keyed (not a plain hash) and the threat model is forgery, not collision. Future Zammad versions that offer SHA-256 will be adopted on a forward-compatible basis — the verifier dispatches on the X-Hub-Signature algorithm prefix.

15.4 Secret storage

SecretWhere storedRotation
FORGE_SHARED_SECRETForge setSecret (encrypted at rest)Admin-page Rotate button + sidecar restart with new value
ADMIN_SECRETCustomer's Docker .envopenssl rand -hex 32 + sidecar restart
ZAMMAD_API_TOKENCustomer's Docker .env (redacted from logs)Customer rotates in Zammad UI; pastes into .env
Atlassian OAuth tokensAtlassian platform; ChefStackZ never seesAtlassian-managed

15.5 Network defences

  • Per-route rate limits via @fastify/rate-limit: 5 req/min on /admin/provision, 30 on /link-ticket, 60 on /search-jira, 300 on /zammad-events.
  • Security headers on every response: X-Frame-Options: SAMEORIGIN, X-Content-Type-Options: nosniff, Content-Security-Policy with script-src 'self', Referrer-Policy: strict-origin-when-cross-origin.
  • Input validation: jiraKey enforced as ^[A-Z][A-Z0-9_]{0,19}-\d{1,9}$; jiraUrl restricted to http(s); JQL capped at 500 chars and rejects email and phone-shaped substrings.
  • Secret redaction on every error path: ZammadApiError echo-bodies run through a sanitiser that strips Authorization: Token ..., Bearer ..., JSON-shaped password / secret / api_token fields, and Cookie / Set-Cookie lines before any log output.

15.6 Atlassian scopes

  • read:jira-work — read issues for the panel; read changelogs and comments for the trigger handlers; run JQL for the agent UI search.
  • write:jira-work — write Jira comments, attachments and field updates.
  • read:jira-user — identity-reconciliation auto-match (read-only; never mutates Jira users).
  • storage:app — Forge link cache, audit log, configuration.

No external.fetch.backend, no manage:jira-configuration, no Confluence scopes.

15.7 Multi-tenant isolation

Every Forge storage write is scoped to the install via Forge's per-install namespace. The schema validator additionally requires installId in every payload and rejects mismatches. A dedicated test tier asserts install-A data never appears in install-B reads, including the audit log, the link cache, the pending-puller queue and the Privacy mode configuration.

15.8 Bug bounty

ChefStackZ is enrolled in Atlassian's Marketplace bug-bounty programme. Reports go to the channel listed at chefstackz.com/security. We triage within one business day and pay out per the Atlassian bounty programme's published tariff. Public disclosure follows after a fix has shipped to all installs.

16. FAQ

Does ChefStackZ copy customer support data into Atlassian?

No. The Forge app holds only ticket IDs, URLs, bucketed status, optional title (default mode), and the requester block (default mode). Ticket bodies, comment contents, attachments and customer PII never persist in Forge storage. Live ticket titles are fetched on render and cached for at most 24 hours; under Privacy mode they're stripped at the sidecar boundary and never reach Forge at all.

Can I run this against Zammad Cloud?

v1.0 is designed for self-hosted Zammad. The sidecar runs alongside Zammad in the same docker-compose network. Zammad Cloud customers should contact support@chefstackz.com to discuss feasibility.

Does the Forge app reach into my network?

No. The Forge app declares no external.fetch.backend permission. Every cross-boundary call originates from the sidecar inside your network. Forge can only respond to inbound requests from the sidecar.

What happens if my sidecar is down?

Zammad's outbound webhooks queue and retry per Zammad's webhook-retry policy. The Forge pull-pending queue holds Jira-side updates indefinitely until the sidecar resumes polling. No data is lost; the sync simply pauses until the sidecar comes back. The admin-page sidecar status badge will flip to not seen recently after roughly an hour.

Can I use this to migrate tickets from Zammad to Jira?

No — this is a sync product, not a migration tool. Each side remains the source of truth for its native data: Zammad for support tickets, Jira for engineering work. The link is referential.

Does Privacy mode affect performance?

Negligibly. The body-substitution step at the sidecar boundary is microseconds. The Forge schema validator already runs on every request whether Privacy mode is on or off; it just enforces a stricter subset under Privacy mode.

How do I know the auto-provisioning ran correctly?

Run docker logs chefstackz-zammad-connector and look for auto-provision: succeeded. Confirm in Zammad: Settings → Objects → Tickets should show four jira_* attributes; Settings → Triggers should show two ChefStackZ triggers; Settings → Macros should show Clear Jira link.

Can multiple Jira issues link to one Zammad ticket?

Yes. The Zammad jira_issue_key field is single-valued by default but the agent UI's bulk-link flow writes additional Remote Issue Links on the Jira side, all globally identified by zammad:<installId>:<ticketId>. Each Jira issue therefore knows its Zammad ticket; a Zammad ticket lists its primary Jira link in the sidebar plus any others in the linking UI.

What languages are supported?

v1.0 ships English (en-US) only. Additional locales are tracked for v1.1 based on customer demand.

Where does my data physically reside?

Atlassian-side data (issues, comments) lives in the Atlassian region you chose for your Jira Cloud site. The Forge link cache lives in the same region. Customer support data lives on your own Zammad host — ChefStackZ never copies or persists it outside Zammad. There are no ChefStackZ-operated servers in the data path.

How is the app updated?

The Forge app updates automatically via the Atlassian Marketplace. The sidecar is updated by downloading a new bundle from chefstackz.com, running docker load, updating the image: line in docker-compose.yml and running docker compose up -d. Your .env and the named volume chefstackz-data carry over automatically.

Can I uninstall cleanly?

Yes. Stop and remove the sidecar container; uninstall the Forge app from your Jira site. The Forge platform purges storage.app on uninstall, removing the link cache, configuration and audit log. The four jira_* attributes, triggers and macro remain in Zammad — delete them manually if you no longer need them, or leave them in case you reinstall.

17. Support

Direct support requests go to support@chefstackz.com. Security reports go to security@chefstackz.com or via the channel listed at chefstackz.com/security.

The full marketplace listing is at chefstackz.com. Documentation, install bundles and a contact form for incident reports are all linked from the home page.

ChefStackZ is an independent integration. Zammad is a registered trademark of the Zammad Foundation; this app is not affiliated with or endorsed by the Foundation.