v0.3.0 AI Drafted

Linear Hardening Guide

DevOps Last updated: 2026-09-25

Issue tracking platform hardening for Linear including SAML SSO and SCIM, login-method restriction, workspace access, team permissions, and integration security

View:

Overview

Linear is a modern issue tracking and project management platform designed for software teams. As a platform managing development workflows and project data, Linear security configurations directly impact operational security and intellectual property protection.

Intended Audience

  • Security engineers managing engineering tools
  • IT administrators configuring Linear
  • Engineering managers managing workspaces
  • GRC professionals assessing development security

How to Use This Guide

  • L1 (Crawl): Essential controls for all organizations
  • L2 (Walk): Enhanced controls for security-sensitive environments
  • L3 (Run): Strictest controls for regulated industries

Scope

This guide covers Linear security including SAML SSO and SCIM provisioning, login-method restriction, workspace membership and invitations, roles and team permissions, integration and API-key security, audit logging, and data residency.

Automation surfaces. Linear’s administrative automation surface is its GraphQL API at https://api.linear.app/graphql; the official TypeScript SDK (@linear/sdk) wraps the same schema. Linear publishes no CLI and no Terraform provider of its own. A community provider (terraform-community-providers/linear) exists, but its linear_workspace_settings resource resets every setting you leave out to the provider’s default, so this guide does not use it. The API packs below are read-only audits. Settings that decide who can sign in are left to the console on purpose, because a wrong value locks people out.


Table of Contents

  1. Authentication & SSO
  2. Access Controls
  3. Integration Security
  4. Monitoring & Compliance
  5. Compliance Quick Reference

1. Authentication & SSO

1.1 Configure SAML Single Sign-On

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 6.3, 12.5
NIST 800-53 IA-2, IA-8

Description

Configure SAML SSO so Linear sign-in runs through your identity provider. Claim your email domains so SAML is required for them, and enable SCIM so the IdP creates and suspends Linear accounts.

Rationale

Why This Matters:

  • Centralizing Linear authentication in your corporate IdP enforces MFA, conditional access, and session policies on every login
  • Without SAML, members sign in with a Google account, an emailed login link or code, or a passkey, all outside your IdP’s policies. Linear has no passwords, so the emailed link is the phishable path
  • Once SAML is enabled, members on SAML-approved (DNS-claimed) domains must sign in through SAML by default
  • SAML’s just-in-time provisioning creates accounts but never removes them. SCIM suspends a Linear account when the IdP deactivates the user, which closes orphaned access to roadmaps and issues
  • Linear workspaces hold product roadmaps, security issues, and internal planning, so a single compromised login can expose sensitive engineering intelligence

Attack Prevented: Credential theft, phishing, account takeover, orphaned-account access

Prerequisites

  • Enterprise plan (SAML and SCIM are Enterprise features)
  • Linear workspace admin or owner access
  • A SAML 2.0 identity provider. Linear documents Okta, Microsoft Entra ID, Google Workspace, and OneLogin, plus a custom SAML 2.0 integration
  • DNS access to publish a TXT record for each domain you claim

ClickOps Implementation

Step 1: Open SAML & SCIM Settings

  1. Navigate to: Settings → Administration → Security
  2. Under Authentication methods, click Configure next to SAML & SCIM

Step 2: Exchange Metadata with Your IdP

  1. Create a SAML 2.0 application in your IdP and enter the details Linear shows you. In Okta, Linear’s ACS URL (it ends in /acs) is the Single sign-on URL
  2. Press Continue, then give Linear your IdP’s metadata, either as a metadata XML URL or as the raw XML. Metadata flows from the IdP to Linear: Okta’s Metadata URL, Entra ID’s Federation Metadata XML, or Google Workspace’s downloaded metadata
  3. In Entra ID, set the NameID to Email Address format. Linear takes each user’s email from the SAML NameID
  4. To change the configuration later, use … → Edit Configuration on the same settings page

Step 3: Claim Your Domains so SAML Is Required

  1. Add each corporate domain under the SAML-approved email domains setting, and publish the DNS TXT record Linear gives you to claim it
  2. Once SAML is enabled, members on SAML-approved domains must sign in through SAML by default. Existing sessions are not logged out, but those users must use SAML at their next sign-in
  3. Allow non-SAML logins only for specific other email domains, such as contractors without IdP accounts, and keep that list short
  4. Use the option to prevent non-admins from creating new Linear workspaces with a claimed-domain email (Linear’s docs call it Disable new workspace creation)
  5. Before you rely on enforcement, complete a SAML sign-in yourself. Owners and admins may be able to sign in with other methods to prevent lockouts, depending on your authentication settings. That is a safety net, not a test

Step 4: Enable SCIM Provisioning

  1. On the same SAML & SCIM page, turn on SCIM and click View configuration to get the SCIM base connector URL and bearer token. Treat the bearer token as a secret
  2. Enter both values in your IdP’s provisioning settings. Linear has tested Okta and OneLogin
  3. SCIM provisions users as Members by default. To assign roles from the IdP, push linear-owners (Enterprise), linear-admins, and linear-guests groups
  4. After SCIM is enabled, manage members and admins in the IdP. A user deactivated there is suspended in Linear

Time to Complete: ~1-2 hours

Code Implementation

Linear’s public GraphQL API has no write interface for SAML or SCIM: OrganizationUpdateInput carries no SAML or SCIM field, so configuration is console-only. The pack below is the read side. It proves both are enabled.

Code Pack: API Script
hth-linear-1.01-audit-saml-scim.sh View source on GitHub ↗
Q_SAML_SCIM='query HthLinearSamlScim { organization { samlEnabled scimEnabled } }'

audit_saml_scim() {
  gql "${Q_SAML_SCIM}"
  local saml scim
  saml="$(dq -r '.organization.samlEnabled')"
  scim="$(dq -r '.organization.scimEnabled')"
  echo "Linear 1.1: SAML SSO and SCIM provisioning"
  echo "  samlEnabled: ${saml}"
  echo "  scimEnabled: ${scim}"

  case "${saml}" in
    true)  ;;
    false) finding "SAML is not enabled. Members sign in with Google, an emailed login link or code, or a passkey, outside your IdP's MFA and session policy (SAML requires the Enterprise plan)." ;;
    *)     undetermined "samlEnabled came back as '${saml}', not a boolean." ;;
  esac
  case "${scim}" in
    true)  ;;
    false) finding "SCIM is not enabled. SAML just-in-time provisioning creates accounts but never suspends them, so offboarding stays a manual step." ;;
    *)     undetermined "scimEnabled came back as '${scim}', not a boolean." ;;
  esac
}

Source: SAML, SCIM


1.2 Restrict Login Methods to SAML or Passkeys

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 6.5
NIST 800-53 IA-2(1)

