Skip to content

SMS provisioning

Provision the AWS messaging resources and cryptographic material required by an SMS-enabled environment.

Owner: PlatformWhen to use: Enable or rotate SMS

Canonical sources. Infrastructure is defined in infra/components/sms-infrastructure.ts and infra/components/sms-crypto-keys.ts. Runtime parameter identifiers are configured in app/config/environments.yaml.

Provisioned by Pulumi

  • sms-infrastructure.ts provisions the configuration set, opt-out list, delivery and inbound topics and queues, and their SSM parameters.
  • sms-crypto-keys.ts provisions immutable, versioned SecureString parameters:
    • /onramp/<env>/sms/content-encryption-key/v1
    • /onramp/<env>/sms/verification/code-pepper/v1
  • Each stable root has a mutable String pointer at <root>/active. Its value is the full path of the selected immutable version.

The configured, unversioned root remains the keyring's configuration identifier. New writes store the full versioned identifier selected by <root>/active. Pulumi keeps the original random-resource and SSM-resource identities for each legacy root, freezes all of their existing inputs, and retains them on removal. Existing ciphertext, links, or digests that stored the unversioned identifier therefore continue to resolve the same legacy root bytes.

Fresh stacks also create a frozen legacy root to keep one deployment shape across environments. New writes use a selected version; the fresh legacy value is compatibility-only. The legacy value is never copied or aliased into v1.

Provision the complete keyring before enabling SMS. Each content-encryption and code-pepper root must have a valid <root>/active pointer to an existing version. Keep every historical key version referenced by persisted ciphertext, links, or digests so the portal and runtime can continue to resolve data written before a pointer rollover or restart. Missing pointers or retained versions fail closed.

Add and select a key version

Add the new version to contentEncryptionKeyVersions or codePepperVersions, then select it with activeContentEncryptionKeyVersion or activeCodePepperVersion. Keep every earlier version in the list. For example, selecting v2 while retaining v1 requires versions ['v1', 'v2'] and active version v2. All four arguments default to v1 provisioning and selection when omitted.

An immutable version uses overwrite: false, retains the AWS parameter when removed from Pulumi, and replaces on every input change. Because replacement creates before deletion at the same fixed SSM path, an attempted mutation fails rather than re-putting secret bytes. Active pointers are the only parameters intended to change in place, and each pointer depends on its selected SecureString so Pulumi cannot publish the pointer before the key material exists.

Do not retire a key on deployment alone. Retain each version until every durable reference to it has expired or migrated. This includes encrypted destinations and bodies, signed links, HMAC digests, and outstanding verification attempts. Re-encryption tooling is deferred and is not part of this provisioning flow.

Operator steps per environment

Complete these steps in the environment's AWS End User Messaging region before Flask boots with SMS enabled:

  1. Lease or identify the toll-free origination number. Store its E.164 value as a SecureString at /onramp/<env>/sms/aws/origination-identity.
  2. Enable self-managed opt-outs on the number: update-phone-number --phone-number-id <id> --self-managed-opt-outs-enabled.
  3. Add START as an opt-in keyword with action OPT_IN. Add UNSTOP if required. STOP and HELP are reserved and already handled.
  4. While the account remains in the SMS sandbox, verify every destination number that must receive test messages.

Verify

Confirm that:

  • every listed key version exists as a SecureString;
  • each /active parameter is a String containing the full path of a provisioned version;
  • each unversioned legacy root still contains its original bytes;
  • the origination identity and all sms-infrastructure.ts parameters exist; and
  • sandbox destinations appear in describe-verified-destination-numbers.

Delivery events flow through the configuration set attached by app/api/sms/providers/aws.py; no per-number configuration-set association is required.

Internal documentation — gated behind Cloudflare Access.