Keelcroft / GitLab Issues Sync for Jira / Security
Security
Effective 30 October 2026 upon publication. Controls and their operational limits are described below.
Architecture
The app runs on Atlassian Forge, including event/queue processing and a five-minute scheduled recovery trigger. Jira data travels directly between Forge and the configured GitLab destination; Keelcroft operates no relay or vendor-hosted backend. Ordinary persistent records use Forge storage; the GitLab access token and root webhook secret use Forge encrypted secret storage.
GitLab egress means the app is not eligible for the Runs on Atlassian badge. No certification, regulatory-compliance status, legal-role classification or data-processing agreement is established by these technical controls.
Access controls
- Configuration, credential storage, enable/pause, link/unlink, replay, health/repair, preview and import controls enforce server-side project administration, with license checks on protected controls. Administrative authorization uses Jira's
ADMINISTER_PROJECTS; displaying a settings page is not itself a permission check. - The Jira project is always taken from the signed resolver context, never from the client payload.
- Each connection is scoped to one Jira project ID and one GitLab project path. Linking reads the Jira target and refuses a different project.
- Changing the GitLab destination is blocked while enabled or while pairs remain linked. Optional workflow changes, including status direction, require paused sync, no saved webhook and zero linked pairs; old baselines are not automatically migrated.
- Jira import selection runs as the administrator, but queued work and scheduled recovery use existing app permissions. They do not freshly impersonate or re-authorize that administrator on every step. Preview and filters are not proof of destination audience equivalence.
Credentials and webhooks
- The supplied GitLab token needs the
apiscope and at least Maintainer project permissions for webhook management. Raw tokens are not returned to the interface or written to application logs. - Public Forge web triggers require POST, validate the connection, and authenticate before parsing the body. Unknown connections and authentication failures receive a generic
404without reflecting request content. - Where present, Standard Webhooks HMAC-SHA256 signatures cover delivery ID, timestamp and raw body; comparisons are constant time and the accepted clock window is five minutes. An invalid or empty present signature is refused, not downgraded to plain-token authentication.
- When signatures are absent on older instances, the shared token is compared in constant time. This legacy path does not provide the signed timestamp-window protection; no universal anti-replay guarantee is made.
- The webhook token and signing token are derived from one stored root secret with domain separation, separately from the customer's access token. Configuration, enable/pause and repair share a connection lock.
- Webhook setup reconciles existing callbacks on retry. Repair reuses the saved root secret where available and preserves mappings, pairs and the enable boundary. A paused connection must be enabled rather than repaired.
- Self-managed hosts require customer organisation-admin egress approval. URL validation requires HTTPS and rejects credentials, explicit ports, queries/fragments and local/private/link-local/multicast hosts, with validated project paths. A configurable host path is not evidence of completed self-managed live testing.
Transfer safeguards and integrity
- Mapped field writes are followed by destination reads and comparisons. Conflicts are held or handled by the configured policy and recorded. Digests minimize stored source values but are not encryption or anonymization.
- At-least-once events are journalled and serialized per connection; terminal events suppress repeated processing. Ambiguous remote responses still prevent any universal atomic/exactly-once guarantee.
- Confidential GitLab export defaults to blocked, including linked pairs and legacy settings. Explicit boolean opt-in warns that Jira issue security is not set and audiences are not preserved. Blocked import rows are excluded before collecting preview titles.
- The confidential safeguard is not an authorization boundary or write ACL. Concurrent changes may race; direct writes/creation can occur before response/read-back refusal. Existing copies are not erased when export is disabled.
- Restricted/internal Jira comments and GitLab internal/system notes are excluded. New comment/file settings default to Both; missing legacy settings stay Off. Source restrictions do not become destination permissions.
- Formatting resolves supported links and escapes copied prose quick actions/unlinked mentions; it does not preserve every GitLab extension or sanitize every possible content risk. Image links are not embedded Jira media, and link conversion itself does not fetch files.
- Optional workflow labels encode explicit Jira statuses and binary GitLab state, not native custom statuses. Ambiguous/unknown/state-inconsistent labels stop transfer. Unsupported dates/weights or missing existing milestones are refused before writing.
- The app does not mirror issue deletion. A missing source leaves its counterpart alone; the separate setup check can delete only its own disposable test issue, or close it if deletion is refused.
File recovery and bounded copying
Upload intent and a non-expiring fence are stored before uploading. A known GitLab upload reference or Jira attachment ID lets attribution-marker work resume without another upload and suppresses reverse discovery. Unknown outcomes, changed digests and incomplete histories stop for reconciliation; a missing marker or expired claim does not authorize blind reupload. Recovery is capped at 100 stages per issue, with no automatic app TTL.
GitLab recovery references contain an upload secret and encoded filename. They are sensitive file-access references, not anonymous metadata. Jira stages retain attachment IDs instead. Raw file bytes are transient; the full durable inventory is in Privacy. Comments have five-page/100-per-page read bounds and ten copies per direction/pass; files have 10 MiB and three-per-direction/pass bounds. Downloads are buffered before size checks, not malware-scanned or strictly streaming-memory-bounded.
Recovery, preview and pause
Five-minute scheduled sweeps requeue existing pairs, at most ten per invocation and five per connection, with a 20-second new-work budget and persistent cursors/pending bucket. The budget does not bound an outstanding API call. Scheduling is not a five-minute completion SLA, load guarantee or discovery of unlinked issues. Disabled connections are skipped, and consumers recheck pause and the original pair so stale two-ID recovery jobs do not recreate an unlinked/replaced pair.
Read-only pair Preview returns field names and decision/problem codes, not field values, authors, bodies, filenames or URLs. It changes no issue, baseline, stored record or queue and is not a permissions audit or reservation.
Pause saves disabled state before remote cleanup and clears the webhook secret. It retains the access token/settings/links/history and a failed-cleanup hook ID for retry. Unlink removes pair/index entries, not copied content or all recovery/history. Neither control cancels writes already in flight; there is no general disconnect/delete-connection control.
Storage and application logs
Connection maps/filters/workflow/confidentiality settings, link digests, upload recovery, latest import runs and recovery cursors are durable with no automatic app TTL. Import runs retain IDs/keys and up to twenty failure entries with reasons bounded to 300 characters; those reasons can contain rejected values. Event records contain identifiers, outcomes and field names, not source content, and expire 90 days after latest write. This event expiry does not erase other stores.
Decision logs record operational identifiers, outcomes, attempts and reason codes. They exclude raw issue content, access tokens, webhook credentials/signatures and sensitive upload references. Automated logging tests exercise content/credential exclusions. Comment text and raw file bytes remain transient, but destination copies and the durable sensitive metadata disclosed above are separate.
Evidence boundaries
Receiving-side GitLab.com checks cover fields, comments and imports; earlier live checks also verified two-way file bytes, creation, assignee mapping and restricted/internal-note exclusions. Automated tests cover additional workflow, preview, recovery and failure paths. These do not establish self-managed compatibility, separate-user access equivalence, load/throttling behavior, scheduled recovery timing or every ambiguous-outcome scenario. No installed competitor benchmark or blanket reliability superiority is claimed.
Vulnerability reporting
Report suspected vulnerabilities privately to support@keelcroft.com with the subject line “Security”. Include reproducible steps and impact, but do not access data that does not belong to you or send credentials/upload secrets. The initial-response target for critical reports is one business day, not a resolution SLA.
The app does not claim SOC 2, Cloud Fortified, an independent penetration test, or participation in the Marketplace Security Bug Bounty Program.