Description

Use Linear’s Restrict login methods setting to require specific sign-in methods for every member: SAML where you have it, passkeys otherwise. Without it, Google, emailed login link, passkey, and SAML all stay open. Linear has no two-factor authentication setting of its own; the lever is which methods you allow.

Rationale

Why This Matters:

  • Linear has no passwords and no built-in second factor. Members sign in with a Google account, an emailed login link or code, a passkey, or SAML, so a Linear login is only as strong as the weakest method you allow
  • With no restriction, every method stays open to every member, and an attacker needs only the weakest one, for example a phished email login code
  • Requiring SAML moves MFA, device posture, and session policy into your IdP. Requiring passkeys gives an origin-bound credential that a phishing proxy cannot relay
  • Owners and admins can still sign in with any method, so they can’t be locked out of this setting. That makes those few accounts the ones to protect most (see 2.3)
  • Linear issues and projects can reveal unreleased features and security work that attackers actively seek

Attack Prevented: Phishing of emailed login links and codes, account takeover through a weaker sign-in method, adversary-in-the-middle relay of non-phishing-resistant logins

Linear has no two-factor authentication setting. Earlier versions of this control told you to turn on a workspace Require two-factor authentication toggle. Linear’s login-methods documentation lists four sign-in methods (Google, email, passkey, SAML) and no second factor, and the enforcement lever is Restrict login methods (Login methods, checked 2026-09-24).

Prerequisites

  • Business or Enterprise plan (login restrictions); Enterprise plan for SAML
  • Linear workspace admin or owner access

ClickOps Implementation

Step 1: Register Passkeys First

  1. Each member adds a passkey from Settings → Account → Security & Access. Several devices can be registered
  2. Passkeys aren’t supported in the Linear desktop app. Before you require them, confirm your members can sign in through a browser or the mobile app
  3. Start with owners and admins, and with members who can see private teams

Step 2: Restrict Login Methods

  1. Navigate to: Settings → Administration → Security
  2. Use Restrict login methods to allow only SAML (once 1.1 is complete) or passkey
  3. Before saving, confirm that at least one owner or admin has completed a sign-in with the method you are requiring

Step 3: Enforce MFA in the IdP

  1. When SAML is the required method, require MFA in your IdP for the Linear application, and phishing-resistant methods (FIDO2/WebAuthn) for admins
  2. Login-method restrictions do not apply to guests invited directly to the workspace. Provision guests through your IdP when they need the same bar

Code Implementation

The login-method list can be set through the API (organizationUpdate with authSettings). This guide deliberately ships only the read side, because a mistaken restriction locks members out.

Code Pack: API Script
hth-linear-1.02-audit-login-methods.sh View source on GitHub ↗
Q_LOGIN_METHODS='query HthLinearLoginMethods { organization { authSettings allowedAuthServices } }'
APPROVED_AUTH_SERVICES="${LINEAR_APPROVED_AUTH_SERVICES:-saml}"

audit_login_methods() {
  gql "${Q_LOGIN_METHODS}"
  local source allowed outside bypass
  # authSettings.allowedAuthServices is current; the top-level field is its deprecated mirror.
  if dq -e '.organization.authSettings | type == "object" and has("allowedAuthServices")' >/dev/null; then
    allowed="$(dq -c '.organization.authSettings.allowedAuthServices')"
    source="authSettings.allowedAuthServices"
  else
    allowed="$(dq -c '.organization.allowedAuthServices')"
    source="allowedAuthServices (deprecated mirror)"
  fi
  echo "Linear 1.2: login-method restriction"

  if ! printf '%s' "${allowed}" | jq -e 'type == "array" and all(.[]; type == "string")' >/dev/null; then
    undetermined "${source} is not a list of strings: ${allowed}"
    return 0
  fi
  echo "  allowed login methods (${source}): $(printf '%s' "${allowed}" | jq -r 'if length == 0 then "ALL (no restriction)" else join(", ") end')"
  echo "  approved set (LINEAR_APPROVED_AUTH_SERVICES): ${APPROVED_AUTH_SERVICES}"

  if [ "$(printf '%s' "${allowed}" | jq 'length')" -eq 0 ]; then
    finding "no login-method restriction. Every sign-in method (Google, emailed login link or code, passkey, SAML) is open to every member (Business/Enterprise plans can restrict it)."
  else
    # TRAP 2: compare case-insensitively against the operator's approved set.
    outside="$(printf '%s' "${allowed}" | jq -r --arg ok "${APPROVED_AUTH_SERVICES}" '
      ($ok | split(",") | map(ascii_downcase | gsub("^\\s+|\\s+$"; "")) | map(select(. != ""))) as $approved
      | [ .[] | select((ascii_downcase) as $s | ($approved | index($s)) == null) ] | join(", ")')"
    if [ -n "${outside}" ]; then
      finding "login methods outside the approved set are allowed: ${outside}"
    fi
  fi

  # TRAP 3: informational only.
  # `//` would turn a real `false` into the fallback, so test for the key instead.
  bypass="$(dq -r '(.organization.authSettings // {}) | if has("disableAuthServiceBypass") then (.disableAuthServiceBypass | tostring) else "unset" end')"
  echo "  owner/admin any-method bypass disabled: ${bypass} (informational; the bypass is Linear's lockout safeguard)"
}

Source: Login methods, Security & Access, SAML


1.3 Configure Allowed Domains

Profile Level: L2 (Walk)

Framework Control
CIS Controls 5.3
NIST 800-53 AC-2

Description

Decide who can join the workspace without an explicit invitation, and who may send invitations. Keep approved email domains to domains your organization owns, limit invitations to admins, and keep the reusable invite link off.

Rationale

Why This Matters:

  • An approved email domain is an auto-join rule, not a block: anyone with a matching address can join the workspace without an invitation or approval, so every listed domain is a standing grant of access
  • Linear states that the setting does not prevent users from creating new workspaces with that domain’s email. Blocking separate workspaces is the job of the SAML domain claim in 1.1
  • A domain you let lapse or transfer keeps auto-join open to whoever controls it next, which is why Linear tells admins to remove such domains
  • On paid plans only admins can invite by default. Turning on Allow users to send invites, or sharing the persistent invite link, widens who can bring people in

Attack Prevented: Unauthorized workspace joining, access through lapsed or transferred domains, invite-link leakage

This control was reframed in 2026-09. Earlier versions described approved domains as a way to block public email providers and outsiders. Linear’s documentation describes an auto-join convenience that “does not prevent users from creating new workspaces with that domain email”, so the hardening is to keep the list minimal (Invite members, checked 2026-09-24).

ClickOps Implementation

