v0.2.0-draft AI Drafted

LaunchDarkly Hardening Guide

DevOps Last updated: 2026-08-08

Feature flag security for SDK keys, environment access, and approval workflows

View:

Overview

LaunchDarkly manages feature flags controlling application behavior across environments. REST API, SDK keys, and webhook integrations control feature rollouts. Compromised access enables feature manipulation, environment privilege escalation, or extraction of targeting rules revealing business logic.

Intended Audience

  • Security engineers managing feature flag systems
  • DevOps/Platform administrators
  • GRC professionals assessing release management
  • Third-party risk managers evaluating deployment integrations

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 LaunchDarkly security configurations including authentication, access controls, and integration security.


Table of Contents

  1. Authentication & Access Controls
  2. SDK & API Security
  3. Environment Security
  4. Monitoring & Detection

1. Authentication & Access Controls

1.1 Enforce SSO with MFA

Profile Level: L1 (Crawl) NIST 800-53: IA-2(1)

Description

Require SAML single sign-on with multi-factor authentication for all LaunchDarkly account access, and provision and deprovision users automatically through SCIM.

Rationale

Why This Matters:

  • Centralizes LaunchDarkly authentication in your corporate IdP so MFA and conditional-access policies apply to every login
  • Local or password-only logins bypass IdP controls and are prime targets for credential stuffing and phishing
  • SCIM provisioning removes departed users automatically, eliminating orphaned accounts that retain flag-modification rights
  • A single compromised LaunchDarkly login can flip feature flags in production, exposing hidden features or disabling security controls

Attack Prevented: Credential theft, phishing, MFA bypass, orphaned-account access

ClickOps Implementation

Step 1: Configure SAML SSO

  1. Navigate to: gear icon (left sidebar) → Organization settings → Security → SSO Management
  2. Click Configure SAML and enter your IdP’s SSO URL, X.509 certificate, and (if required) the request signing settings
  3. Enable: Require SSO

Set the default initial role to “No access.” LaunchDarkly recommends configuring the default initial role for SSO-provisioned members to the lowest privilege available so that a newly federated account cannot read or change flags before an admin grants scoped access. On Developer and Foundation plans the lowest available default is Reader; Enterprise plans can set No access. (SSO documentation)

One identity provider per account. LaunchDarkly supports a single configured identity provider per account — you cannot federate a second IdP alongside the first, so plan IdP migrations as a cutover with an admin fallback path. (SSO documentation)

Step 2: Configure SCIM

  1. Enable SCIM provisioning (Enterprise — see Appendix A)
  2. Configure user/group sync
  3. Set deprovisioning behavior

Code Pack: API Script
hth-launchdarkly-1.01-enforce-sso-with-mfa.sh View source on GitHub ↗
# Audit all members for MFA enrollment
MEMBERS=$(ld_get "/members?limit=100") || {
  fail "1.1 Unable to retrieve member list"
  increment_failed; summary; exit 0
}

TOTAL=$(echo "${MEMBERS}" | jq '.totalCount')
NO_MFA=$(echo "${MEMBERS}" | jq '[.items[] | select(.mfa == false)] | length')
info "1.1 Total members: ${TOTAL}, without MFA: ${NO_MFA}"

if [ "${NO_MFA}" -gt 0 ]; then
  warn "1.1 Members without MFA:"
  echo "${MEMBERS}" | jq -r '.items[] | select(.mfa == false) | "  - \(.email) (role: \(.role))"'
  fail "1.1 ${NO_MFA} member(s) do not have MFA enabled"
  increment_failed
else
  pass "1.1 All members have MFA enabled"
  increment_applied
fi

1.2 Role-Based Access Control

Profile Level: L1 (Crawl) NIST 800-53: AC-3, AC-6

Description

Define custom roles and project- and environment-scoped permissions so each member has only the LaunchDarkly access their job requires.

Rationale

Why This Matters:

  • Least-privilege roles limit who can create, modify, or toggle flags in sensitive environments like production
  • Default broad access lets any member change targeting rules that govern application behavior and customer exposure
  • Scoping roles to specific projects and environments contains the blast radius of a single compromised account
  • Reader-only roles for auditors and observers prevent accidental or malicious flag changes

Attack Prevented: Privilege escalation, unauthorized flag changes, lateral movement across projects, insider misuse

ClickOps Implementation

