intermediate mpamulti-party-approvalsecurityconfiguration

Multi-Party Approval Setup

Configure and use multi-party approval for sensitive actions requiring M-of-N group consensus

Multi-Party Approval Setup

Multi-party approval (MPA) lets you require M out of N approvals before a sensitive action can proceed. This guide shows you how to configure MPA policies and integrate them into your application.

What You’ll Learn

  • When to use MPA
  • How to structure approval groups
  • How to create MPA requests
  • How to handle partial approvals and denials
  • Best practices for timeout and escalation

When to Use MPA

MPA is for actions that are destructive, irreversible, or high-risk. Examples:

  • Infrastructure: Cold boot a production server, delete a database, rotate encryption keys
  • Finance: Wire transfer over $10,000, change account ownership, approve large refunds
  • Security: Disable 2FA, export user data, grant admin access, revoke certificates
  • Gaming: Adjust house edge, override RNG state, emergency pause, manual payout

MPA is not for routine operations. Don’t require MPA for login, password reset, or everyday actions — you’ll train users to click “approve” without reading.


How It Works

  1. Your app creates an MPA request with M (required approvals) and N groups
  2. Sigil sends a challenge to all approvers’ devices
  3. Approvers see the action details and approve or deny
  4. When M groups approve, the request succeeds
  5. If any group denies, or if time runs out, the request fails

Group-based quorum

MPA works on groups, not individual approvers.

  • Each group has one or more approvers
  • A group is “satisfied” when any one approver in that group approves
  • You need M satisfied groups to proceed

Example:

Required: 2 out of 3 groups

Group 1 (Engineering): Alice, Bob
Group 2 (Security): Carol
Group 3 (Operations): Dave, Eve

Alice approves → Group 1 satisfied
Carol approves → Group 2 satisfied
→ 2 of 3 groups satisfied → MPA approved

Even though Dave and Eve haven’t responded, the request succeeds because 2 groups approved.


Structuring Approval Groups

Good group design balances security and availability.

Anti-pattern: One person per group

Don’t do this:

Required: 2 of 3
Group 1: Alice
Group 2: Bob
Group 3: Carol

If Alice is on vacation, the request can still succeed (Bob + Carol). But if Bob and Carol both deny, the request fails even if Alice would have approved. This creates a veto scenario, not a quorum.

Better:

Required: 2 of 3
Group 1 (Engineering): Alice, Bob
Group 2 (Security): Carol, Dave
Group 3 (Operations): Eve, Frank

Now each group has redundancy. If Alice is unavailable, Bob can approve for the Engineering group.

Role-based groups

Organize groups by role or expertise:

  • Engineering Leads — Approve infrastructure changes
  • Security Team — Approve access grants
  • Finance — Approve large transactions
  • On-Call Rotation — Approve emergency actions

Time-zone considerations

If your team is distributed, ensure each group has approvers in multiple time zones. A request that expires while half your team is asleep is a failed request.

Avoid single points of failure

If Group 1 has only one person, and that person is offline, Group 1 can never be satisfied. Always have at least 2 approvers per group.


Prerequisites

Before creating an MPA request, ensure you have:

  • [ ] Sigil service running — See Self-Hosting Guide for deployment instructions
  • [ ] SDK installed and configured — Node or Go SDK with SIGIL_API_KEY set in your environment
  • [ ] Device fingerprints and public keys — From device registration flow (each approver must have a paired device)
  • [ ] Approval groups defined — Know which users/devices belong to which groups and what your M-of-N threshold should be

If you don’t have device fingerprints yet, you need to implement device registration first. See the Integrator Quickstart for the basic challenge flow — MPA uses the same device registration model.


Creating an MPA Request

Here’s the complete flow using the Node SDK. The Go SDK works the same way.

Example: Cold boot a production server

import { SigilAuth } from '@sigilauth/sdk';

const sigil = new SigilAuth({
  serviceUrl: 'https://sigil.example.com'
});

const mpaRequest = await sigil.mpa.request({
  required: 2,  // Need 2 out of 3 groups
  groups: [
    {
      name: 'Engineering Leads',
      approvers: [
        {
          fingerprint: 'abc123...',  // Alice
          device_public_key: 'Ag8xYz...'
        },
        {
          fingerprint: 'def456...',  // Bob
          device_public_key: 'AhMxNj...'
        }
      ]
    },
    {
      name: 'Security Team',
      approvers: [
        {
          fingerprint: 'ghi789...',  // Carol
          device_public_key: 'AjQyOT...'
        }
      ]
    },
    {
      name: 'Operations',
      approvers: [
        {
          fingerprint: 'jkl012...',  // Dave
          device_public_key: 'AkU3MT...'
        },
        {
          fingerprint: 'mno345...',  // Eve
          device_public_key: 'AlY4Nz...'
        }
      ]
    }
  ],
  action: {
    type: 'cold_boot',
    description: 'Cold boot engine ENG-001',
    params: {
      engine_id: 'ENG-001',
      reason: 'Scheduled maintenance',
      requested_by: 'alice@example.com',
      ticket: 'MAINT-1234'
    }
  },
  expires_in_seconds: 600  // 10 minutes
});