Step 1: Review Approved Email Domains

  1. Navigate to: Settings → Administration → Security
  2. Keep the approved email domains to domains your organization owns and controls. Never list a public email provider’s domain, because every address on it could join
  3. Remove any domain you have cancelled or transferred to another organization
  4. Re-review the list on a regular schedule

Step 2: Restrict Invitations

  1. On the same page, leave Allow users to send invites off so only admins can invite members (paid plans; on the Free plan every member is an admin)
  2. Leave the invite link disabled. If you must use one, share it internally only and click Reset invite link whenever it may have leaked. Invite links are unavailable in SAML- and SCIM-enabled workspaces

Step 3: Don’t Confuse the Two Domain Settings

  1. The approved email domains here only streamline joining
  2. The DNS-claimed SAML-approved domains in 1.1 are what make SAML sign-in mandatory for a domain

Code Implementation

Linear’s public GraphQL schema has mutations for domains but no query that lists a workspace’s approved email domains, so reviewing that list (Step 1) is console-only. The pack audits who may send invitations.

Code Pack: API Script
hth-linear-1.03-audit-invitations.sh View source on GitHub ↗
Q_INVITATIONS='query HthLinearInvitations { organization { securitySettings allowMembersToInvite } }'

audit_invitations() {
  gql "${Q_INVITATIONS}"
  local role legacy
  role="$(dq -r '(.organization.securitySettings // {}).invitationsRole // ""')"
  legacy="$(dq -r '.organization.allowMembersToInvite | if . == null then "" else tostring end')"
  echo "Linear 1.3: who may invite people into the workspace"

  if [ -n "${role}" ]; then
    echo "  securitySettings.invitationsRole: ${role}"
    case "${role}" in
      owner|admin) ;;
      user|guest)  finding "invitationsRole is '${role}': members below admin can invite people into the workspace (\"Allow users to send invites\" is on)." ;;
      *)           undetermined "invitationsRole has an unrecognised value '${role}'." ;;
    esac
  elif [ -n "${legacy}" ]; then
    echo "  allowMembersToInvite (deprecated mirror): ${legacy}"
    case "${legacy}" in
      false) ;;
      true)  finding "\"Allow users to send invites\" is on: any member can invite people into the workspace." ;;
      *)     undetermined "allowMembersToInvite has an unrecognised value '${legacy}'." ;;
    esac
  else
    # TRAP 2
    undetermined "neither securitySettings.invitationsRole nor allowMembersToInvite is set, so the effective invitation policy could not be read."
  fi

  # TRAP 1
  echo "  approved email domains: NOT CHECKED. No public query lists them; review Settings > Administration > Security."
}

Source: Invite members, SAML


2. Access Controls

2.1 Configure Team Permissions

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 5.4
NIST 800-53 AC-6

Description

Implement least privilege with Linear’s roles and teams. Give each person the lowest workspace role that fits, restrict team creation to admins, and use team owners to delegate team administration.

Rationale

Why This Matters:

  • Scoping members to only the teams they need limits how much of the workspace any single compromised account can reach
  • Linear’s roles (Workspace owner, Admin, Team owner, Member, Guest) separate what ordinary users can change from what administrators can, and Guests see only the teams they are added to
  • On the Free plan every user is an Admin, so least privilege in Linear is only possible on a paid plan
  • Function-based teams contain the blast radius of a phished or insider account to a narrow slice of issues
  • Regular access reviews catch privilege creep before it becomes a standing risk

Attack Prevented: Privilege escalation, lateral movement, insider data access, over-permissioned accounts

Prerequisites

  • Linear workspace admin access
  • Business or Enterprise plan for the Team owner and Guest roles; Enterprise plan for the Workspace owner role

ClickOps Implementation

Step 1: Structure Teams

  1. Create teams by function: navigate to Settings, scroll to Your teams, and click + (Join or create a team). Admins can also manage teams from Settings → Administration → Teams
  2. Restrict team creation to admins: Settings → Administration → Security → Restrict team creation
  3. Set each team’s visibility (see 2.2)

Step 2: Assign Minimum Necessary Roles

  1. Navigate to: Settings → Administration → Members
  2. Review the workspace roles:
    • Workspace owner (Enterprise only): full control, including billing, security, audit logs, workspace exports, and OAuth application approvals
    • Admin: routine workspace administration; on Enterprise plans, admins have more limited permissions than owners
    • Team owner (Business and Enterprise): delegated control of an individual team
    • Member: collaborates across the teams they can access; no workspace administration
    • Guest (Business and Enterprise): access only to the teams they are explicitly added to
  3. Change a role from the member’s row: overflow menu (⋯) → Change role…
  4. Use Guests for contractors and external collaborators instead of Members
  5. With SCIM enabled, roles come from IdP groups (linear-owners, linear-admins, linear-guests); manage them there
  6. Review membership regularly. Suspend leavers from the row’s overflow menu (⋯) → Suspend user…; suspended users lose all access immediately

Step 3: Delegate Team Administration

  1. Promote team owners from Team settings → Members
  2. In Team settings → Access and permissions, restrict label, template, team-settings, and member management to team owners where appropriate

Code Implementation

Code Pack: API Script
hth-linear-2.01-audit-roles.sh View source on GitHub ↗
Q_USERS='query HthLinearUsers($after: String) {
  users(first: 250, after: $after) { nodes { active admin owner guest app } pageInfo { hasNextPage endCursor } }
}'
Q_TEAM_CREATION='query HthLinearTeamCreation { organization { securitySettings restrictTeamCreationToAdmins } }'