Step 1: Define Custom Roles

Role Permissions
Admin Full access
Writer Create/modify flags
Reader View only
No access Blocked

Step 2: Configure Project/Environment Access

  1. Navigate to: gear icon (left sidebar) → Organization settings → Roles, then click New role
  2. Create environment-specific roles
  3. Apply least privilege

Custom roles are an Enterprise capability — see Appendix A. (Creating custom roles)


Code Pack: Terraform
hth-launchdarkly-1.02-role-based-access-control.tf View source on GitHub ↗
# Production read-only role — least-privilege RBAC
resource "launchdarkly_custom_role" "prod_readonly" {
  key              = "hth-prod-readonly"
  name             = "HTH Production Read-Only"
  description      = "View production flags without modification rights"
  base_permissions = "no_access"

  policy_statements {
    effect    = "allow"
    actions   = ["viewProject"]
    resources = ["proj/*"]
  }

  policy_statements {
    effect    = "deny"
    actions   = ["updateOn", "updateOff", "updateRules", "updateTargets", "updateFallthrough"]
    resources = ["proj/*:env/production:flag/*"]
  }
}

# Staging deployer role — write flags in staging only
resource "launchdarkly_custom_role" "staging_deployer" {
  key              = "hth-staging-deployer"
  name             = "HTH Staging Deployer"
  description      = "Manage flags in staging, read-only in production"
  base_permissions = "no_access"

  policy_statements {
    effect    = "allow"
    actions   = ["viewProject"]
    resources = ["proj/*"]
  }

  policy_statements {
    effect    = "allow"
    actions   = ["*"]
    resources = ["proj/*:env/staging:flag/*"]
  }

  policy_statements {
    effect    = "deny"
    actions   = ["*"]
    resources = ["proj/*:env/production:flag/*"]
  }
}

# Scoped service token for CI/CD
resource "launchdarkly_access_token" "cicd" {
  name          = "HTH CI/CD Pipeline"
  service_token = true

  inline_roles {
    effect    = "allow"
    actions   = ["updateOn", "updateOff"]
    resources = ["proj/${var.project_key}:env/staging:flag/*"]
  }

  inline_roles {
    effect    = "deny"
    actions   = ["*"]
    resources = ["proj/${var.project_key}:env/production:flag/*"]
  }
}
Code Pack: API Script
hth-launchdarkly-1.02-role-based-access-control.sh View source on GitHub ↗
# Create a production read-only custom role
EXISTING=$(ld_get "/roles" | jq -r '.items[].key') || true

if echo "${EXISTING}" | grep -q "^hth-prod-readonly$"; then
  info "1.2 Role 'hth-prod-readonly' already exists"
else
  info "1.2 Creating 'hth-prod-readonly' custom role..."
  RESPONSE=$(ld_post "/roles" '{
    "key": "hth-prod-readonly",
    "name": "HTH Production Read-Only",
    "description": "Read-only access to production environment",
    "basePermissions": "no_access",
    "policy": [
      {
        "effect": "allow",
        "actions": ["viewProject"],
        "resources": ["proj/*"]
      },
      {
        "effect": "deny",
        "actions": ["updateOn", "updateOff", "updateRules", "updateTargets", "updateFallthrough"],
        "resources": ["proj/*:env/production:flag/*"]
      }
    ]
  }') || {
    fail "1.2 Failed to create custom role"
    increment_failed; summary; exit 0
  }
  pass "1.2 Custom role 'hth-prod-readonly' created"
fi
Code Pack: Sigma Detection Rule
hth-launchdarkly-1.02-role-based-access-control.yml View source on GitHub ↗
detection:
    selection:
        kind: 'member'
        accesses.action: 'updateMemberRole'
    filter_admin:
        title|contains: 'admin'
    condition: selection and filter_admin
fields:
    - date
    - title
    - member.email
    - subject.name
    - accesses.resource

2. SDK & API Security

2.1 Secure SDK Keys

Profile Level: L1 (Crawl) NIST 800-53: IA-5

Description

Protect LaunchDarkly SDK keys.

Rationale

Why This Matters:

  • Server-side SDK keys grant read access to all flags and targeting rules in an environment and must never ship in client code
  • Mobile keys and client-side IDs are exposure-safe, but using the wrong key type leaks server-only data to browsers and mobile apps
  • Periodic key rotation limits how long a leaked key remains usable by an attacker
  • Targeting rules often encode business logic, customer segments, and rollout plans that competitors or attackers can exploit

