> For the complete documentation index, see [llms.txt](https://tsanet.gitbook.io/connect/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tsanet.gitbook.io/connect/documentation/zendesk-app/security-considerations.md).

# Security Considerations

Security posture of the TSANet Connect integration for Zendesk: trust boundaries, data flows, authentication and secret storage for the ZAF sidebar app and ZIS, transport security, least privilege, an

This document describes how the TSANet Connect integration for Zendesk is secured. It is written in two layers: a posture summary for security and IT reviewers, and an internal hardening section that is candid about known tradeoffs and the roadmap to close them.

The integration has two moving parts:

* **The ZAF sidebar app** runs inside Zendesk, in the support agent's browser.
* **ZIS (Zendesk Integration Services)** runs server-side inside Zendesk's own infrastructure and handles inbound delivery from TSANet.

Nothing is hosted outside Zendesk and TSANet — there is no external server, scheduler, or third-party runner in the architecture.

{% hint style="info" %}
**Bottom line.** No data leaves Zendesk except what is required to collaborate with a partner over TSANet, and every connection is TLS-encrypted. The TSANet API password is stored as a Zendesk **secure setting** (kept out of the app's front-end code and injected server-side at login), and it belongs to a dedicated service account scoped to your TSANet membership. ZIS connection secrets live in Zendesk's server-side secret store. See [Credential Handling](#authentication-and-credential-storage) and [Known Considerations and Hardening](#known-considerations-and-hardening).
{% endhint %}

## Architecture and Trust Boundaries

There are three trust zones. Data crosses a boundary only at the arrows below.

| Zone                       | What runs there                              | Trust boundary                                  |
| -------------------------- | -------------------------------------------- | ----------------------------------------------- |
| **Agent browser**          | The ZAF sidebar app (UI + background poller) | Cross-origin sandboxed iframe served by Zendesk |
| **Zendesk infrastructure** | ZIS flows, connections, ticket data          | Zendesk's hosted platform and secret store      |
| **TSANet Connect**         | The TSANet API and partner routing           | External party, reached over TLS                |

The agent browser never talks to the partner directly. Outbound requests go to the TSANet API; inbound events arrive from TSANet into ZIS. The partner's systems and your systems are never directly connected. TSANet sits in the middle and routes the collaboration.

## Data Flows and What Leaves Zendesk

| Direction         | Data that crosses the boundary                                                                   | Goes to                  |
| ----------------- | ------------------------------------------------------------------------------------------------ | ------------------------ |
| Outbound request  | The collaboration detail your agent enters (problem summary, partner, responding engineer email) | TSANet, then the partner |
| Inbound request   | The partner's case detail                                                                        | Into a Zendesk ticket    |
| Notes and replies | Only **public** notes and public replies                                                         | TSANet, then the partner |
| Internal comments | Never leave Zendesk                                                                              | Stay on the ticket       |

Two controls limit what can leave:

* **Public-only propagation.** Only public content reaches the partner. Internal Zendesk comments are never forwarded. A fail-closed guard on the comment forwarding flow only forwards replies authored by an **agent or admin**; an end customer's public reply is never forwarded.
* **Registered-domain enforcement.** TSANet rejects any submitter or responding engineer email that is not on your company's TSANet-registered domain. This is enforced server-side by TSANet, independent of the app, and prevents a case from being attributed to a customer address or a personal account.

### Every outbound destination, enumerated

For egress review, two origins matter: calls from the agent's or admin's **browser**, and calls Zendesk makes server-side on the integration's behalf (the ZAF proxy and ZIS).

| Destination                                   | Initiated from                                                                           | Credential                                         | What is sent                                                                                                               |
| --------------------------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `static.zdassets.com`                         | Agent/admin browser                                                                      | None                                               | The ZAF SDK script request itself, nothing else. Version-pinned with subresource integrity                                 |
| `connect2.tsanet.net` / `connect2.tsanet.org` | Zendesk's ZAF proxy, server-side. The browser never contacts these hosts directly        | TSANet API user                                    | Case data, by design: partner searches, submissions, accept/reject/request-info, notes, attachments                        |
| The same two `connect2` hosts                 | Zendesk ZIS, server-side                                                                 | Entra client credential                            | Inbound event pulls and ticket-driven case actions                                                                         |
| `login.microsoftonline.com`                   | Zendesk ZIS, server-side                                                                 | Entra client ID + secret                           | A standard OAuth token request. No ticket or case data                                                                     |
| `api.github.com`                              | The admin's browser, directly (the call sets `cors: true`, which bypasses the ZAF proxy) | None; unauthenticated, the only header is `Accept` | No ticket, member, or PII data. What is disclosed is the admin's IP address and the fact that the deploy screen was opened |

The `api.github.com` call asks for the connector's latest GitHub release so the deploy screen can show whether a newer bundle exists, and runs on every open of that screen (an admin surface). The response gates nothing: the redeploy recommendation is driven by byte-comparison of bundle content, and the Deploy button never depends on it. If the call fails, or the unauthenticated GitHub rate limit (60 requests per hour per IP) is exhausted, the screen shows an informational row stating explicitly that this does not affect deploying. Blocking `api.github.com` at your egress costs you the release row and nothing else.

## Authentication and Credential Storage

Each connection authenticates independently. No single credential unlocks everything.

| Connection              | Method                                                                                                                                                                                                             | Where the secret lives                                                                                                                                                                                                    |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ZAF app to Zendesk      | The agent's own Zendesk session, inherited via the ZAF SDK (`client.request()`)                                                                                                                                    | Nothing stored. The app never holds a Zendesk credential.                                                                                                                                                                 |
| ZAF app to TSANet API   | JWT Bearer token, obtained at login. The password is a ZAF **secure setting**: the login request goes through the Zendesk proxy, which substitutes the password server-side, so it never appears in front-end code | Username is a normal app setting; the password is a **secure** setting, not exposed to the browser. The resulting JWT is held in browser memory only (about 50 minutes) and is never written to local or session storage. |
| ZIS to TSANet API       | OAuth 2.0 client credentials (Microsoft Entra). ZIS mints and renews its own short-lived tokens                                                                                                                    | The long-lived Entra client credential is stored **server-side** in the Zendesk ZIS connection store. There is no static token at rest.                                                                                   |
| TSANet to ZIS (inbound) | HTTP Basic auth on every delivery, enforced by ZIS. TSANet additionally signs each delivery with an HMAC-SHA256 signature (`X-Hub-Signature-256`)                                                                  | Per-subscription ingest credentials, held by TSANet for delivery and by ZIS for verification.                                                                                                                             |
| ZIS to Zendesk          | OAuth 2.0 client credentials against your **own** instance, scope-limited to `read tickets:write`. ZIS mints and renews its own short-lived tokens (about 30 minutes)                                              | The OAuth client credential is stored **server-side** in the Zendesk ZIS connection store. There is no static token and no API token at rest.                                                                             |

{% hint style="info" %}
**Credential handling, stated plainly.** The TSANet API password is stored as a Zendesk **secure setting** (`secure: true`). Secure settings are not exposed to the app's front-end code; when the app logs in to TSANet, the request is made through the Zendesk proxy, which substitutes the password server-side, so the cleartext password is never present in the agent's browser. Additional factors: the credential belongs to a **dedicated TSANet service account** (not a person), is scoped to your TSANet membership, travels only over TLS, and the resulting token is held in memory only. This was shipped in ZAF app **v1.0.42** and validated end to end on Beta.
{% endhint %}

## Transport and Network Security

* **TLS everywhere.** Every connection (browser to TSANet, ZIS to TSANet, TSANet to ZIS, and all Zendesk API traffic) is HTTPS.
* **Egress allow-list.** The ZAF app declares a `domainWhitelist` of exactly two hosts (`connect2.tsanet.net` and `connect2.tsanet.org`), and Zendesk blocks proxied app requests to any other external domain. Two deliberate exceptions sit outside the proxy and carry no case data: the ZAF SDK script load from `static.zdassets.com` (version-pinned, subresource integrity) and the deploy screen's unauthenticated release check to `api.github.com` (admin surface only). Both are enumerated in the destination table under Data Flows.
* **Cross-origin sandbox.** The app runs in a sandboxed, cross-origin iframe. Browser primitives such as `prompt()` and `confirm()` are blocked, and the app cannot reach the parent Zendesk page outside the ZAF SDK's defined channel.

## Data at Rest

* **No external database.** The integration is stateless. All state lives in Zendesk ticket fields and the TSANet API. The app holds no datastore of its own.
* **Tokens in memory only.** The ZAF app's TSANet JWT exists only in browser memory for the life of the session and is re-minted on expiry. Nothing is written to local or session storage.
* **Secrets held server-side.** ZIS connection secrets (the Entra client credential and the Zendesk OAuth client credential) are stored inside Zendesk's hosted secret store and are not retrievable in plaintext through the API.
* **No API token, anywhere.** The integration neither stores nor uses a Zendesk API token — setup commands authenticate with short-lived OAuth setup tokens minted from the integration's confidential OAuth client, and the runtime connection mints its own. (Zendesk is retiring API tokens for the Ticketing API — all tokens stop working on April 30, 2027 — so this is a compliance point as well as a hardening one.) The one-time flow-bundle upload is the only call that rejects OAuth on Zendesk's side; it runs inside the TSANet Connect app on the installing admin's own session, so it needs no API token, no password access, and no long-lived credential of any kind.

## Data Retention and the PII Lifecycle

Cross-org case content (partner company names, submitter and engineer contact details, problem narratives) mirrors into ticket subjects, descriptions, comments, and custom fields, and persists until the member removes it. Three facts govern the lifecycle:

* **Three-copy model.** Every collaboration exists as your ticket, the TSANet platform case, and the partner's CRM case, each under a different controller. Anything you delete or scrub applies to your copy only; the Connect API's notes are append-only, so no erasure propagates cross-org. Cross-org erasure is controller-to-controller coordination through TSANet.
* **Selectors are built in.** Every ticket the integration touches is tagged `tsanet_inbound` or `tsanet_outbound`; retention tooling keys on those tags. From the v1.0.64 bundle onward, `tsanet_updated` additionally marks tickets whose case status changed. Earlier bundles never applied that tag, so a sweep keyed on it will not match tickets whose last status change predates your v1.0.64 deploy — scope those on `tsanet_inbound` / `tsanet_outbound`, which have applied since installation.
* **Two supported removal modes.** Whole-ticket deletion (native deletion schedules, which act on tickets closed 120+ days; tag-scoped schedules require the ADPP add-on, otherwise use a scheduled API sweep) or selective scrubbing that preserves the case record (the redaction API permanently removes comment and attachment content; note that cleared field values remain in ticket audit history, which only deletion removes).

The retention window is the member's policy decision as data controller. The full data map and both recipes: [PII Retention and Data Handling](https://github.com/tsanetgit/Zendesk_App/blob/main/PII_Retention_and_Data_Handling.md).

## How the App Package Reaches You

The connector ships as a ZIP that you upload into your own Zendesk instance, so the integrity of that file is a shared concern: TSANet controls how it is built and published, and you control what you install.

On the publishing side, the release pipeline builds only from a tagged commit with no separate build step, so the package cannot diverge from the source it claims to come from. It runs with a least-privilege token, pins its build actions to exact commit revisions, and publishes only after an explicit human approval. Each release carries a **build-provenance attestation** and a **SHA-256 checksum**.

On your side, those two artifacts let you confirm that the ZIP you downloaded is the one TSANet published, rather than a file altered in transit or substituted somewhere along the way:

{% code overflow="wrap" %}

```bash
# Provenance (requires the GitHub CLI)
gh attestation verify tsanet-connect-v<version>.zip --repo tsanetgit/Zendesk_App

# Or checksum only — run where the ZIP and checksums.txt both sit
shasum -a 256 -c checksums.txt
```

{% endcode %}

{% hint style="danger" %}
If verification fails, do not install the package, and contact <membership@tsanet.org>. A failure means the file does not match what TSANet published. This is worth reporting even if a later re-download verifies cleanly.
{% endhint %}

Step-by-step instructions sit in the [Installation Guide](/connect/documentation/zendesk-app/installation-guide.md); the commands are repeated here because verification is a security control rather than an installation detail.

## Access Control and Least Privilege

* **Dedicated service account.** The TSANet API credential is a service account belonging to the integration, not an individual, on your registered domain.
* **Scoped ZIS management.** ZIS connections and flows can only be managed with a ZIS OAuth token of the correct scope. A standard Zendesk API token cannot read or alter them.
* **Scope-limited Zendesk access.** The `zendesk` connection's OAuth tokens carry only `read tickets:write` — the integration can read data and write tickets, but cannot modify users, settings, or anything else in the account.
* **Fail-closed forwarding.** Comment forwarding to the partner only fires for agent or admin public replies. End-user replies and internal notes are never forwarded.
* **Shared author stays inside your org (v1.0.64+).** The optional shared author user is a record in your own directory with an email on **your own** domain — never the partner's — so Zendesk notifications for tickets it touches cannot route outside your org. A wrong or deleted user id cannot break ticket creation: Zendesk silently falls back to the connection user, and the deploy pre-flight resolves the id and shows the name so a typo is caught before it ships.
* **Asymmetric close.** Only the submitting company can close a collaboration, so a receiving member cannot unilaterally end a case it did not open.
* **App visibility is installation-scoped.** Zendesk's role and group restrictions decide who sees the app at all, and they apply to every surface at once: excluding an agent removes the ticket-sidebar panel and the background poller along with the left-nav deploy entry. For staff who work TSANet tickets the left-nav entry cannot be hidden separately; it is admin-gated in function, so for agents it is a read-only status page. The in-app **Allowed action roles** setting separately gates who can invoke TSANet actions inside a visible panel.

## Attack Surface Summary

| Surface                        | Control                                                                                                                                                                                                                                                                                                             |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| App distribution and tampering | Private app, ZIP install only, no public Marketplace listing. The release pipeline runs least-privilege, pins its actions to commit SHAs, and publishes only after a required human approval; every package ships with a build-provenance attestation and a SHA-256 checksum that members verify before installing. |
| Outbound exfiltration          | `domainWhitelist` limits proxied app egress to the two TSANet hosts. The two non-proxy destinations (the SDK load and the deploy screen's release check) carry no case data; every destination is enumerated under Data Flows.                                                                                      |
| Inbound spoofing               | Basic auth (`callbackAuth`) on every delivery, enforced by ZIS; two independent rotatable secrets (capability-token ingest URL + Basic credential); event content is pulled from the TSANet API by token, so forged deliveries cannot inject case content.                                                          |
| Unauthorized forwarding        | Fail-closed agent/admin author guard.                                                                                                                                                                                                                                                                               |
| Credential theft in transit    | TLS on every hop; token held in memory only.                                                                                                                                                                                                                                                                        |
| Stale or leaked tokens         | Short-lived tokens; ZIS re-mints its own; the ZAF JWT expires in about 50 minutes.                                                                                                                                                                                                                                  |

## Operational Security Guidance for Members

* **Verify each package before installing it.** Check the provenance attestation or the checksum on every install and upgrade, not only the first one. See *How the App Package Reaches You* above.
* Use a **dedicated TSANet API service account** on your registered domain, never a personal login or a customer address.
* **Rotate the TSANet API password** on a schedule. When you do, update the ZAF app settings.
* **Restrict Zendesk administrator access.** Only administrators can view or change the app settings, so administrator accounts are the sensitive surface.
* Keep the app on `BETA` only during setup and testing, and switch to `PRODUCTION` when you go live.
* **Leave password access disabled.** Nothing in this integration needs it, including the flow-bundle upload, which runs on the installing admin's session inside the app. If a procedure ever asks you to enable password access for TSANet Connect, it is out of date.

## Known Considerations and Hardening

This section is candid about the current tradeoffs and the work planned to close them.

### TSANet credential storage (resolved in v1.0.42)

**Resolved.** The TSANet API password is stored as a Zendesk **secure setting** (`secure: true`), so it is not exposed to the app's front-end code. At login the app calls TSANet through the Zendesk proxy (`client.request()` with `secure: true`), and the proxy substitutes the password server-side, so the cleartext never reaches the agent's browser. The token-based calls that follow carry only the short-lived JWT, not a secret.

**History.** Earlier versions stored the password as a non-secure setting and called the TSANet API directly from the browser via `fetch()`, which made the password readable in the app's browser context. This was hardened in ZAF app **v1.0.42** (tracked as issue #49 in `tsanetgit/Zendesk_App`) and validated end to end on Beta.

### Renaming the ZIS integration does not retire the old one (operational, advisory)

ZIS integration names are globally unique across every Zendesk account, so a member whose account cannot claim `tsanet_connect` registers their own name and sets it in the app's **TSANet integration name** setting (app **v1.0.61 or later**). That is a supported path, and on a fresh install it is uneventful.

**On an instance that has already deployed, a rename is not a move.** The old integration is not retired by registering a new one. Its job specs stay installed and keep intercepting the same events, so both integrations act on every inbound collaboration and **tickets are created twice**. Each copy is a genuine copy of partner case content, so this is a data-handling consequence and not merely a tidiness problem: the duplicate ticket carries the same cross-org submitter and engineer contact details as the original, and it persists until removed.

The app's pre-flight warns about this and prints the commands to uninstall the old job specs. The warning is deliberately **advisory rather than blocking**, because running two integrations briefly is not always wrong, and because the check reads the *default* registry explicitly. A rename from one custom name to another is therefore not detectable by the app at all.

**If you rename after deploying:**

* Uninstall the old integration's job specs before or immediately after the new deploy, using the commands the pre-flight prints.
* Recreate the `tsanet_oauth` and `zendesk` connections under the new name. **ZIS connections are integration-scoped** and do not move with a rename, so "missing" and "exists under the old name" are different faults with different fixes.
* Re-run the credential-rotation scripts with `--integration <your-name>`. The URLs in the rotation runbook embed the integration name.
* Check for duplicate tickets created in the window when both integrations were live, and remove the copies you do not intend to keep.

### Inbound signature verification (platform-gated; compensating controls in place)

TSANet signs each delivery with an HMAC-SHA256 signature, but the Zendesk Integration Services platform cannot verify it: ZIS inbound webhooks support Basic auth only, and flows receive parsed events with no access to the raw body or headers. Authenticity therefore rests on two independent rotatable secrets (the capability-token ingest URL and the Basic `callbackAuth` credential), with blast radius bounded because the flow pulls case content from the TSANet API by token rather than trusting the delivery body. Members whose posture requires signature verification can front the ingest URL with a small verification proxy; the pattern is documented in <https://github.com/tsanetgit/Zendesk_App/issues/90>, which records the full disposition and evidence.

### Secret rotation (webhook credential rotation shipped)

The ingest (webhook Basic) credential now has a scripted make-before-break rotation with no delivery downtime and true revocation of the old credential: see the [rotation runbook](https://github.com/tsanetgit/Zendesk_App/blob/main/ZIS_Rotation_Runbook.md) and `scripts/rotate-inbound-webhook.py`. Rotating it on a schedule (quarterly is a reasonable default) is the primary control on the inbound pipe. The Entra client credential and the Zendesk OAuth client secret should also be rotated on a defined schedule; scripted procedures for both are tracked in <https://github.com/tsanetgit/Zendesk_App/issues/91>. Note that regenerating a Zendesk OAuth client secret invalidates the ZIS connection built on it — re-register the ZIS OAuth client and recreate the connection as part of the rotation.

### Zendesk API token retirement (migration complete)

Zendesk is removing API tokens as an authentication method for the Ticketing API: creation is blocked for new accounts on July 28, 2026 and for all accounts on October 27, 2026, and all existing tokens stop working on April 30, 2027. The integration is ahead of this: no API token is used anywhere. The `zendesk` ZIS connection uses OAuth client credentials (no stored token), and setup commands authenticate with short-lived OAuth setup tokens minted from the integration's confidential OAuth client. The flow-bundle upload is the one call Zendesk refuses OAuth on, and it is handled without a token at all: the TSANet Connect app performs it on the installing administrator's own session. Because the session is the credential, it lasts exactly as long as the admin is signed in, there is nothing to store, and nothing to rotate or revoke afterwards.

This also means the upload inherits the administrator's own privileges rather than holding standing ones. Zendesk documents the ZIS registry endpoints as admin-only, and the app additionally checks the signed-in user's role before enabling the deploy button — but note that the in-app check is a usability guard, not the security boundary. The boundary is Zendesk's own server-side authorization on the endpoint.

TSANet has tested that boundary rather than only citing it. On 2026-07-28 a session holding the most privileged non-admin role available on the test instance was refused by the server on both endpoints the deploy screen uses: `403 Only admin user is allowed` on the bundle upload, and `403 Forbidden` on the app-settings write that **Apply** performs. Both requests carried a deliberately malformed body, so neither could have changed anything even had authorization passed. Read this as one role, on one instance, on one day: it confirms the vendor's documentation rather than replacing it, and roles and instances vary.

The same screen's **Detect field IDs** and **Apply** work the same way. Detect reads your ticket fields, and Apply writes the matched IDs back into the app's own settings; both run on the signed-in administrator's session, hold no standing credential, and are governed by the same server-side authorization. Two properties are worth knowing. Apply never writes a value you have not been shown: Detect displays the full field-to-ID mapping first, and it refuses to resolve a field at all when two fields share a title or the matched field is the wrong type, rather than guessing. And Apply writes only the field-ID settings — your TSANet credentials are untouched by it, and the password remains a secure setting that the app cannot read back.

## Questions

For a deeper security review, environment access, or to request the current hardening status, contact <membership@tsanet.org>.