audit_roles() {
  paginate "${Q_USERS}" "users"
  local counts humans owners admins members guests apps role legacy
  # TRAP 2: apps are counted apart from humans.
  counts="$(printf '%s' "${PAGE_NODES}" | jq -c '
    [ .[] | select(.active == true) ] as $a
    | ($a | map(select(.app != true))) as $h
    | { humans:  ($h | length),
        owners:  ($h | map(select(.owner == true)) | length),
        admins:  ($h | map(select(.owner != true and .admin == true)) | length),
        guests:  ($h | map(select(.owner != true and .admin != true and .guest == true)) | length),
        members: ($h | map(select(.owner != true and .admin != true and .guest != true)) | length),
        apps:    ($a | map(select(.app == true)) | length) }')"
  humans="$(printf '%s' "${counts}" | jq '.humans')"
  owners="$(printf '%s' "${counts}" | jq '.owners')"
  admins="$(printf '%s' "${counts}" | jq '.admins')"
  members="$(printf '%s' "${counts}" | jq '.members')"
  guests="$(printf '%s' "${counts}" | jq '.guests')"
  apps="$(printf '%s' "${counts}" | jq '.apps')"
  echo "Linear 2.1: role distribution and team creation"
  echo "  active humans: ${humans} (owners ${owners}, admins ${admins}, members ${members}, guests ${guests}); app users: ${apps}"

  if [ "${humans}" -eq 0 ]; then
    undetermined "the users query returned no active human users, so the role split could not be assessed."
  elif [ "${humans}" -gt 1 ] && [ "$((owners + admins))" -eq "${humans}" ]; then
    # TRAP 1
    finding "every active user holds an owner or admin role, so no one has least privilege (Linear's Free plan makes every user an admin)."
  fi

  gql "${Q_TEAM_CREATION}"
  role="$(dq -r '(.organization.securitySettings // {}).teamCreationRole // ""')"
  legacy="$(dq -r '.organization.restrictTeamCreationToAdmins | if . == null then "" else tostring end')"
  if [ -n "${role}" ]; then
    echo "  securitySettings.teamCreationRole: ${role}"
    case "${role}" in
      owner|admin) ;;
      user|guest)  finding "teamCreationRole is '${role}': members below admin can create teams (\"Restrict team creation\" is off)." ;;
      *)           undetermined "teamCreationRole has an unrecognised value '${role}'." ;;
    esac
  elif [ -n "${legacy}" ]; then
    echo "  restrictTeamCreationToAdmins (deprecated mirror): ${legacy}"
    case "${legacy}" in
      true)  ;;
      false) finding "\"Restrict team creation\" is off: any member can create teams." ;;
      *)     undetermined "restrictTeamCreationToAdmins has an unrecognised value '${legacy}'." ;;
    esac
  else
    undetermined "neither securitySettings.teamCreationRole nor restrictTeamCreationToAdmins is set, so the team-creation policy could not be read."
  fi
}

Source: Members and roles, Teams


2.2 Configure Project Visibility

Profile Level: L2 (Walk)

Framework Control
CIS Controls 5.4
NIST 800-53 AC-6

Description

Control who can see sensitive work by making the teams that own it private. In Linear, issue and project visibility follows team membership: people who are not members of a private team cannot see its issues.

Rationale

Why This Matters:

  • By default every workspace member can view and join any team that is not private, and search its issues, projects, and documents
  • Private teams hide their issues from non-members, and projects created under a private team are visible only to its members
  • Controlling cross-team access enforces need-to-know on the most sensitive work
  • A compromised low-privilege account can only see what visibility settings expose to it
  • Private-team data can still leave through the API, webhooks, and integrations: a personal API key of a private-team member can read that team’s issues, and team owners and admins can set up webhooks for private teams

Attack Prevented: Unauthorized data access, information disclosure, insider snooping

Prerequisites

  • Business or Enterprise plan (private teams); Enterprise plan for sharing individual issues out of a private team
  • Workspace owner, admin, or team owner access to change an existing team’s visibility

ClickOps Implementation

Step 1: Make Sensitive Teams Private

  1. For a new team, turn on Make team private when you create it from workspace settings
  2. For an existing team, open its team settings (right-click the team in the sidebar) → Access and permissions → Change team visibility
  3. Converting a team to private removes non-members from active issue assignments and unsubscribes non-member subscribers
  4. Admins can review every private team at Settings → Administration → Teams

Step 2: Restrict Who Can Join Public Teams

  1. In Team settings → Access and permissions, restrict team access to members that team owners add or invite

Step 3: Control Sub-Teams and Issue Sharing

  1. Under a private parent team, choose Private for any sub-team that must be hidden from the parent team’s members. Restricted, the default, lets members of the parent team see and join it
  2. On Enterprise, decide in Team settings → Access and permissions → Issue sharing whether members may share individual issues out of a private team

Step 4: Review Exposure Paths

  1. Review the API keys (3.2), webhooks, and integrations (3.1) held by members of private teams
  2. When exporting, remember that admins can include issues from any private team

Code Implementation

Code Pack: API Script
hth-linear-2.02-audit-team-visibility.sh View source on GitHub ↗
TEAM_FIELDS='nodes { id key visibility scimManaged allMembersCanJoin } pageInfo { hasNextPage endCursor }'
Q_TEAMS="query HthLinearTeams(\$after: String) { teams(first: 250, after: \$after) { ${TEAM_FIELDS} } }"
Q_ADMIN_TEAMS="query HthLinearAdminTeams(\$after: String) { administrableTeams(first: 250, after: \$after) { ${TEAM_FIELDS} } }"
SENSITIVE_TEAM_KEYS="${LINEAR_SENSITIVE_TEAM_KEYS:-}"

audit_team_visibility() {
  local readable administrable teams key vis
  paginate "${Q_TEAMS}" "teams";                     readable="${PAGE_NODES}"
  paginate "${Q_ADMIN_TEAMS}" "administrableTeams";  administrable="${PAGE_NODES}"
  # TRAP 1: union by id, so private teams the key administers but cannot read are included.
  teams="$(jq -nc --argjson a "${readable}" --argjson b "${administrable}" '$a + $b | unique_by(.id)')"

  echo "Linear 2.2: team visibility"
  printf '%s' "${teams}" | jq -r '
    "  teams visible to this key: \(length) (private \(map(select(.visibility == "private")) | length),"
    + " restricted \(map(select(.visibility == "restricted")) | length),"
    + " public \(map(select(.visibility == "public")) | length))",
    "  public teams any member can join: \(map(select(.visibility == "public" and .allMembersCanJoin == true)) | length)",
    (sort_by(.key)[] | "    - \(.key): \(.visibility)\(if .scimManaged then " (SCIM-managed)" else "" end)")'

  if [ -z "${SENSITIVE_TEAM_KEYS}" ]; then
    # TRAP 2
    undetermined "LINEAR_SENSITIVE_TEAM_KEYS is not set. Name the teams that hold sensitive work (e.g. SEC,HR) to get a verdict."
    return 0
  fi
  echo "  declared sensitive teams: ${SENSITIVE_TEAM_KEYS}"
  local IFS=','
  for key in ${SENSITIVE_TEAM_KEYS}; do
    key="$(printf '%s' "${key}" | tr -d '[:space:]')"
    [ -n "${key}" ] || continue
    vis="$(printf '%s' "${teams}" | jq -r --arg k "${key}" 'map(select((.key | ascii_upcase) == ($k | ascii_upcase))) | first | .visibility // ""')"
    case "${vis}" in
      private)    ;;
      public)     finding "team ${key} is public. Every workspace member can view and join it, and search its issues, projects and documents." ;;
      restricted) finding "team ${key} is restricted. Every member of its private parent team can see and join it; make it Private." ;;
      "")         undetermined "team ${key} was not visible to this key. It may be private and not administered by this user, or the key may be wrong. Re-run with an admin's or owner's key." ;;
      *)          undetermined "team ${key} has an unrecognised visibility '${vis}'." ;;
    esac
  done
}

Source: Private teams, Members and roles, Teams