Attack Prevented: SDK key leakage, flag enumeration, targeting-rule extraction, business-logic disclosure

Attack Scenario: Exposed SDK key enables flag enumeration; mobile SDK key in client bundle allows targeting rule extraction.

Implementation

SDK Key Types:

Key Type Exposure Risk Use Case
SDK Key Server-side only Backend services
Mobile Key Client-side safe Mobile apps
Client-side ID Client-side safe Browser apps

Step 1: Rotate Keys

  1. Navigate to: Project settings → Environments
  2. Reset SDK keys periodically
  3. Update applications

Code Pack: API Script
hth-launchdarkly-2.01-secure-sdk-keys.sh View source on GitHub ↗
# Check secure mode on all environments
ENVIRONMENTS=$(ld_get "/projects/${LD_PROJECT_KEY}/environments") || {
  fail "2.1 Unable to retrieve environments"
  increment_failed; summary; exit 0
}

ENVS_WITHOUT_SECURE=$(echo "${ENVIRONMENTS}" | jq '[.items[] | select(.secureMode == false)] | length')
TOTAL_ENVS=$(echo "${ENVIRONMENTS}" | jq '.items | length')
info "2.1 Total environments: ${TOTAL_ENVS}"

if [ "${ENVS_WITHOUT_SECURE}" -gt 0 ]; then
  warn "2.1 Environments without secure mode:"
  echo "${ENVIRONMENTS}" | jq -r '.items[] | select(.secureMode == false) | "  - \(.key)"'
fi

# Enable secure mode on production environment
PROD_SECURE=$(echo "${ENVIRONMENTS}" | jq -r '.items[] | select(.key == "production") | .secureMode')
if [ "${PROD_SECURE}" = "true" ]; then
  pass "2.1 Production environment has secure mode enabled"
  increment_applied
else
  info "2.1 Enabling secure mode on production..."
  ld_semantic_patch "/projects/${LD_PROJECT_KEY}/environments/production" '{
    "comment": "HTH: Enable secure mode to prevent client-side SDK user impersonation",
    "instructions": [
      {"kind": "updateSecureMode", "value": true}
    ]
  }' || {
    fail "2.1 Failed to enable secure mode on production"
    increment_failed; summary; exit 0
  }
  pass "2.1 Secure mode enabled on production"
  increment_applied
fi

2.2 API Token Security

Profile Level: L1 (Crawl) NIST 800-53: IA-5

Description

Inventory LaunchDarkly access tokens, remove unused ones, and issue new tokens scoped with the least-privilege role or inline policy the caller actually needs — choosing personal tokens for human-owned scripts and service tokens for automation that must survive offboarding.

Rationale

Why This Matters:

  • API tokens authenticate automated access to the full LaunchDarkly REST API and can modify flags without a human login or MFA prompt
  • Long-lived or unscoped tokens that leak give attackers persistent control over feature configuration
  • Scoping tokens to least-privilege base roles, custom roles, or inline policies limits what a stolen token can do
  • Service tokens are deliberately member-independent, so they keep working after the person who created them leaves — that durability is why they need a standing inventory and an owner of record

Attack Prevented: Token theft, persistent API access, unauthorized flag manipulation, MFA bypass via automation

Personal tokens and service tokens are not interchangeable. Personal tokens are bound to the member who created them: they can never exceed that member’s own permissions, and LaunchDarkly automatically deactivates them when the member is removed from the account. Service tokens (plan-dependent — see Appendix A) are independent of any member, and their permissions are fixed at creation and cannot be edited afterward — to change a service token’s access you must create a replacement and revoke the old one. Use service tokens for CI/CD and automation precisely because they survive offboarding, and compensate with an inventory that records each token’s owner and purpose. (Access tokens documentation)

ClickOps Implementation

Step 1: Audit Access Tokens

  1. Navigate to: gear icon (left sidebar) → Organization settings → Authorization
  2. In the Access tokens section, review every token, its role, and its owner
  3. Remove unused tokens

