From a1e629a4aa44c6d3329a2d09a9096b9929bb64ec Mon Sep 17 00:00:00 2001 From: ayshadogo Date: Sat, 17 Oct 2026 19:30:14 +0100 Subject: [PATCH 1/2] docs: add key, certificate, vulnerability SLA, and patch management policies Add four policies under Governance/policies/ covering the lifecycle of everything that can be expired, leaked, or left waiting: - KEY_MANAGEMENT.md: key inventory with an owner per group, generation, storage and access rules, rotation, revocation at source, destruction, and verification. - CERTIFICATE_MANAGEMENT.md: issuance from a controlled source, renewal lead times, ownership, expiry monitoring, revocation, and a certificate inventory. - VULN_SLA.md: remediation targets per severity, measured from awareness to release, with the escalation path when a target is missed. - PATCH_MANAGEMENT.md: patch cadence by class, prioritisation order, and the emergency-patch path from declaration to recovery. Where existing governance already sets a rule, these policies reference it rather than restating it: the App Signing Key Policy keeps custody of signing material, the Secret Rotation Policy keeps credential cadence, and the Security Severity Rubric keeps the severity definitions. Each document states what is enforced by a workflow today and what is a human process. Closes #1489 Closes #1490 Closes #1491 Closes #1492 --- Governance/policies/CERTIFICATE_MANAGEMENT.md | 180 +++++++++++++++ Governance/policies/KEY_MANAGEMENT.md | 189 ++++++++++++++++ Governance/policies/PATCH_MANAGEMENT.md | 180 +++++++++++++++ Governance/policies/VULN_SLA.md | 210 ++++++++++++++++++ 4 files changed, 759 insertions(+) create mode 100644 Governance/policies/CERTIFICATE_MANAGEMENT.md create mode 100644 Governance/policies/KEY_MANAGEMENT.md create mode 100644 Governance/policies/PATCH_MANAGEMENT.md create mode 100644 Governance/policies/VULN_SLA.md diff --git a/Governance/policies/CERTIFICATE_MANAGEMENT.md b/Governance/policies/CERTIFICATE_MANAGEMENT.md new file mode 100644 index 0000000..7960a95 --- /dev/null +++ b/Governance/policies/CERTIFICATE_MANAGEMENT.md @@ -0,0 +1,180 @@ +# Certificate Management Policy + +## Purpose + +This policy defines how certificates used by TeachLink Mobile are issued, renewed, +monitored for expiry, and revoked when they should no longer be trusted. A certificate +that expires takes a feature down with it; one that stays trusted after it should have +been revoked is a way in. Both failures are preventable by treating certificates as +tracked assets with an owner and a date, rather than as configuration somebody renews +when they notice. + +## Scope + +This policy covers every certificate in the project's lifecycle: + +| Certificate | Purpose | Where it lives | +| --- | --- | --- | +| iOS distribution certificate (`.cer` / `.p12`) | Signs the iOS app distributed through the App Store | EAS credential store | +| iOS provisioning profile (`.mobileprovision`) | Binds the distribution certificate to the App ID and devices | EAS credential store | +| TLS server certificates | Terminates HTTPS for the API and WebSocket endpoints | Infrastructure provider | +| Certificate pinning material | Backup pins for app-to-server trust | App configuration, per the [Encryption Policy](ENCRYPTION.md) | +| Client and mTLS certificates, where used | Authenticates CI or a service to an internal endpoint | Secret store | + +It does not cover code-signing or commit-signing keys as such: the iOS distribution +certificate is included because it is a certificate, while the private key behind it +and the Android upload keystore are governed by the +[App Signing Key Policy](../domains/APP_SIGNING_KEYS.md) and the +[Key Management Policy](KEY_MANAGEMENT.md). + +## Issuance Rules + +Certificates are created from a controlled source so that there is always one record of +what was issued and by whom. + +- **Issued for a stated purpose.** Every certificate records what it is for, which + system consumes it, and the period it is valid for. A certificate issued "just in + case" is not tracked and eventually becomes the one nobody recognised. +- **Shortest viable validity.** Validity is set to what the job needs. A server + certificate valid for a year is preferred over one valid for ten, because the renewal + process is then exercised regularly and revocation is quicker to complete. +- **Controlled issuers.** Certificates are issued from the project's own accounts or + providers. A certificate obtained from a public free-CA service, or one whose private + key was generated on an untrusted machine, is not accepted. +- **Private keys generated in the controlled environment.** The private key is created + where it will be stored, or transferred by an approved secure channel, and never + generated on a personal machine and moved. +- **No shared certificates.** Each environment and purpose has its own certificate. A + staging certificate is not reused in production. +- **Recorded at issuance.** The certificate, its purpose, its issuer, its not-before and + not-after dates, and its owner are entered in the certificate inventory below + immediately, not when someone next thinks to update it. +- **Store approval required.** A new certificate on an Apple or Google identity, or on + a production hostname, is issued only with the approval required by the + [PR Approval Requirements](APPROVAL_REQUIREMENTS.md). + +## Ownership + +| Certificate group | Accountable owner | Responsible for performing | +| --- | --- | --- | +| iOS distribution certificate and provisioning profile | [Release Manager](../roles/RELEASE_MANAGER.md) | Performing renewal; releasing the new profile into EAS | +| TLS certificates for production endpoints | [Security Officer](../roles/SECURITY_OFFICER.md) with the infrastructure maintainer | Approving issuance; the infrastructure maintainer performs it | +| Pinning material | [Security Officer](../roles/SECURITY_OFFICER.md) | Approving a pin change, which requires two maintainers under the [CI/CD Governance Policy](CICD_GOVERNANCE.md) | +| Client and mTLS certificates for CI | [Security Officer](../roles/SECURITY_OFFICER.md) | Approving; the requesting maintainer performs it | +| Test and preview certificates | The maintainer running the preview environment | Performing renewal | + +Rules that hold for every certificate: + +- Ownership is a role, not a person, so the responsibility survives a handover. +- The owner is someone who can actually renew the certificate without needing the + previous owner. A certificate whose only knowledgeable holder is one person is a + single point of failure and is flagged for a second owner. +- Two people must be able to complete any renewal, matching the rule in the + [Secret Rotation Policy](SECRET_ROTATION.md). +- A certificate with no identified owner is treated as unmanaged: it is renewed and + given an owner, or revoked. + +## Renewal Rules + +Renewal starts early enough that expiry is never the reason a build fails. + +| Requirement | Detail | +| --- | --- | +| Renewal lead time | Production certificates are renewed **at least 30 days** before `notAfter`, and 60 days before for anything Apple-issued, because profile regeneration is not always immediate | +| Renewal changes nothing else | A renewal replaces the certificate, not the identity. Changing the subject, the SANs, or the key alongside the renewal is a separate change with its own review | +| Provisioning profile follows the certificate | An iOS renewal regenerates the provisioning profile bound to the new certificate; renewing one without the other leaves the pair mismatched | +| Verification before promotion | A renewed certificate is validated by a `preview` build or a staging request before it is used in production, per the promotion path in the [EAS Build Governance](../domains/EAS_BUILD_GOVERNANCE.md) | +| Retain the previous certificate | The superseded certificate is kept, in the secure store only, until a production build or request has been accepted with the new one, so a failed renewal can be reversed without an outage | +| No reuse of the old private key | A renewal generates a fresh key. Reusing the previous private key defeats much of the purpose of renewing | +| Retired certificates are revoked | Once the successor is in production, the previous certificate is revoked at the issuer rather than merely deleted locally, per the [Key Management Policy](KEY_MANAGEMENT.md#revocation) | +| Record it | Each renewal records the date, the owner, the certificate identifier, the new expiry, and the verification result | + +## Expiry Monitoring + +Expiry is monitored so that renewal is a scheduled task rather than an outage. The +owner of each certificate group is responsible for the following: + +| Control | Requirement | +| --- | --- | +| Expiry date recorded | Every certificate's `notAfter` is in the inventory, so the remaining life is visible without opening the certificate | +| Calendar entry | A renewal date is scheduled per certificate, with a reminder 14 days ahead of the renewal date and a second reminder at 30 days before `notAfter` | +| Monthly check | The owner confirms the calendar matches the actual certificates in the store. A certificate present in the store but absent from the inventory is treated as a finding | +| Automated expiry alert | Where the provider or platform offers expiry notification, it is enabled rather than relied upon alone, so that a rotation does not silence the alert | +| Escalation on approach | If a certificate is within 14 days of expiry and not renewed, the owner escalates through the [Escalation Path](../processes/ESCALATION_PATH.md) rather than waiting for it to lapse | +| Post-expiry check | After each renewal the owner confirms the old certificate no longer appears as current anywhere, including in the EAS credential list and in any local `.mobileprovision` left on a build machine | + +An expired certificate fails in the least obvious way: an iOS distribution certificate +blocks submission with a message that does not always name expiry, and a lapsed TLS +certificate produces an untrusted-connection screen for users. Both are more expensive +than a calendar entry. + +## Revocation + +A certificate is revoked at its issuer immediately when: + +- its private key is suspected or confirmed exposed; +- its validity period ends and it has no successor; +- it is issued to a person or machine that leaves the project, under + [Contributor Offboarding](../processes/OFFBOARDING.md); +- it is found configured for a purpose it was not issued for; or +- a provider or store announces that it has been superseded. + +Revocation removes trust at the source; deleting the local copy is not revocation. The +revocation is recorded with its date and reason, and where the certificate was a +signing identity, the exposure is reported through the +[Security Policy](../SECURITY_POLICY.md) and tracked in the +[Incident Log](../registers/INCIDENT_LOG.md). + +## Certificate Inventory + +| Certificate | Issuer | Purpose | Owner | `notAfter` | Status | +| --- | --- | --- | --- | --- | --- | +| iOS distribution certificate | Apple Developer | App Store distribution | [Release Manager](../roles/RELEASE_MANAGER.md) | Recorded in the EAS credential store | Managed | +| iOS provisioning profile | Apple Developer | Binds certificate to App ID | [Release Manager](../roles/RELEASE_MANAGER.md) | Regenerated with the certificate | Managed | +| Production TLS certificate | Infrastructure provider | `api.teachlink.com` | [Security Officer](../roles/SECURITY_OFFICER.md) | Provider-managed | Managed | +| Backup pin set | App configuration | App-to-server trust | [Security Officer](../roles/SECURITY_OFFICER.md) | Set with a documented expiry | Managed | + +Where a certificate's expiry date is held by a provider rather than by the project, the +inventory records who to ask. Where a value is not yet known, the cell names the source +of truth instead of being left blank, because a blank cell is indistinguishable from an +unmanaged certificate. + +## Enforcement Status + +- Expiry monitoring is a human activity driven by the inventory and the calendar. No + workflow currently fails on an approaching certificate expiry. +- Apple's platform enforces its own expiry: an expired distribution certificate blocks + submission, and an expired profile blocks installation of the build it belongs to. + This is a backstop, not a control; it is detected at the worst possible moment. +- Pinning material, if introduced, is the certificate with the largest expiry risk, + because a missed rotation becomes an outage rather than a warning. A pin set without + a backup pin is rejected under the [Encryption Policy](ENCRYPTION.md). + +## Exceptions + +A certificate that cannot be renewed within the lead time, or that must remain valid +past its intended expiry, is recorded in the +[Exception and Waiver Register](../registers/EXCEPTION_REGISTER.md) with an approver +holding the authority to grant it, the compensating control, and an expiry no more than +12 months ahead. An exception to "renew before expiry" is rarely acceptable for a +signing certificate; for a test-environment certificate it may be. + +## Ownership and Review + +The Security Officer owns this policy, with the Release Manager responsible for the +Apple-issued certificates. It is reviewed when a new certificate type is introduced, +when a provider changes its issuance model, and at least once every 12 months. Changes +are proposed in a pull request that touches only the `Governance/` folder. + +## Related Documentation + +- [Key Management Policy](KEY_MANAGEMENT.md): the private key lifecycle behind each + certificate +- [Secret Rotation Policy](SECRET_ROTATION.md): credential cadence and verification +- [Encryption Policy](ENCRYPTION.md): transport requirements and pinning rules +- [App Signing Key Policy](../domains/APP_SIGNING_KEYS.md): iOS and Android signing + custody and promotion +- [EAS Build Governance](../domains/EAS_BUILD_GOVERNANCE.md): the promotion path a + renewal follows +- [Contributor Offboarding](../processes/OFFBOARDING.md): revocation on departure +- [Exception and Waiver Register](../registers/EXCEPTION_REGISTER.md) diff --git a/Governance/policies/KEY_MANAGEMENT.md b/Governance/policies/KEY_MANAGEMENT.md new file mode 100644 index 0000000..326f8c6 --- /dev/null +++ b/Governance/policies/KEY_MANAGEMENT.md @@ -0,0 +1,189 @@ +# Key Management Policy + +## Purpose + +This policy defines the lifecycle of every cryptographic key TeachLink Mobile depends +on: how one is generated, where it is stored, who may reach it, when it is rotated, and +how it is revoked. A key that nobody can enumerate is a key nobody can rotate, and a key +that is never revoked stays useful to anyone who copied it. This document exists so +that the answer to "which keys does this project hold, and who owns each one?" is +written down rather than reconstructed from a build configuration. + +## Scope + +This policy covers all cryptographic keys, from creation to destruction, including: + +- symmetric data-encryption keys generated on device for the encrypted feature cache, + as in `src/utils/encryptedStorage.ts`; +- asymmetric keys and certificates used for app signing, submission, and update + delivery, whose custody is detailed in the + [App Signing Key Policy](../domains/APP_SIGNING_KEYS.md); +- TLS server certificates and any pinning material, under the + [Certificate Management Policy](CERTIFICATE_MANAGEMENT.md); +- commit and tag signing keys, under the + [Commit Signing Policy](COMMIT_SIGNING.md); +- keys issued to CI automation and third-party integrations, whose rotation cadence is + set by the [Secret Rotation Policy](SECRET_ROTATION.md); and +- service-account keys, under the [Service Accounts Policy](SERVICE_ACCOUNTS.md). + +This policy governs the **key**; the [Encryption Policy](ENCRYPTION.md) governs the +**algorithm**, and the two are read together because a key is only as strong as the +primitive protecting it. + +## Key Inventory + +Every key the project holds is listed with an owner. A key that exists and is not on +this list is unmanaged, and the first action is to find its owner or destroy it. + +| Key group | Where it lives | Accountable owner | Lifecycle policy | +| --- | --- | --- | --- | +| iOS distribution certificate and private key | EAS credential store | [Release Manager](../roles/RELEASE_MANAGER.md) | [App Signing Keys](../domains/APP_SIGNING_KEYS.md) | +| iOS provisioning profile | EAS credential store | [Release Manager](../roles/RELEASE_MANAGER.md) | [Certificate Management](CERTIFICATE_MANAGEMENT.md) | +| App Store Connect API key (`.p8`) | EAS and secret store | [Release Manager](../roles/RELEASE_MANAGER.md) | [Secret Rotation](SECRET_ROTATION.md) | +| Android upload keystore (`.jks`) | EAS credential store | [Release Manager](../roles/RELEASE_MANAGER.md) | [App Signing Keys](../domains/APP_SIGNING_KEYS.md) | +| Google Play service-account key JSON | Secret store | [Security Officer](../roles/SECURITY_OFFICER.md) | [Service Accounts](SERVICE_ACCOUNTS.md) | +| Commit and tag signing keys | Each contributor's own device or hardware token | The signer, verified by the [Security Officer](../roles/SECURITY_OFFICER.md) | [Commit Signing](COMMIT_SIGNING.md) | +| On-device feature-cache data key | iOS Keychain / Android Keystore via `expo-secure-store` | Generated and destroyed on device; no human owner | This policy | +| CI and integration API tokens | GitHub Actions secrets, provider secret stores | [Security Officer](../roles/SECURITY_OFFICER.md) | [Secret Rotation](SECRET_ROTATION.md) | + +Recording a new key group is part of creating it. A key introduced by a change lands in +this table, or in the register the policy it belongs to, in that same change. + +## Key Generation + +- Keys are generated by a cryptographically secure random number generator, never by a + general-purpose one. `encryptedStorage.ts` generates its AES key with `expo-crypto`; + a change that substitutes `Math.random()` is a defect. +- Key length and algorithm follow the + [approved algorithms](ENCRYPTION.md#approved-algorithms): AES-GCM 256 for symmetric + data keys, RSA at 2048 bits or above or ECDSA P-256 and above for asymmetric keys. +- A key is not generated from a passphrase-derived value unless a KDF per the approved + list is applied. Deriving a key from a password with a bare hash is not permitted. +- Each key has a purpose. A key used for two purposes must be split, because the + compromise of a shared key compromises both uses at once. +- Keys are generated as late as possible and destroyed as early as possible. An + on-device cache key is created when the feature first needs it rather than at install + time. + +## Storage and Access Rules + +| Rule | Detail | +| --- | --- | +| Never in the repository | No key, private half, keystore password, or passphrase is committed, including in examples, tests, fixtures, and screenshots | +| Approved stores only | Protected storage (`expo-secure-store`, i.e. Keychain and Android Keystore), the EAS credential store, or the platform secret store | +| Never beside the ciphertext | A data key is not stored in the same place as the data it protects. The feature-cache key lives in protected storage while the ciphertext lives in `AsyncStorage` | +| Narrow scope | A key is scoped to the single store, workflow, or environment that needs it | +| Individual custody | Private keys are personal and non-transferable; a shared key defeats attribution and is not permitted | +| No sharing | A private key is never pasted into an issue, pull request, chat, or log | +| Least privilege for access | Only the named owner can export or replace a key; contributors and reviewers may request a build but may not read or copy key material | +| Minimum exposure | A key is never held in memory longer than the operation requires, and never written to a crash report or telemetry event | +| Backup exclusion | Stored key material is excluded from device backup, so a restored device cannot resurrect a session or key | + +Where a decision requires a judgement about who should hold a key, the +[Decision Rights Matrix](../matrices/DECISION_RIGHTS_MATRIX.md) resolves it. + +## Rotation + +Rotation replaces a key with a new one before the old one becomes a liability, on the +cadence set by the policy that owns the key: + +| Key group | Cadence | Rotation policy | +| --- | --- | --- | +| Signing and submission material | At minimum annually | [App Signing Key Policy](../domains/APP_SIGNING_KEYS.md#rotation-process) | +| Platform tokens and service-account keys | 90 days | [Secret Rotation Policy](SECRET_ROTATION.md) | +| Third-party and CI integration keys | 180 days | [Secret Rotation Policy](SECRET_ROTATION.md) | +| Commit signing keys | On expiry or suspicion of compromise | [Commit Signing Policy](COMMIT_SIGNING.md#rotation-expiry-and-revocation) | +| On-device feature-cache keys | On compromise signal, or when the protected storage is reset | This policy | + +Rotation is immediate, regardless of schedule, when a key is suspected or confirmed +exposed, when a holder leaves the team or loses the device holding it, or when there is +evidence of an unauthorised build, upload, or use. + +Order of operations matters. Revoke or retire the old key at the source **before** +returning to service, then verify the new key works. For signing material the +superseded key is retained in the secure store only until the store accepts a build +signed with the new key, which is the rollback position described in the +[App Signing Key Policy](../domains/APP_SIGNING_KEYS.md#verification-and-rollback). + +Previously signed artefacts stay valid. History is not rewritten to re-sign commits or +re-issue builds that users already have; the affected window is reviewed instead. + +## Revocation + +Revocation is the action that actually removes trust, and replacing a secret without +revoking it leaves the old value usable. + +- Revocation happens **at the source**: the certificate is revoked, the token is + deleted, the keystore is disabled, or the signing key is removed from the account. + Overwriting a stored value is not revocation. +- Revocation is recorded. The revoked key identifier, the date, the reason, and the + person who performed it are written to the relevant register or issue, so a later + reader can tell a deliberate revocation from a lost record. +- Revoked keys are never deleted from the inventory. They are marked revoked, with the + date, because the record of what was trusted is part of the audit trail. +- A revoked key that is later found in use indicates a compromise, not a mistake, and + is handled as an incident through the [Security Policy](../SECURITY_POLICY.md) and + recorded in the [Incident Log](../registers/INCIDENT_LOG.md). +- Revocation is complete when the key can no longer authenticate or validate anything. + A key that is revoked but still present in a cached build configuration is not + revoked. + +## Destruction + +A retired key is destroyed once the retention requirement is met and no build, tag, or +commit still depends on it. Destruction means the key material is no longer +recoverable from the store, from backups, and from the devices of anyone who held it. +On a user's device, destroying the protected-storage entry destroys the data it +protects, which is why the feature cache is designed to be re-fetchable rather than +irreplaceable. + +## Verification + +Key controls are verified rather than assumed: + +| Verification | How | +| --- | --- | +| A key is reachable only by its owner | The [Access Register](../registers/ACCESS_REGISTER.md) entry matches the exportable set | +| A key is not in the repository | `.gitignore` covers the key file types, and the ignore-rule audit in the [App Signing Key Policy](../domains/APP_SIGNING_KEYS.md#enforcement-checks) runs before a release | +| A rotation worked | The new key authenticates and the old one is verifiably rejected at the source, per the [Secret Rotation Policy](SECRET_ROTATION.md#verification-requirement) | +| A revocation worked | An attempt to use the revoked key fails, and the result is recorded | +| A key is in inventory | Each key group in the inventory table has an owner who confirms it still exists | + +## Enforcement Status + +- Key custody is enforced by platform storage guarantees, `.gitignore` rules, review of + diffs touching `eas.json`, `app.config.ts`, and `.github/workflows/`, and the + pre-release ignore-rule audit. No automated check inspects a key store. +- Key rotation age is not machine-checked. Cadences are tracked on the calendar the + [Secret Rotation Policy](SECRET_ROTATION.md#scheduling-and-reminders) describes. +- The inventory above is a maintained document; it is only useful if it is corrected + when a key group changes. + +## Exceptions + +A deviation — an unrotated key past its cadence, a key stored outside an approved +store, or shared custody — is recorded in the +[Exception and Waiver Register](../registers/EXCEPTION_REGISTER.md) with an approver +holding the authority to grant it, the compensating control, and an expiry no more than +12 months ahead. Where children's data is involved, the +[Children's Data Policy](../compliance/CHILDRENS_DATA.md) applies. + +## Ownership and Review + +The Security Officer owns this policy with the release-management team, who hold the +material. It is reviewed whenever a key group is added or removed, when a provider +changes its key model, and at least once every 12 months. Changes are proposed in a +pull request that touches only the `Governance/` folder. + +## Related Documentation + +- [Encryption Policy](ENCRYPTION.md): approved algorithms and what must be encrypted +- [Secret Rotation Policy](SECRET_ROTATION.md): rotation cadence and verification +- [Certificate Management Policy](CERTIFICATE_MANAGEMENT.md): certificate lifecycle +- [Service Accounts Policy](SERVICE_ACCOUNTS.md): machine identities holding keys +- [App Signing Key Policy](../domains/APP_SIGNING_KEYS.md): build and store signing +- [Commit Signing Policy](COMMIT_SIGNING.md): commit and tag keys +- [Security Policy](../SECURITY_POLICY.md) and + [Incident Log](../registers/INCIDENT_LOG.md) +- [Exception and Waiver Register](../registers/EXCEPTION_REGISTER.md) and + [Access Register](../registers/ACCESS_REGISTER.md) diff --git a/Governance/policies/PATCH_MANAGEMENT.md b/Governance/policies/PATCH_MANAGEMENT.md new file mode 100644 index 0000000..031391d --- /dev/null +++ b/Governance/policies/PATCH_MANAGEMENT.md @@ -0,0 +1,180 @@ +# Patch Management Policy + +## Purpose + +This policy defines how patches are brought into TeachLink Mobile, how they are +prioritised against other work, and how an emergency patch is delivered faster than the +normal release process. A patch process that only describes the routine case leaves the +project improvising exactly when a patch matters most, so the routine cadence, the +prioritisation rules, and the emergency path are all specified here. + +## Scope + +This policy covers every update that changes shipped code or a shipped dependency: + +- application source changes that fix a defect or a security issue; +- dependency and transitive-dependency updates, driven by Renovate and by `audit.yml`; +- base image, toolchain, and build-dependency updates, including Expo SDK and React + Native upgrades; +- Android and iOS platform dependency updates shipped with the app; and +- patches to vendored or generated code committed to the repository. + +It does not govern the content of a security remediation deadline, which is the +[Vulnerability Remediation SLA Policy](VULN_SLA.md), nor the mechanics of producing and +signing a build, which are in the +[EAS Build Governance](../domains/EAS_BUILD_GOVERNANCE.md) and the +[App Signing Key Policy](../domains/APP_SIGNING_KEYS.md). + +## Patch Cadence + +| Patch class | Target window | Delivery | +| --- | --- | --- | +| Security patch, Critical or High severity | Per the [remediation SLA](VULN_SLA.md#remediation-targets-by-severity) | Expedited release, outside the normal cadence | +| Security patch, Medium or Low severity | 30 and 90 calendar days respectively | Next regular release | +| Routine dependency update, no advisory | Within **14 days** of the update becoming available, or in the next release, whichever is sooner | Next regular release | +| Security-adjacent tooling update, such as a CI action | Within **7 days** | Next release; immediately if it fixes an exploitable workflow flaw | +| Base image or toolchain patch | Within **30 days** | Next release, with build verification | +| Major version or framework upgrade | Planned work, not a patch | Milestone, following the [Versioning Policy](VERSIONING.md) | +| Platform SDK adoption | Planned work | Milestone, following the [OS Version Support Policy](../domains/OS_VERSION_SUPPORT.md) | + +Two rules apply across all classes: + +- **A patch is not a feature.** A patch lands on its own, is reviewed on its own, and is + not bundled with unrelated changes. Bundling makes a patch unreviewable, which is the + main reason bundled hotfixes take longer than they should. +- **Verification is part of the patch.** A patch ships only after the checks in + [PR Approval Requirements](APPROVAL_REQUIREMENTS.md#required-checks) pass. A patch + that cannot pass the checks is escalated, not merged with the checks relaxed. + +## Prioritisation + +When more patches are available than the release can carry, they are ordered by this +rule set, applied in order. + +| Priority | Criterion | Example | +| --- | --- | --- | +| 1 | Actively exploited, or a flaw in the release-signing or update-delivery chain | Compromised signing credential; an advisory with a known exploit in the wild | +| 2 | Critical or High severity affecting users' data, accounts, or payment | Authentication bypass; token exposure | +| 3 | Severity for which no workaround or mitigation is available to users | A crash or data-loss defect on the current release line | +| 4 | Severity for which a workaround exists | Medium severity with a configuration mitigation | +| 5 | Patch that removes an exception, or closes a security finding | Clearing a recorded `security` finding | +| 6 | Routine dependency update removing a deprecated or vulnerable package | Removing a package flagged by `audit.yml` | +| 7 | Toolchain or build-dependency update with no user-visible effect | Developer-experience and build-stability updates | + +Rules that resolve ties and edge cases: + +- **User exposure outranks internal risk.** A defect affecting a teacher's data ranks + above a defect affecting only maintainer tooling, at the same severity. +- **Reachability decides, not the advisory.** An advisory for a code path the app never + reaches is not automatically low priority; it is first assessed for reachability, and + the assessment is recorded. Severity is only lowered with a written argument, which is + the adjustment the [Vulnerability Remediation SLA Policy](VULN_SLA.md) describes. +- **Unblocking counts.** A patch that unblocks a release or removes a flakiness that is + consuming maintainer time moves up. +- **A patch is never deprioritised for being inconvenient.** "Large diff", "touching + many files", and "risky to merge" are reasons to review carefully and split into + reviewable pieces, not reasons to postpone indefinitely. +- **Batch size is bounded.** A routine update release carries a limited number of + changes, so that a regression can be attributed. When the limit is reached, the + remainder waits for the next release rather than growing an unreviewable batch. + +## Emergency-Patch Path + +The emergency path exists for a defect or security issue that cannot wait for the normal +release cadence. It shortens the process; it does not remove the controls. + +1. **Declare.** The Release Manager or Security Officer declares the emergency, states + the user impact, and opens an issue labelled `emergency`. +2. **Scope minimally.** The patch contains only what is needed to resolve the emergency. + Additional improvements wait for the next release, which is what keeps the patch + reviewable. +3. **Fix on a dedicated branch.** The work happens on a branch from the current release + tag or `main`, so it does not inherit unrelated in-flight changes. +4. **Review.** Two maintainers review, at least one of whom is the Release Manager. For + a security fix the Security Officer reviews as well. The emergency override in + [PR Approval Requirements](APPROVAL_REQUIREMENTS.md#emergency-overrides) allows + fewer checks, never fewer approvals, and the reason is recorded. +5. **Verify locally as far as the emergency allows.** The full suite is still run; where + a non-blocking check cannot complete in time, it is recorded and re-run afterwards + rather than assumed. +6. **Ship.** The build follows the promotion path in the + [EAS Build Governance](../domains/EAS_BUILD_GOVERNANCE.md): `development`, then + `preview` with a real device, then `production`. A signed `preview` build is + confirmed before promotion. +7. **Recover.** Within 24 hours the full test suite is run against the shipped change. + Where it fails, the emergency patch is reverted on the next release and the cause is + recorded, following the override's post-merge requirements. +8. **Follow up.** A post-incident review is filed for the cause and for anything the + emergency deferred, so the gap is closed rather than remembered. + +An emergency patch that changes the release-signing chain additionally follows the +[App Signing Key Policy](../domains/APP_SIGNING_KEYS.md), including the requirement to +validate with a `preview` build before a production submission. + +Emergency status is used for user impact, not for a missed internal deadline. If it is +being used for the second thing, the deadline is the problem to fix, and that is +recorded as a governance item. + +## Release Cadence Relationship + +Patches flow through the +[Release Cadence](../processes/RELEASE_CADENCE.md) and the +[Release Branching Policy](RELEASE_BRANCHING.md). A regular release draws from +`main` and carries the routine batch; an expedited release is cut from the emergency +branch and is reconciled into `main` afterwards, so `main` always reflects what shipped. +Every patch appears in `CHANGELOG.md` and the release notes under the +[Changelog Policy](CHANGELOG_POLICY.md), with the issue it resolves referenced. + +## Verification + +| Check | Requirement | +| --- | --- | +| Checks pass | The required checks pass on the patch, or a recorded exception applies | +| No unrelated change | The diff contains only the patch, as reviewers confirm | +| Reproduces and fixes | Where the defect is reproducible, the patch is shown to fix it and not to change the behaviour under test | +| Rehearsed on a real build | For a platform or dependency patch, a `preview` build installs and launches | +| Attributable | The change is one patch per release entry, so a regression can be traced to it | +| Reverted cleanly if wrong | A revert is possible without a coordinated rollback, which is checked before shipping | + +## Enforcement Status + +- `audit.yml` runs on push and weekly and fails on high and critical dependency + advisories; Renovate proposes routine updates. This is the automated part of the + cadence. +- The 14-day routine window, the prioritisation order, and the emergency declaration + are human processes. No automation enforces a patch age or the batch-size bound. +- Reverting is deliberately easy because patches land on their own branches; that is a + design property, not a control. + +## Exceptions + +A patch that cannot be applied within its window — a dependency that breaks the build, +a vendor release that regresses a tested path, a platform fix unavailable to the project +— is recorded in the +[Exception and Waiver Register](../registers/EXCEPTION_REGISTER.md) with the reason, +the compensating control, an approver with authority, and an expiry no more than 12 +months ahead. An exception says why a patch is late; it does not make the late patch +optional, and the patch is still applied as soon as the blocker is resolved. + +## Ownership and Review + +The Release Manager owns this policy, with the Security Officer responsible for the +security-patch classes. It is reviewed after every emergency patch, when the release +cadence changes, and at least once every 12 months. Changes are proposed in a pull +request that touches only the `Governance/` folder. + +## Related Documentation + +- [Vulnerability Remediation SLA Policy](VULN_SLA.md): remediation deadlines by severity +- [Release Cadence](../processes/RELEASE_CADENCE.md) and + [Release Branching Policy](RELEASE_BRANCHING.md): how patches reach users +- [PR Approval Requirements](APPROVAL_REQUIREMENTS.md): checks and the emergency + override +- [EAS Build Governance](../domains/EAS_BUILD_GOVERNANCE.md): the promotion path +- [App Signing Key Policy](../domains/APP_SIGNING_KEYS.md): patches that touch signing +- [Versioning Policy](VERSIONING.md): where a major upgrade sits relative to a patch +- [Bot and Automation Governance Policy](BOT_AUTOMATION.md): Renovate and automated + updates +- [Dependency Register](../registers/DEPENDENCY_REGISTER.md) and + [Exception and Waiver Register](../registers/EXCEPTION_REGISTER.md) +- [Release Manager role](../roles/RELEASE_MANAGER.md) diff --git a/Governance/policies/VULN_SLA.md b/Governance/policies/VULN_SLA.md new file mode 100644 index 0000000..e98649a --- /dev/null +++ b/Governance/policies/VULN_SLA.md @@ -0,0 +1,210 @@ +# Vulnerability Remediation SLA Policy + +## Purpose + +This policy sets a remediation deadline for every known vulnerability that affects +TeachLink Mobile, measured from the moment the project becomes aware of it, and defines +what happens when a deadline is missed. Acknowledging a report quickly is of little +value if nothing is fixed for a year; this document is about the fix, and about being +honest when the fix will be late. + +## Scope + +This policy covers remediation of vulnerabilities that are known to the project. It +begins when the project becomes aware of a vulnerability through any channel — a +private report, an automated advisory, a dependency alert, a store notice, a public +disclosure, or a maintainer's own finding — and ends when users are no longer exposed, +either because a fix has shipped or because the exposure is formally closed. + +It covers vulnerabilities in: + +- TeachLink Mobile's own application code; +- dependencies in the [Dependency Register](../registers/DEPENDENCY_REGISTER.md), + including transitive ones reported by `audit.yml`; +- the build, release, and update-delivery chain, where a compromise of signing material + is treated as Critical per the + [Security Severity Rubric](../SECURITY_SEVERITY_RUBRIC.md); +- the mobile platform, such as an OS or SDK flaw affecting the app; and +- third-party services the app depends on, in coordination with the provider. + +It does not govern how a vulnerability is reported. That is the +[Security Policy](../SECURITY_POLICY.md) and the +[Vulnerability Disclosure Process](../processes/VULN_DISCLOSURE.md). + +## Remediation Targets by Severity + +Severity is assigned using the +[Security Severity Rubric](../SECURITY_SEVERITY_RUBRIC.md#severity-levels), so that +the same level means the same thing everywhere. A project-specific adjustment is +permissible where the rubric does not fit, for example when a library flaw has no +reachable call path in the app; the adjustment and its reasoning are recorded with the +finding rather than applied silently. + +| Severity | Fix or mitigation deployed | Update available to users | Notes | +| --- | --- | --- | --- | +| **Critical** | **24 hours** from awareness | 7 calendar days | Remote exploitation without interaction, account takeover, data loss, or compromise of the release-signing chain | +| **High** | **7 calendar days** from awareness | 14 calendar days | Limited-interaction or limited-privilege exploitation of user data, payment, or authentication | +| **Medium** | **30 calendar days** from awareness | Next regular release | Meaningful interaction or unusual conditions required; targeted exposure or denial of service | +| **Low** | **90 calendar days** from awareness | Next regular release | Limited impact, self-inflicted harm, or best-practice hardening | +| **Informational** | No fixed deadline | Not applicable | Recorded, and acted on only if it becomes part of a larger fix | + +Two rules qualify every row: + +- **Mitigation counts only if it removes the exposure.** A change that reduces severity + without removing the vulnerable path is not a mitigation. A workaround that blocks + the exploitation path, such as disabling the affected feature behind a flag, counts; + a comment, a note, or a monitoring rule does not. +- **A fix with no release path is not a fix.** The deadline that matters to a user is + the one in the "available to users" column. Where app-store review gates a release, + the target includes that lead time, and a delay caused by store review is recorded as + such rather than counted against the team. + +The rubric's acknowledgement targets — 4 business hours for Critical, 1 business day +for High — continue to apply unchanged and are measured separately. + +## Measurement + +Every vulnerability is tracked as an issue so that the clock is visible and auditable. + +### What starts the clock + +The clock starts at the **time of awareness**, which is the earliest of: + +- the moment a private report reaches the project, recorded by the + [Security Officer](../roles/SECURITY_OFFICER.md); +- the moment an automated advisory matching an affected dependency or image is published; +- the moment a store or platform vendor notifies the project; or +- the moment a maintainer identifies the issue in their own work. + +Awareness time, not publication time, is the reference. A vulnerability published +yesterday and discovered today starts today, and the days it was public before +discovery count against the provider only if the provider was in a position to notify +the project. + +### What stops the clock + +The clock stops when the exposure ends for users: + +- a release containing the fix has **shipped and is available**; or +- the vulnerability is formally closed because it is not exploitable in this app, which + requires a documented reachability argument, not an absence of evidence of exploit. + +A fix merged to `main` but not yet released does not stop the clock. The gap between +merge and release is the project's responsibility and is planned for, not treated as +waiting time. + +### What is recorded + +| Field | Content | +| --- | --- | +| Awareness timestamp | With time zone, and the source that triggered it | +| Severity and rationale | The level, and why it matches the rubric | +| Affected versions | Which released app versions and which dependency versions are affected | +| Exposure scope | Which users and which data, per the [Data Processing Register](../registers/DATA_PROCESSING_REGISTER.md) | +| Owner | The maintainer accountable for the fix | +| Milestones | Reproduce, fix merged, fix released, advisory published | +| Actual times | The real timestamps for each milestone, used to compute the variance | + +These fields match the issue templates and the +[Incident Log](../registers/INCIDENT_LOG.md), so a vulnerability that becomes an +incident is not re-described from scratch. + +### Tracking the targets + +- Open vulnerabilities are listed with the days remaining against their targets at the + monthly security review, which is the security counterpart of the reporting in the + [Code Review Service Level Agreement](REVIEW_SLA.md). +- A vulnerability approaching 80 percent of its window is flagged to the Security + Officer and the Release Manager. +- The measurement method is deliberately simple and stated here so that it cannot be + quietly adjusted per finding: start at awareness, stop at release, count calendar + days, and record both. + +## Escalation When a Target Is Missed + +A missed target is a signal to change the plan, not to relabel the severity. + +1. **Declare it.** Within one business day of the target being missed, the owner marks + the issue as missed and states why: not started, in progress, blocked, awaiting + review, awaiting store review, or awaiting a third party. +2. **Escalate immediately** for Critical and High. The [Escalation Path](../processes/ESCALATION_PATH.md) + applies, and the Release Manager decides whether the fix ships as an expedited + release outside the normal cadence. A security fix may not wait for a milestone that + is weeks away. +3. **Tell the users when the exposure is user-facing.** If a released version is + affected and no fix is available, the disclosure decision follows the coordinated + process in the + [Security Severity Rubric](../SECURITY_SEVERITY_RUBRIC.md#coordinated-disclosure), + including notifying store listings if that is what reaches users. +4. **Record the cause.** A missed target without a stated cause is treated as an + incomplete record and is raised at the next security review. +5. **Fix the system, not just the instance.** Where the cause is structural — no + automated dependency alert, a test that does not cover the affected path, a single + maintainer who alone understands the area — a follow-up task is created to remove + that dependency, so the next occurrence is less likely to be late. +6. **Do not repeat silently.** A second missed target for the same vulnerability, or a + pattern of misses in one severity band, is escalated to the maintainers as a + governance item. + +Missing a target is not itself a fault, provided it is declared, escalated, and +explained. Hiding it, or silently extending the deadline, is. + +## Dependencies and Third Parties + +- An automated advisory with no fix available is remediated by mitigation: the affected + dependency is removed, replaced, or disabled, and the exposure is recorded with the + provider's response time. +- A fix is merged within the target window even when the provider has not shipped one, + by pinning to a safe version, backporting the fix, or removing the feature path. +- A third-party service vulnerability that the project cannot mitigate is recorded as + accepted risk in the [Risk Register](../registers/RISK_REGISTER.md) with the + compensating control, and is reported to users if their data is affected. +- Renovate and `audit.yml` handle routine dependency updates; a high or critical + advisory bypasses the normal update cadence and is treated as a Critical or High + finding by severity, not by convenience. + +## Exceptions + +A remediation target that cannot be met for a documented reason — a platform vendor +with no fix and no workaround, a store review outside the project's control, a +dependency that cannot be removed without breaking the app — is recorded in the +[Security Exceptions Register](../registers/SECURITY_EXCEPTIONS_REGISTER.md), with the +compensating control and an expiry no more than 12 months ahead. An exception is not +available for "we did not get to it". + +## Enforcement Status + +- `audit.yml` runs on push and on a weekly schedule and fails on high and critical + dependency advisories, which is the automated half of dependency remediation. Its + checks are the repository's real defence for the dependency case. +- Advisory matching, exposure analysis, and user notification are manual. Nothing + currently pages anyone when an advisory appears or when a target is missed, so + measurement depends on a maintainer reading the weekly audit run. +- Automation for advisory matching and for target tracking is a reasonable follow-up and + would be introduced under the + [Bot and Automation Governance Policy](BOT_AUTOMATION.md). + +## Ownership and Review + +The Security Officer owns this policy. It is reviewed after any missed Critical or High +target, when the severity rubric changes, when a new automated advisory source is +added, and at least once every 12 months. Changes are proposed in a pull request that +touches only the `Governance/` folder. + +## Related Documentation + +- [Security Severity Rubric](../SECURITY_SEVERITY_RUBRIC.md): severity levels and + acknowledgement SLAs +- [Security Policy](../SECURITY_POLICY.md): how to report a vulnerability +- [Vulnerability Disclosure Process](../processes/VULN_DISCLOSURE.md): coordination with + reporters +- [Escalation Path](../processes/ESCALATION_PATH.md): what happens when a target is + missed +- [App Signing Key Policy](../domains/APP_SIGNING_KEYS.md): compromise of the + release-signing chain +- [Dependency Register](../registers/DEPENDENCY_REGISTER.md) and + [Risk Register](../registers/RISK_REGISTER.md) +- [Incident Log](../registers/INCIDENT_LOG.md) and + [Security Exceptions Register](../registers/SECURITY_EXCEPTIONS_REGISTER.md) +- [Security Officer role](../roles/SECURITY_OFFICER.md) and + [Release Manager role](../roles/RELEASE_MANAGER.md) From 03412b7b3355c129bb58c44e9012edbd951ee6c7 Mon Sep 17 00:00:00 2001 From: ayshadogo Date: Sat, 17 Oct 2026 20:14:01 +0100 Subject: [PATCH 2/2] docs(governance): correct anchor reference in the key management policy APP_SIGNING_KEYS.md has no "Verification and rollback" heading; the rollback position is described inside the "Rotation Process" section, so the link now points at that heading. Closes #1489 --- Governance/policies/KEY_MANAGEMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Governance/policies/KEY_MANAGEMENT.md b/Governance/policies/KEY_MANAGEMENT.md index 326f8c6..666a769 100644 --- a/Governance/policies/KEY_MANAGEMENT.md +++ b/Governance/policies/KEY_MANAGEMENT.md @@ -103,7 +103,7 @@ Order of operations matters. Revoke or retire the old key at the source **before returning to service, then verify the new key works. For signing material the superseded key is retained in the secure store only until the store accepts a build signed with the new key, which is the rollback position described in the -[App Signing Key Policy](../domains/APP_SIGNING_KEYS.md#verification-and-rollback). +[App Signing Key Policy](../domains/APP_SIGNING_KEYS.md#rotation-process). Previously signed artefacts stay valid. History is not rewritten to re-sign commits or re-issue builds that users already have; the affected window is reviewed instead.