2.3 Limit Admin Access

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 5.4
NIST 800-53 AC-6(1)

Description

Keep the set of workspace owners and admins small and documented, and use Linear’s workspace restrictions to decide which role may perform sensitive workspace-level actions.

Rationale

Why This Matters:

  • Owners and admins control sign-in methods, SAML, approved domains, API and integration settings, and membership, and they can sign in with any login method even when others are restricted. Each one is a high-value target
  • Keeping admins to a small, documented set shrinks the attack surface for workspace takeover
  • Protecting admin accounts with passkeys or IdP-enforced MFA protects the controls every other user depends on
  • Monitoring admin activity surfaces unauthorized configuration changes quickly
  • On the Free plan every user is an Admin, so this control can only be met on a paid plan

Attack Prevented: Workspace takeover, privilege abuse, unauthorized configuration changes

Prerequisites

  • A paid plan (on the Free plan every user is an Admin)
  • Enterprise plan and the Workspace owner role for workspace restrictions

ClickOps Implementation

Step 1: Inventory Admins and Owners

  1. Navigate to: Settings → Administration → Members and filter by role
  2. Any member can list the admins from the command menu (Cmd/Ctrl K) → View workspace admins
  3. Document why each person holds the role

Step 2: Reduce and Protect

  1. Limit admins and owners to 2-3 people
  2. Demote anyone else from the member’s row: overflow menu (⋯) → Change role… With SCIM, change membership of the linear-admins and linear-owners groups in the IdP instead
  3. Protect every owner and admin account with a passkey (1.2) or IdP-enforced MFA (1.1)
  4. Monitor admin activity in the audit log (4.1)

Step 3: Scope Sensitive Actions (Enterprise)

  1. As a workspace owner, navigate to: Settings → Administration → Security → Workspace restrictions
  2. Configure which roles may perform workspace-level actions. Linear’s API exposes these as minimum-role settings for inviting users, creating teams, creating personal API keys, managing API settings, installing integrations, and granting the admin role

Code Implementation

Code Pack: API Script
hth-linear-2.03-audit-admin-count.sh View source on GitHub ↗
Q_USERS='query HthLinearPrivilegedUsers($after: String) {
  users(first: 250, after: $after) { nodes { active admin owner app } pageInfo { hasNextPage endCursor } }
}'
Q_ADMIN_MGMT='query HthLinearAdminManagement { organization { securitySettings } }'
MAX_ADMINS="${LINEAR_MAX_ADMINS:-3}"
PRIVILEGED=0

audit_admin_count() {
  case "${MAX_ADMINS}" in
    ''|*[!0-9]*|0) echo "PRECONDITION: LINEAR_MAX_ADMINS must be a whole number of 1 or more (got '${MAX_ADMINS}')" >&2; exit 2 ;;
  esac
  paginate "${Q_USERS}" "users"
  local humans owners admins mgmt
  humans="$(printf '%s' "${PAGE_NODES}" | jq '[ .[] | select(.active == true and .app != true) ] | length')"
  owners="$(printf '%s' "${PAGE_NODES}" | jq '[ .[] | select(.active == true and .app != true and .owner == true) ] | length')"
  admins="$(printf '%s' "${PAGE_NODES}" | jq '[ .[] | select(.active == true and .app != true and .owner != true and .admin == true) ] | length')"
  PRIVILEGED=$((owners + admins))
  echo "Linear 2.3: privileged accounts"
  echo "  active humans: ${humans}; owners: ${owners}; admins: ${admins}; privileged total: ${PRIVILEGED}; ceiling (LINEAR_MAX_ADMINS): ${MAX_ADMINS}"

  gql "${Q_ADMIN_MGMT}"
  mgmt="$(dq -r '(.organization.securitySettings // {}).adminManagementRole // "unset"')"
  echo "  securitySettings.adminManagementRole: ${mgmt} (informational)"

  if [ "${humans}" -eq 0 ]; then
    undetermined "the users query returned no active human users, so the privileged count could not be assessed."
  elif [ "${PRIVILEGED}" -eq 0 ]; then
    undetermined "no active human holds the owner or admin role, which a working workspace cannot have. Check the key's visibility."
  elif [ "${PRIVILEGED}" -gt "${MAX_ADMINS}" ]; then
    finding "${PRIVILEGED} active humans hold owner or admin (ceiling ${MAX_ADMINS}). Each one can change sign-in, SAML, API and membership settings, and bypasses login-method restrictions."
  fi
}

Source: Members and roles, Login methods


3. Integration Security

3.1 Configure Integration Access

Profile Level: L2 (Walk)

Framework Control
CIS Controls 3.11
NIST 800-53 SC-12

Description

Control which third-party apps can be installed. Review the integrations, OAuth applications, and webhooks already connected, and verify the signature on every webhook your systems receive from Linear.

Rationale

Why This Matters:

  • Each connected integration is granted access to workspace data and expands the supply-chain attack surface
  • Removing unused integrations eliminates dormant OAuth grants that attackers can abuse if the vendor is compromised
  • An app requests its OAuth scopes when it is authorized, and Linear documents no admin setting that narrows them afterward, so the effective control is deciding which apps get installed at all
  • Integrations enabled for the workspace are reachable by guest users, which can expose data from teams the guest was never added to
  • A webhook receiver that does not verify Linear’s signature accepts forged events from anyone who learns its URL

Attack Prevented: Supply-chain compromise, OAuth token abuse, data exfiltration via third parties, forged webhook events

Prerequisites

  • Any paid plan for third-party application approvals
  • Admin permissions to view Settings → Administration → API; on Enterprise, the workspace owner manages app approvals

ClickOps Implementation

Step 1: Require Approval for Third-Party Apps

  1. Navigate to: Settings → Administration → Security
  2. Turn on third-party application approvals. On Enterprise a workspace owner does this; on other paid plans a workspace admin does
  3. Members who try to install an app can then only request approval. Approved and denied apps appear in Applications

Step 2: Review Integrations, OAuth Apps, and Webhooks

  1. Navigate to: Settings → Integrations and remove integrations the workspace no longer uses
  2. Navigate to: Settings → Administration → API and review OAuth applications and webhooks. Delete any webhook without a current owner
  3. For Linear-built integrations (GitHub, GitLab, Figma, Sentry, Intercom, Zapier, Airbyte), make sure guest users have no access to your accounts on those services