Step 2: Create Scoped Tokens and Rotate on a Cadence

  1. In the Access tokens section, click Create token, give it a human-readable Name, and assign the least-privilege Role — Reader, Writer, Admin, Owner, a custom role, or an inline policy
  2. Select This is a service token only for automation that must outlive its creator; leave it unchecked for a personal token
  3. LaunchDarkly’s token-creation flow does not offer an expiration date, so enforce lifetime yourself: set a written rotation cadence (for example, quarterly), record each token’s issue date in your inventory, and reset or delete tokens on schedule rather than relying on a platform expiry (Creating API access tokens)

Code Pack: Terraform
hth-launchdarkly-2.02-api-token-security.tf View source on GitHub ↗
# Scoped read-only token for monitoring
resource "launchdarkly_access_token" "monitoring" {
  name          = "HTH Monitoring Read-Only"
  service_token = true

  inline_roles {
    effect    = "allow"
    actions   = ["viewProject"]
    resources = ["proj/*"]
  }

  inline_roles {
    effect    = "deny"
    actions   = ["createFlag", "deleteFlag", "updateOn", "updateOff"]
    resources = ["proj/*:env/*:flag/*"]
  }
}

# Scoped token for a specific project/environment
resource "launchdarkly_access_token" "project_scoped" {
  name          = "HTH Project-Scoped Token"
  service_token = true

  inline_roles {
    effect    = "allow"
    actions   = ["viewProject", "updateOn", "updateOff"]
    resources = ["proj/${var.project_key}:env/${var.environment_key}:flag/*"]
  }
}
Code Pack: API Script
hth-launchdarkly-2.02-api-token-security.sh View source on GitHub ↗
# List all tokens and check for overly permissive ones
TOKENS=$(ld_get "/tokens?showAll=true") || {
  fail "2.2 Unable to retrieve access tokens"
  increment_failed; summary; exit 0
}

TOTAL_TOKENS=$(echo "${TOKENS}" | jq '.items | length')
ADMIN_TOKENS=$(echo "${TOKENS}" | jq '[.items[] | select(.role == "admin")] | length')
NO_ROLE_TOKENS=$(echo "${TOKENS}" | jq '[.items[] | select(.role == "writer" or .role == "admin") | select(.serviceToken == true)] | length')

info "2.2 Total tokens: ${TOTAL_TOKENS}"
info "2.2 Admin-level tokens: ${ADMIN_TOKENS}"
info "2.2 Service tokens with write/admin: ${NO_ROLE_TOKENS}"

if [ "${ADMIN_TOKENS}" -gt 0 ]; then
  warn "2.2 Tokens with admin role (should use scoped roles instead):"
  echo "${TOKENS}" | jq -r '.items[] | select(.role == "admin") | "  - \(.name // "unnamed") (id: \(._id))"'
  fail "2.2 ${ADMIN_TOKENS} token(s) have admin role — scope down with custom roles"
  increment_failed
else
  pass "2.2 No tokens with admin role"
  increment_applied
fi
Code Pack: Sigma Detection Rule
hth-launchdarkly-2.02-api-token-security.yml View source on GitHub ↗
detection:
    selection:
        kind: 'token'
        accesses.action: 'createToken'
    condition: selection
fields:
    - date
    - title
    - member.email
    - target.name
    - accesses.resource

3. Environment Security

3.1 Environment Segmentation

Profile Level: L1 (Crawl) NIST 800-53: CM-3

Description

Separate LaunchDarkly environments and require comments, review or approval, and change history for changes to production.

Rationale

Why This Matters:

  • Isolating dev, staging, and production prevents test changes from accidentally altering live application behavior
  • Required reviews and approvals add a human checkpoint before high-impact production flag changes take effect
  • Mandatory comments and change history create an audit trail tying every change to a reason and an author
  • Approval workflows stop a single compromised or careless account from unilaterally toggling production features

Attack Prevented: Unauthorized production changes, accidental misconfiguration, unreviewed flag flips, change repudiation

ClickOps Implementation

Step 1: Configure Environment Settings

  1. Navigate to: Project settings → Environments
  2. Configure:
    • Require comments for changes
    • Require review for production
    • Enable change history

Step 2: Approval Workflows (Enterprise)

  1. Configure approval requirements
  2. Set minimum approvers
  3. Define bypass conditions