console.log('MPA request created:', mpaRequest.request_id);
console.log('Waiting for', mpaRequest.groups_required, 'of', mpaRequest.groups_total, 'groups');

// Poll for result
const result = await sigil.mpa.awaitResult(mpaRequest.request_id, {
  pollInterval: 3000,  // Check every 3 seconds
  timeout: 600000      // 10 minutes
});

if (result.status === 'approved') {
  console.log('MPA approved by:', result.groups_satisfied);
  // Proceed with cold boot
  await coldBootEngine('ENG-001');
} else if (result.status === 'rejected') {
  console.log('MPA denied by at least one group');
  // Log the denial
} else if (result.status === 'timeout') {
  console.log('MPA timed out before reaching quorum');
  // Escalate or retry
}

What Approvers See

When you create an MPA request, all approvers get a push notification:

“Acme Corp is requesting approval”

They tap it and see:

Acme Corp

Cold boot engine ENG-001

Engine ID: ENG-001
Reason: Scheduled maintenance
Requested by: alice@example.com
Ticket: MAINT-1234

1 of 2 approvals received. Waiting for one more.

[Deny] [Approve with Face ID]

The approval screen shows:

  • What action is being requested (description)
  • Detailed context (params)
  • Current quorum status (how many groups have approved)
  • Their group name (in the fine print)

If they approve, they see:

Approved

Waiting for other approvers.
You'll be notified when the action completes or times out.

If someone else denies, everyone gets a notification:

“MPA request denied”


Handling Results

Success case

When the Mth group approves, status becomes "approved" and groups_satisfied lists the groups that approved.

if (result.status === 'approved') {
  console.log('Approved by:', result.groups_satisfied);
  // ["Engineering Leads", "Security Team"]
  
  // Proceed with the action
  await performSensitiveAction();
}

Denial case

If any approver denies, the request immediately fails. status becomes "rejected".

if (result.status === 'rejected') {
  console.log('At least one group denied the request');
  
  // Log who requested it and why it was denied
  await logDenial(mpaRequest.request_id, action);
  
  // Notify the requester
  await notifyRequester('Your MPA request was denied');
}

You don’t know which approver denied — Sigil only exposes the group-level result.

Timeout case

If the MPA request expires before reaching quorum, status becomes "timeout".

if (result.status === 'timeout') {
  console.log('MPA timed out');
  
  console.log('Groups that approved:', result.groups_satisfied);
  // Might be ["Engineering Leads"] — not enough
  
  // Escalate or retry
  await escalateToOnCall(mpaRequest.request_id);
}

Best Practices

Set appropriate timeouts

The default timeout is 5 minutes (expires_in_seconds: 300). For urgent actions, use a shorter timeout. For planned maintenance, use a longer one.

expires_in_seconds: 60   // 1 minute — urgent production incident
expires_in_seconds: 300  // 5 minutes — default
expires_in_seconds: 900  // 15 minutes — planned change

Don’t go below 60 seconds (minimum) or above 900 seconds (maximum, 15 minutes).

Include enough context

The params object is shown to approvers. Include everything they need to make an informed decision:

params: {
  engine_id: 'ENG-001',
  reason: 'Scheduled maintenance',
  requested_by: 'alice@example.com',
  ticket: 'MAINT-1234',
  estimated_downtime: '30 minutes',
  backup_completed: true
}

Approvers should be able to decide without opening another tab.

Log everything

MPA requests are audit events. Log:

  • Who requested the action
  • What the action was
  • Which groups approved
  • When the request completed
  • Whether the action was actually performed

Store these logs in a tamper-evident system (append-only log, SIEM, etc.).

Use webhooks for async notification

Instead of polling, configure a webhook. When the MPA request completes, Sigil POSTs to your webhook URL:

{
  "type": "mpa.approved",
  "request_id": "7c3e5f9a-2b4d-4a1e-8f6c-9d2a1b3c4e5f",
  "status": "approved",
  "groups_satisfied": ["Engineering Leads", "Security Team"],
  "timestamp": "2026-04-24T10:35:42Z"
}