Step 3: Verify Webhook Signatures

  1. Copy each webhook’s signing secret from the webhook’s detail page
  2. Every receiver must verify the Linear-Signature header, a hex-encoded HMAC-SHA256 of the raw request body keyed with that secret, and reject any request whose signed webhookTimestamp is more than a minute old
  3. Optionally, also accept only Linear’s published outbound IP addresses (https://linear.app/.well-known/appspecific/app.linear.ips.json)

Code Implementation

The API pack audits the integration surface. The SDK pack is a webhook receiver that accepts only signed, fresh events, using Linear’s official SDK helper.

Code Pack: API Script
hth-linear-3.01-audit-integrations.sh View source on GitHub ↗
Q_INTEGRATIONS='query HthLinearIntegrations($after: String) {
  integrations(first: 250, after: $after) { nodes { service createdAt } pageInfo { hasNextPage endCursor } }
}'
Q_WEBHOOKS='query HthLinearWebhooks($after: String) {
  webhooks(first: 250, after: $after) { nodes { enabled url allPublicTeams resourceTypes label } pageInfo { hasNextPage endCursor } }
}'
Q_INTEGRATION_ROLE='query HthLinearIntegrationRole { organization { securitySettings } }'
APPROVED_INTEGRATIONS="${LINEAR_APPROVED_INTEGRATIONS:-}"

audit_integrations() {
  local integrations webhooks unapproved insecure role
  paginate "${Q_INTEGRATIONS}" "integrations"; integrations="${PAGE_NODES}"
  paginate "${Q_WEBHOOKS}" "webhooks";         webhooks="${PAGE_NODES}"

  echo "Linear 3.1: integrations, webhooks, and who may install integrations"
  printf '%s' "${integrations}" | jq -r '
    "  integrations: \(length)",
    (group_by(.service)[] | "    - \(.[0].service): \(length) connection(s), oldest \(map(.createdAt) | min | .[0:10])")'
  # TRAP 1: scheme://host only.
  printf '%s' "${webhooks}" | jq -r '
    "  webhooks: \(length) (enabled \(map(select(.enabled == true)) | length))",
    (.[] | "    - \((.url // "(no url)") | capture("^(?<o>[A-Za-z][A-Za-z0-9+.-]*://[^/?#]*)").o // "(unparseable url)")"
           + " enabled=\(.enabled) allPublicTeams=\(.allPublicTeams) resources=\((.resourceTypes // []) | join(","))")'

  insecure="$(printf '%s' "${webhooks}" | jq '[ .[] | select(.enabled == true and ((.url // "") | test("^https://"; "i") | not)) ] | length')"
  if [ "${insecure}" -gt 0 ]; then
    finding "${insecure} enabled webhook(s) deliver workspace data to a non-HTTPS URL."
  fi

  if [ -z "${APPROVED_INTEGRATIONS}" ]; then
    # TRAP 3
    undetermined "LINEAR_APPROVED_INTEGRATIONS is not set. Name the integration services you have approved to get a verdict on the list above."
  else
    unapproved="$(printf '%s' "${integrations}" | jq -r --arg ok "${APPROVED_INTEGRATIONS}" '
      ($ok | split(",") | map(ascii_downcase | gsub("^\\s+|\\s+$"; "")) | map(select(. != ""))) as $approved
      | [ .[].service | select((ascii_downcase) as $s | ($approved | index($s)) == null) ] | unique | join(", ")')"
    if [ -n "${unapproved}" ]; then
      finding "integrations outside LINEAR_APPROVED_INTEGRATIONS are connected: ${unapproved}"
    fi
  fi

  gql "${Q_INTEGRATION_ROLE}"
  role="$(dq -r '(.organization.securitySettings // {}).integrationCreationRole // ""')"
  case "${role}" in
    owner|admin) echo "  securitySettings.integrationCreationRole: ${role}" ;;
    user|guest)  echo "  securitySettings.integrationCreationRole: ${role}"
                 finding "integrationCreationRole is '${role}': members below admin can install and connect new integrations." ;;
    "")          undetermined "securitySettings.integrationCreationRole is not set, so who may install integrations could not be read." ;;
    *)           undetermined "integrationCreationRole has an unrecognised value '${role}'." ;;
  esac

  # TRAP 2
  echo "  third-party application approvals: NOT CHECKED. Not readable through the public API; confirm it in Settings > Administration > Security."
}
Code Pack: SDK Script
hth-linear-3.01-verify-webhook-signatures.js View source on GitHub ↗
const secret = process.env.LINEAR_WEBHOOK_SECRET;
if (!secret) {
  // TRAP 2
  console.error("PRECONDITION: set LINEAR_WEBHOOK_SECRET to the webhook's signing secret");
  process.exit(2);
}
const port = Number(process.env.PORT || 8080);

// createHandler() checks the Linear-Signature HMAC over the raw body and the
// signed webhookTimestamp (±60 s), answering 400 on either failure and 405 to
// anything but POST. Only verified events reach the handlers registered below.
const handler = new LinearWebhookClient(secret).createHandler();

handler.on("*", async (payload) => {
  // Replace with your processing. `payload` is already verified.
  console.log(`verified ${payload.type} ${payload.action} webhook=${payload.webhookId}`);
});

http
  .createServer((req, res) => {
    if (req.url !== "/hooks/linear") {
      res.statusCode = 404;
      res.end();
      return;
    }
    // TRAP 1: the raw IncomingMessage goes straight to the SDK, unparsed.
    handler(req, res);
  })
  .listen(port, () => console.log(`listening on :${port}/hooks/linear`));

Source: Third-Party App Approvals, API and Webhooks, Webhooks, SDK webhooks, Members and roles, GitHub integration


3.2 Configure API Tokens

Profile Level: L2 (Walk)

Framework Control
CIS Controls 3.11
NIST 800-53 SC-12

Description

Restrict who may create personal API keys. Then review and prune everything that holds standing credentials to a Linear account: personal API keys, active sessions, and authorized OAuth applications.

Rationale

Why This Matters:

  • Personal API keys act as long-lived credentials that bypass interactive SSO and login-method restrictions
  • Keys leaked in code, logs, or CI configs grant direct programmatic access to workspace data
  • Admins decide whether Members may create API keys at all, and can see and revoke every existing key in the workspace
  • Authorized OAuth applications hold delegated access that persists after the tool stops being used, so an unrevoked grant is a live path into the workspace if the third party is breached
  • Active sessions are recorded with location and date, which makes an unrecognized session the earliest visible sign of account takeover
  • Documenting key purposes makes it possible to spot and revoke credentials that no longer have an owner

Attack Prevented: Credential leakage, token theft, unauthorized API access, OAuth grant abuse, persistent access via stale tokens and sessions

ClickOps Implementation

Step 1: Restrict and Review Workspace API Keys

  1. Navigate to: Settings → Administration → API
  2. Under Member API keys, choose whether Members may create their own API keys. Admins can always create them
  3. Review the workspace’s existing API keys on the same page and revoke any without a documented owner and purpose

Step 2: Review Personal API Keys

  1. Navigate to: Settings → Account → Security & Access
  2. Review every personal API key and document its purpose and owner
  3. Create automation keys with the narrowest permission that works (Read, Write, Admin, Create issues, Create comments), limited to specific teams where possible
  4. Revoke keys with no documented owner or purpose