Code Pack: Terraform
hth-launchdarkly-3.01-environment-segmentation.tf View source on GitHub ↗
# Hardened production environment with approval workflows
resource "launchdarkly_environment" "production" {
  key                  = "production"
  name                 = "Production"
  color                = "FF0000"
  project_key          = var.project_key
  require_comments     = true
  confirm_changes      = true
  secure_mode          = true
  critical             = true
  default_track_events = true

  approval_settings {
    required                   = true
    min_num_approvals          = 2
    can_review_own_request     = false
    can_apply_declined_changes = false
    required_approval_tags     = ["sensitive"]
    service_kind               = "launchdarkly"
  }

  tags = ["hth-hardened"]
}

# Staging environment — comments required, but no approval gate
resource "launchdarkly_environment" "staging" {
  key              = "staging"
  name             = "Staging"
  color            = "FFA500"
  project_key      = var.project_key
  require_comments = true
  confirm_changes  = true
  secure_mode      = false
  critical         = false
}
Code Pack: API Script
hth-launchdarkly-3.01-environment-segmentation.sh View source on GitHub ↗
# Enable require-comments, confirm-changes, and mark as critical
CURRENT=$(ld_get "/projects/${LD_PROJECT_KEY}/environments/production") || {
  fail "3.1 Unable to retrieve production environment"
  increment_failed; summary; exit 0
}

REQUIRE_COMMENTS=$(echo "${CURRENT}" | jq -r '.requireComments')
CONFIRM_CHANGES=$(echo "${CURRENT}" | jq -r '.confirmChanges')
IS_CRITICAL=$(echo "${CURRENT}" | jq -r '.critical')

INSTRUCTIONS="[]"

if [ "${REQUIRE_COMMENTS}" != "true" ]; then
  INSTRUCTIONS=$(echo "${INSTRUCTIONS}" | jq '. + [{"kind": "updateRequireComments", "value": true}]')
fi
if [ "${CONFIRM_CHANGES}" != "true" ]; then
  INSTRUCTIONS=$(echo "${INSTRUCTIONS}" | jq '. + [{"kind": "updateConfirmChanges", "value": true}]')
fi
if [ "${IS_CRITICAL}" != "true" ]; then
  INSTRUCTIONS=$(echo "${INSTRUCTIONS}" | jq '. + [{"kind": "updateCritical", "value": true}]')
fi

if [ "$(echo "${INSTRUCTIONS}" | jq 'length')" -gt 0 ]; then
  info "3.1 Applying environment hardening..."
  PAYLOAD=$(jq -n --argjson inst "${INSTRUCTIONS}" '{
    "comment": "HTH: Harden production environment controls",
    "instructions": $inst
  }')
  ld_semantic_patch "/projects/${LD_PROJECT_KEY}/environments/production" "${PAYLOAD}" || {
    fail "3.1 Failed to update production environment"
    increment_failed; summary; exit 0
  }
  pass "3.1 Production environment hardened"
  increment_applied
else
  pass "3.1 Production environment already hardened"
  increment_applied
fi
Code Pack: Sigma Detection Rule
hth-launchdarkly-3.01-environment-segmentation.yml View source on GitHub ↗
detection:
    selection:
        kind: 'environment'
        accesses.action:
            - 'updateRequireComments'
            - 'updateConfirmChanges'
            - 'updateSecureMode'
            - 'updateApprovalSettings'
    filter_production:
        accesses.resource|contains: 'env/production'
    condition: selection and filter_production
fields:
    - date
    - title
    - member.email
    - accesses.action
    - accesses.resource
    - previousVersion
    - currentVersion

3.2 Flag Security

Profile Level: L2 (Walk) NIST 800-53: CM-7

Description

Tag flags that control security-sensitive behavior, apply extra review to them, and restrict who can view and change targeting rules.

Rationale

Why This Matters:

  • Flags that gate authentication, authorization, or other security controls can disable protections if flipped maliciously
  • Tagging and extra review ensure security-relevant flags receive scrutiny proportional to their impact
  • Restricting visibility of targeting rules prevents leakage of customer segments and internal business logic
  • Monitoring rule changes detects enumeration or tampering attempts before they affect users

Attack Prevented: Security-control bypass, targeting-rule enumeration, business-logic disclosure, unauthorized flag tampering

Implementation

Step 1: Tag Sensitive Flags

  1. Tag flags controlling security features
  2. Apply additional review requirements
  3. Audit changes

Step 2: Targeting Rule Protection

  1. Limit who can view targeting rules
  2. Audit rule changes
  3. Monitor for enumeration

