v0.1.0-draft AI Drafted

LaunchDarkly Hardening Guide

DevOps Last updated: 2025-12-14

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

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 (Baseline): Essential controls for all organizations
  • L2 (Hardened): Enhanced controls for security-sensitive environments
  • L3 (Maximum Security): 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 (Baseline) NIST 800-53: IA-2(1)

ClickOps Implementation

Step 1: Configure SAML SSO

  1. Navigate to: Account settings → Security → SAML
  2. Configure SAML IdP
  3. Enable: Require SSO

Step 2: Configure SCIM

  1. Enable SCIM provisioning
  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 (Baseline) NIST 800-53: AC-3, AC-6

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: Account settings → Roles
  2. Create environment-specific roles
  3. Apply least privilege

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 (Baseline) NIST 800-53: IA-5

Description

Protect LaunchDarkly SDK keys.

Rationale

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 (Baseline) NIST 800-53: IA-5

ClickOps Implementation

Step 1: Audit Access Tokens

  1. Navigate to: Account settings → Authorization → Access tokens
  2. Review all tokens
  3. Remove unused tokens

Step 2: Create Scoped Tokens

  1. Create tokens with custom roles
  2. Limit to specific projects/environments
  3. Set expiration dates

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 (Baseline) NIST 800-53: CM-3

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 (Hardened) NIST 800-53: CM-7

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 (Baseline) NIST 800-53: AU-2, AU-3

ClickOps Implementation

Step 1: Access Audit Log

  1. Navigate to: Account settings → Audit log
  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

Control Pro Enterprise
SAML SSO
SCIM
Custom Roles
Approval Workflows

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
2025-12-14 0.1.0 draft Initial LaunchDarkly hardening guide Claude Code (Opus 4.5)