Step 3: Revoke Stale Sessions

  1. On the same page, review active sessions. Each is listed with its location and date last seen, and clicking an entry shows its IP address and original sign-in date
  2. Revoke any session from an unrecognized location, an old device, or a departed contractor. Inactive sessions expire automatically after 30 days
  3. Treat an unexplained session as a suspected compromise: revoke it, then rotate the account’s API keys

Step 4: Review Authorized OAuth Applications

  1. Review the list of authorized OAuth applications on the same page
  2. Revoke every application the member no longer actively uses
  3. Re-review after any third-party breach disclosure affecting a connected vendor

When a user is suspended or converted to a guest, Linear revokes that user’s API tokens.

Code Implementation

Linear’s public GraphQL schema has no type that lists personal API keys, so reviewing existing keys (Steps 1–2) is console-only. The pack checks who may create keys and inventories the key owner’s own sessions.

Code Pack: API Script
hth-linear-3.02-audit-api-keys-and-sessions.sh View source on GitHub ↗
Q_API_KEY_POLICY='query HthLinearApiKeyPolicy { organization { securitySettings } }'
Q_SESSIONS='query HthLinearSessions { authenticationSessions { createdAt lastActiveAt isCurrentSession type } }'

audit_api_keys_and_sessions() {
  local role
  gql "${Q_API_KEY_POLICY}"
  role="$(dq -r '(.organization.securitySettings // {}).personalApiKeysRole // ""')"
  echo "Linear 3.2: personal API key policy and sessions"
  case "${role}" in
    owner|admin) echo "  securitySettings.personalApiKeysRole: ${role} (members cannot create personal API keys)" ;;
    user|guest)  echo "  securitySettings.personalApiKeysRole: ${role}"
                 finding "personalApiKeysRole is '${role}': members below admin can mint long-lived personal API keys (\"Member API keys\" is on)." ;;
    "")          undetermined "securitySettings.personalApiKeysRole is not set, so who may create personal API keys could not be read." ;;
    *)           undetermined "personalApiKeysRole has an unrecognised value '${role}'." ;;
  esac
  # TRAP 1
  echo "  existing personal API keys: NOT CHECKED. No public query lists them; review Settings > Administration > API."

  # TRAP 2: informational inventory of the key owner's sessions, with no IP or location.
  gql "${Q_SESSIONS}"
  dq -r '
    def age_days: (now - (sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601)) / 86400 | floor;
    .authenticationSessions as $s
    | "  active sessions of the key owner: \($s | length)"
      + " (by client: \($s | group_by(.type) | map("\(.[0].type)=\(length)") | join(", ")))",
      (if ($s | length) > 0 then
         "  oldest session created \($s | map(.createdAt | age_days) | max) day(s) ago;"
         + " least recently active \($s | map((.lastActiveAt // .createdAt) | age_days) | max) day(s) ago"
       else empty end)'
}

Source: API and Webhooks, Security & Access, Invite members


4. Monitoring & Compliance

4.1 Configure Audit Logs

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 8.2
NIST 800-53 AU-2

Description

Review Linear’s workspace audit log and stream it to your SIEM. Linear keeps audit events for only 90 days, so anything you need beyond that window has to be exported while it still exists.

Rationale

Why This Matters:

  • The audit log is the workspace’s record of who did what. Each entry carries the actor’s IP address and country, which is what makes an anomalous login or a session from an unexpected jurisdiction visible
  • Linear retains audit log events for 90 days only. An intrusion discovered on a typical breach-detection timeline can easily predate the oldest available evidence, and exporting to a SIEM is what preserves the trail
  • Monitoring authentication, permission, and integration events surfaces account compromise and insider abuse early
  • Retained audit records support SOC 2, GDPR, and HIPAA evidence requirements, which generally demand retention well beyond 90 days

Attack Prevented: Undetected intrusion, insider abuse, repudiation, delayed incident response, evidence loss through log expiry

Prerequisites

  • Enterprise plan
  • Workspace Owner role; the audit log is restricted to workspace owners

ClickOps Implementation

Step 1: Access the Audit Log

  1. Navigate to: Settings → Administration → Audit Log (workspace owners only)
  2. Review entries (actor, action, IP address, and country). You can filter by event type and hide session-creation events
  3. Confirm the 90-day retention window against your own evidence requirements

Step 2: Stream Logs to Your SIEM

  1. On the Audit Log page, enable Stream logs and configure the webhook endpoint
  2. Point the webhook at your SIEM’s HTTP collector so events are captured continuously rather than exported by hand
  3. Verify the webhook’s signature with its signing secret, as in 3.1, so forged events cannot be injected into your audit trail
  4. Verify events are arriving, then set retention in the SIEM to match your compliance obligation

Step 3: Query Programmatically

  1. The audit log is queryable through Linear’s GraphQL API (auditEntries), filterable by type, actor, IP address, and date range; auditEntryTypes lists every event type
  2. Use it for point-in-time investigation, and to reconcile SIEM coverage gaps before the 90-day window closes

Key Events to Monitor:

  • Authentication events and sessions from unexpected countries
  • Permission and role changes
  • Integration and OAuth application changes
  • Membership additions and removals

Code Implementation

Code Pack: API Script
hth-linear-4.01-audit-log-entries.sh View source on GitHub ↗
Q_AUDIT='query HthLinearAuditEntries($since: DateTimeOrDuration!, $after: String) {
  auditEntries(first: 250, after: $after, filter: { createdAt: { gte: $since } }) {
    nodes { type createdAt } pageInfo { hasNextPage endCursor }
  }
}'
WINDOW_DAYS="${LINEAR_AUDIT_WINDOW_DAYS:-7}"