Code Pack: Terraform
hth-launchdarkly-3.02-flag-security.tf View source on GitHub ↗
# Example: Secure feature flag with restricted client-side exposure
resource "launchdarkly_feature_flag" "secure_flag_example" {
  project_key    = var.project_key
  key            = "hth-example-secure-flag"
  name           = "HTH Example Secure Flag"
  description    = "Demonstrates HTH flag security best practices"
  variation_type = "boolean"
  temporary      = true

  # Restrict client-side SDK exposure
  client_side_availability {
    using_environment_id = false
    using_mobile_key     = false
  }

  # Assign a team maintainer for lifecycle ownership
  maintainer_team_key = var.maintainer_team_key

  tags = ["sensitive", "hth-managed"]

  variations {
    value       = true
    name        = "Enabled"
    description = "Feature is active"
  }

  variations {
    value       = false
    name        = "Disabled"
    description = "Feature is inactive"
  }

  defaults {
    on_variation  = 0
    off_variation = 1
  }
}
Code Pack: API Script
hth-launchdarkly-3.02-flag-security.sh View source on GitHub ↗
# Check for stale flags and unrestricted client-side exposure
FLAGS=$(ld_get "/flags/${LD_PROJECT_KEY}?summary=true&limit=100") || {
  fail "3.2 Unable to retrieve flags"
  increment_failed; summary; exit 0
}

TOTAL_FLAGS=$(echo "${FLAGS}" | jq '.items | length')
TEMP_FLAGS=$(echo "${FLAGS}" | jq '[.items[] | select(.temporary == true)] | length')
CLIENT_EXPOSED=$(echo "${FLAGS}" | jq '[.items[] | select(.clientSideAvailability.usingEnvironmentId == true)] | length')
NO_MAINTAINER=$(echo "${FLAGS}" | jq '[.items[] | select(._maintainer == null and ._maintainerTeam == null)] | length')

info "3.2 Total flags: ${TOTAL_FLAGS}"
info "3.2 Temporary flags: ${TEMP_FLAGS}"
info "3.2 Client-side exposed: ${CLIENT_EXPOSED}"
info "3.2 Without maintainer: ${NO_MAINTAINER}"

if [ "${NO_MAINTAINER}" -gt 0 ]; then
  warn "3.2 ${NO_MAINTAINER} flag(s) have no assigned maintainer"
fi

if [ "${CLIENT_EXPOSED}" -gt 0 ]; then
  warn "3.2 ${CLIENT_EXPOSED} flag(s) exposed to client-side SDKs — review for sensitive data"
fi

pass "3.2 Flag security audit complete"
increment_applied

4. Monitoring & Detection

4.1 Audit Log

Profile Level: L1 (Crawl) NIST 800-53: AU-2, AU-3

Description

Review the LaunchDarkly audit log regularly and export it to your SIEM to retain a durable record of all account, role, token, and flag changes.

Rationale

Why This Matters:

  • The audit log records who changed which flag, role, token, or setting and when, enabling investigation and accountability
  • Exporting to a SIEM preserves events beyond the platform retention window and correlates them with other security telemetry
  • Without centralized monitoring, malicious flag changes and token abuse can go undetected until they cause harm
  • Audit evidence supports SOC 2, ISO 27001, and other compliance obligations for change management

Attack Prevented: Undetected tampering, repudiation, delayed incident detection, audit-trail gaps

ClickOps Implementation

Step 1: Access Audit Log

  1. Navigate to: Account settings → Audit log (LaunchDarkly renamed the account settings area to Organization settings, reached via the gear icon in the left sidebar; the audit log documentation page was not publicly resolvable during this pass, so verify the exact path in your own account)
  2. Review changes
  3. Configure SIEM export

Detection Focus

Code Pack: Terraform
hth-launchdarkly-4.01-audit-log.tf View source on GitHub ↗
# Splunk audit log subscription
resource "launchdarkly_audit_log_subscription" "splunk" {
  integration_key = "splunk"
  name            = "HTH Splunk Audit Stream"
  on              = true

  config = {
    base_url = var.splunk_hec_url
    token    = var.splunk_hec_token
  }

  statements {
    effect    = "allow"
    actions   = ["*"]
    resources = ["proj/*"]
  }

  tags = ["hth", "siem"]
}