Your webhook handler performs the action and logs the result.

Don’t block the user

After creating an MPA request, return immediately. Don’t make the user wait for approvals.

// ❌ Bad: blocks the requester
const result = await sigil.mpa.awaitResult(mpaRequest.request_id);
await performAction();
return result;

// ✅ Good: return immediately, use webhook
await sigil.mpa.request({ ... });
return { message: 'MPA request created. You'll be notified when approved.' };

// Webhook handler performs the action when approved

Test your quorum thresholds

If you set required: 3 out of 3 groups, all three must approve. If one person is on vacation and their group has only one approver, the request will always fail.

For critical actions, use required: M where M < N, so you have fault tolerance.

Notify approvers via multiple channels

Push notifications can be missed. Consider sending email or Slack notifications as well:

await sigil.mpa.request({ ... });

// Also notify via email
await sendEmail({
  to: ['alice@example.com', 'bob@example.com', 'carol@example.com'],
  subject: 'MPA approval required: Cold boot engine ENG-001',
  body: 'Check your Sigil Auth app to approve or deny.'
});

Common Patterns

Emergency override

For true emergencies, you might want a “break glass” override that skips MPA. Design this carefully:

if (isEmergency && onCallApproved) {
  // Skip MPA, perform action immediately
  await performAction();
  
  // But still create a post-facto MPA request for audit
  await sigil.mpa.request({
    required: 2,
    groups: [...],
    action: {
      type: 'emergency_override',
      description: 'Emergency action performed without MPA',
      params: { ... }
    }
  });
}

Log every emergency override and review them in your weekly security meeting.

Progressive escalation

If an MPA request times out, escalate to a wider group:

let result = await sigil.mpa.request({
  required: 2,
  groups: [teamLeads, security],
  expires_in_seconds: 300
});

if (result.status === 'timeout') {
  // Escalate to on-call rotation
  result = await sigil.mpa.request({
    required: 1,
    groups: [onCallRotation],
    expires_in_seconds: 300
  });
}

User-initiated MPA

When a user requests a sensitive action on their own account (e.g., “delete my account”), you might want MPA from their other devices:

const userDevices = await getUserDevices(userId);

await sigil.mpa.request({
  required: 2,  // Need 2 of their 3 devices
  groups: userDevices.map(device => ({
    name: device.name,
    approvers: [{ fingerprint: device.fingerprint, device_public_key: device.public_key }]
  })),
  action: {
    type: 'delete_account',
    description: 'Delete your account',
    params: { user_id: userId, email: userEmail }
  }
});

This prevents account takeover via a single compromised device.


Security Considerations

Approver devices are the root of trust

If an attacker compromises an approver’s device and their biometric, they can approve MPA requests. Mitigations:

  • Require device attestation in production (SIGIL_ATTESTATION_REQUIRED=true)
  • Monitor for unusual approval patterns (e.g., same device approving at 3am every night)
  • Rotate approver lists regularly
  • Require approvers to use devices with secure hardware (no emulators)

Denial isn’t anonymous

While Sigil doesn’t expose which individual denied, approvers in the same group can see that their group denied. If a group has only one person, everyone knows who denied.

If you need anonymous denials, use single-person groups (but see “Anti-pattern” above for the downsides).

Timing attacks

If you log “Alice approved at 10:32:15” and “MPA approved at 10:32:17”, you can infer Alice was the second approver. If you need to hide approval timing, batch-process approvals every 30 seconds.

Quorum changes

If you change required from 2 to 3 mid-request, existing requests still use the old value. MPA policies are immutable once created.


Troubleshooting

“Invalid MPA request: required cannot exceed number of groups”

You set required: 4 but only provided 3 groups. required must be ≤ the number of groups.

“Invalid expires_in_seconds: must be between 60 and 900”

Timeout is too short or too long. Use 60–900 seconds (1–15 minutes).

MPA times out even though approvers are online

Check:

  • Are approvers getting push notifications? (check relay logs)
  • Is expires_in_seconds long enough for everyone to respond?
  • Are approvers in different time zones?

Approver approved but group isn’t satisfied

Only one approval per group is needed. If Alice and Bob both approve for the Engineering group, the group is still only satisfied once. You need approvals from different groups.


Next Steps

MPA is a powerful tool for reducing risk. Used well, it prevents single-person compromise and enforces real human oversight on dangerous actions. Used poorly, it’s security theater that trains people to click “approve” without reading. Design your policies carefully.