audit_log_entries() {
  case "${WINDOW_DAYS}" in
    ''|*[!0-9]*|0) echo "PRECONDITION: LINEAR_AUDIT_WINDOW_DAYS must be a whole number from 1 to 90 (got '${WINDOW_DAYS}')" >&2; exit 2 ;;
  esac
  if [ "${WINDOW_DAYS}" -gt 90 ]; then
    echo "PRECONDITION: LINEAR_AUDIT_WINDOW_DAYS exceeds Linear's 90-day audit retention" >&2
    exit 2
  fi
  local since="-P${WINDOW_DAYS}D" after="" more pages=0 vars count=0 page_count
  : > "${TYPES_FILE}"
  while :; do
    vars="$(jq -nc --arg s "${since}" --arg a "${after}" '{since: $s, after: (if $a == "" then null else $a end)}')"
    gql "${Q_AUDIT}" "${vars}"
    dq -r '.auditEntries.nodes[].type' >> "${TYPES_FILE}"
    page_count="$(dq '.auditEntries.nodes | length')"
    count=$((count + page_count))
    more="$(dq -r '.auditEntries.pageInfo.hasNextPage')"
    after="$(dq -r '.auditEntries.pageInfo.endCursor // ""')"
    pages=$((pages + 1))
    case "${more}" in
      false) break ;;
      true)  ;;
      *)     echo "PRECONDITION: auditEntries.pageInfo.hasNextPage came back as '${more}'" >&2; exit 2 ;;
    esac
    if [ -z "${after}" ] || [ "${pages}" -ge 400 ]; then
      echo "PRECONDITION: pagination of auditEntries stopped before the last page" >&2
      exit 2
    fi
  done

  echo "Linear 4.1: audit log, last ${WINDOW_DAYS} day(s)"
  echo "  entries read: ${count}"
  if [ "${count}" -eq 0 ]; then
    # TRAP 1
    undetermined "no audit entries in the last ${WINDOW_DAYS} day(s). An empty read is not evidence the log is recording; widen LINEAR_AUDIT_WINDOW_DAYS or generate a known event and re-run."
  else
    echo "  top event types:"
    sort "${TYPES_FILE}" | uniq -c | sort -rn | awk 'NR <= 10 {printf "    - %s: %s\n", $2, $1}'
  fi
  # TRAP 2
  echo "  stream logs to SIEM: NOT CHECKED. Not readable through the public API; confirm under Settings > Administration > Audit Log."
}

Source: Audit log


4.2 Choose Data Residency at Workspace Creation

Profile Level: L2 (Walk)

Framework Control
CIS Controls 3.1
NIST 800-53 SA-9

Description

Linear lets you choose whether a workspace’s data is hosted in the US or the EU region. The choice is made at workspace creation and is not self-serve to change afterward, so regulated teams must decide before the workspace exists.

Rationale

Why This Matters:

  • Data residency is effectively fixed in Linear. Pick the wrong region and the self-serve remedy is creating a new workspace and migrating the data, which is expensive and disruptive once issues, projects, and integrations have accumulated
  • Teams subject to GDPR, national data-localization rules, or customer contracts with residency clauses need the EU region selected up front to keep issue content, attachments, and comments inside the required jurisdiction
  • Issue trackers accumulate personal data incidentally (customer names in bug reports, screenshots, support transcripts), so residency obligations apply even when the workspace was never intended to hold regulated data
  • Documenting which region a workspace runs in gives auditors and vendor-risk reviewers a verifiable answer instead of an assumption

Attack Prevented: Regulatory exposure from cross-border data transfer, residency-clause breach, unplanned data migration

Some data stays in the US whatever region you choose. Workspace information, all user account information, and user-created API keys (used to authenticate users and route them to the right region), notification emails (held 7 days by Linear’s email provider), usage and analytics data, and crash-related account information are always stored in the United States (Security, checked 2026-09-24). Record these exceptions in your data-processing inventory.

ClickOps Implementation

Step 1: Decide Region Before Creating the Workspace

  1. Confirm your residency obligation (GDPR, customer contract, internal policy) before anyone provisions the workspace
  2. Select the EU region at creation if any obligation requires EU hosting; otherwise select US
  3. Record the chosen region in your vendor inventory, together with the US-only data categories above

Step 2: Verify Existing Workspaces

  1. Navigate to: Settings → Administration → Workspace to see each existing workspace’s region
  2. Where a workspace is in the wrong region, plan a migration to a new, correctly provisioned workspace rather than expecting a setting change
  3. Document the outcome for audit evidence

Automation: ClickOps only. Linear exposes no write interface for this setting: the region is chosen when the workspace is created and “isn’t self-serve to change later” (Security, 2026-09-24).

Validation & Testing

Read the workspace’s region at Settings → Administration → Workspace and check that it matches the region recorded in your data-processing inventory. Since the setting is fixed at creation, validation is a one-time attestation rather than a recurring config check.

Source: Security


5. Compliance Quick Reference

SOC 2 Trust Services Criteria Mapping

Control ID Linear Control Guide Section
CC6.1 SSO and login-method restriction 1.1
CC6.2 Team permissions 2.1
CC7.2 Audit logs 4.1

NIST 800-53 Rev 5 Mapping

Control Linear Control Guide Section
IA-2 SSO 1.1
IA-2(1) Login-method restriction 1.2
AC-6 Team permissions 2.1
AU-2 Audit logs 4.1

Appendix A: References

Official Linear Documentation:

API & Developer Resources:

Compliance Frameworks:

Security Incidents:

  • No major public security breaches identified as of this writing.

Changelog

Date Version Maturity Changes Author
2026-09-25 0.3.0 ai-drafted validate-hth-guide run (Phases 4–5, read-only tenant policy). 0 surfaces exercised live: the Linear console was behind a sign-in wall in the validation browser and no API key could be minted, so maturity is unchanged. Corrections from current Linear docs: rewrote 1.2 around Restrict login methods (Linear has no two-factor setting; renamed from “Enforce Two-Factor Authentication”); fixed console paths throughout (Settings → Administration → …); 1.1 now describes metadata exchange, domain claiming and SCIM; 1.3 reframed approved domains as auto-join rather than a block and added invitation controls; 2.1 lists all five roles and the Free-plan all-admin caveat; 2.2 gains real paths for private teams; 2.3 adds workspace restrictions; 3.1 adds third-party app approvals and webhook signature verification; 3.2 adds the workspace Member API keys setting; 4.2 lists the data that always stays in the US; replaced the stale developers.linear.app links. Added nine read-only GraphQL audit packs and one SDK webhook-verification pack, all run against a schema-validating mock and failing closed against the real API; 4.2 carries a ClickOps-only automation verdict Claude Code (Opus 5.5)
2026-08-08 0.2.0 ai-drafted Currency pass, scope-limited: only three Linear documentation pages (security, security-and-access, audit-log) were publicly reachable this pass, so findings outside them soften rather than assert. Rewrote 4.1 audit log (Enterprise, workspace owners only, 90-day retention, GraphQL queryable, webhook streaming to SIEM, actor IP/country); added 4.2 data residency (US/EU, fixed at workspace creation); added passkeys to 1.2 and OAuth-grant/session review to 3.2; annotated 1.2 and 1.3 as unverifiable against current public docs; removed the 404 SAML SSO link and the Trust Center/security marketing links in favor of linear.app/docs/security Claude Code (Opus 4.8)
2026-06-29 0.1.1 ai-drafted Add cheat-sheet Description and Rationale for all controls Claude Code (Opus 4.8)
2025-02-05 0.1.0 ai-drafted Initial guide with SSO, teams, and integrations Claude Code (Opus 4.5)

Contributing

Found an issue or want to improve this guide?