# Signed webhook for custom SIEM
resource "launchdarkly_webhook" "siem" {
  name   = "HTH SIEM Webhook"
  url    = var.siem_webhook_url
  on     = true
  secret = var.webhook_signing_secret

  statements {
    effect    = "allow"
    actions   = ["*"]
    resources = ["proj/*"]
  }

  tags = ["hth", "siem"]
}
Code Pack: API Script
hth-launchdarkly-4.01-audit-log.sh View source on GitHub ↗
# Create a signed webhook for SIEM audit log streaming
EXISTING=$(ld_get "/webhooks" | jq -r '.items[] | select(.name == "HTH SIEM Webhook") | ._id') || true

if [ -n "${EXISTING}" ]; then
  info "4.1 SIEM webhook already exists (id: ${EXISTING})"
  pass "4.1 Audit log webhook configured"
  increment_applied
else
  WEBHOOK_SECRET="${LD_WEBHOOK_SECRET:-$(openssl rand -hex 32)}"
  info "4.1 Creating signed webhook for SIEM..."
  RESPONSE=$(ld_post "/webhooks" "$(jq -n \
    --arg url "${LD_SIEM_WEBHOOK_URL}" \
    --arg secret "${WEBHOOK_SECRET}" \
    '{
      "name": "HTH SIEM Webhook",
      "url": $url,
      "sign": true,
      "secret": $secret,
      "on": true,
      "tags": ["hth", "siem"],
      "statements": [
        {
          "effect": "allow",
          "actions": ["*"],
          "resources": ["proj/*"]
        }
      ]
    }'
  )") || {
    fail "4.1 Failed to create SIEM webhook"
    increment_failed; summary; exit 0
  }
  pass "4.1 SIEM webhook created"
  info "4.1 Webhook signing secret: ${WEBHOOK_SECRET}"
  increment_applied
fi
Code Pack: Sigma Detection Rule
hth-launchdarkly-4.01-audit-log.yml View source on GitHub ↗
detection:
    selection_webhook:
        kind: 'webhook'
        accesses.action:
            - 'deleteWebhook'
    selection_integration:
        kind: 'integration'
        accesses.action:
            - 'deleteIntegration'
    condition: selection_webhook or selection_integration
fields:
    - date
    - title
    - member.email
    - target.name
    - accesses.resource

Appendix A: Edition Compatibility

LaunchDarkly’s public pricing page lists three plan tiers — Developer, Foundation, and Enterprise. The “Pro” tier this guide previously referenced no longer appears in LaunchDarkly’s plan lineup.

Control Developer Foundation Enterprise
SAML SSO
SCIM provisioning
Custom Roles
Approval Workflows
Service tokens
Default initial role “No access” ❌ (Reader) ❌ (Reader)

Two LaunchDarkly sources disagree on tier naming — verify against your own contract. The pricing page lists Developer / Foundation / Enterprise, while the SSO documentation attributes SCIM and team sync to a “Guardian” tier that does not appear in the published pricing lineup. Both are first-party LaunchDarkly sources, so this guide records the disagreement rather than resolving it: treat SCIM, custom roles, service tokens, and approvals as top-tier-only capabilities and confirm the exact plan name on your contract or with your account team before planning around them. (Pricing · SSO documentation)


Appendix B: References

Official LaunchDarkly Documentation:

API & Developer Resources:

Compliance Frameworks:

  • SOC 2 Type II, ISO 27001, ISO 27701, FedRAMP Moderate ATO, HIPAA – compliance reports available upon request via LaunchDarkly Support

Security Incidents:

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

Changelog

Date Version Maturity Changes Author
2026-08-08 0.2.0 draft Currency pass: correct plan tiers to Developer/Foundation/Enterprise with a both-sources note on the “Guardian” naming conflict; update console paths to Organization settings (SSO, Roles, Authorization verified against current docs; audit-log path annotated as unverified); add SSO default-initial-role and single-IdP callouts; distinguish personal vs service tokens and replace the unsupported token-expiration step with a manual rotation cadence; replace Trust Center/security-program links with first-party configuration docs Claude Code (Opus 4.8)
2026-06-29 0.1.1 draft Add cheat-sheet Description and Rationale for all controls Claude Code (Opus 4.8)
2025-12-14 0.1.0 draft Initial LaunchDarkly hardening guide Claude Code (Opus 4.5)

Contributing

Found an issue or want to improve this guide?