# AWS backup infrastructure reference

This reference provides the commands, policy JSON, and troubleshooting for the AWS side of Wallet-as-a-Service backups: recovery key generation, the S3 bucket, the IAM role, and the console configuration. For the setup workflow and the decisions behind it, see [Configure backup and recovery](/pt-br/products/wallet/admin-guide/configure-backup-and-recovery); for restoring from backups, see [Disaster recovery](/pt-br/products/wallet/admin-guide/disaster-recovery).

This backup process works with [MPC quorums](/pt-br/products/wallet/user-interface/security-controls/mpc-quorums) to ensure key recovery.

## Architecture overview

Wallet-as-a-Service always writes every backup to a Wallet-as-a-Service-managed S3 bucket. When you configure your own AWS infrastructure (this reference), the Wallet-as-a-Service MPC service also assumes a role in your AWS account to write an additional, independently retrievable copy to your bucket. Wallet-as-a-Service encrypts all copies to your RSA public key before it writes them, so you maintain full ownership and control of your data. Your S3 bucket stores the encrypted backups in a specific folder structure, and AWS IAM roles control access.

```mermaid
flowchart LR
    subgraph palisade["Wallet-as-a-Service Cloud"]
        MPC["MPC Service"]
    end

    subgraph customer["Your AWS Account"]
        IAM["IAM Role<br/>PalisadeS3BackupRole"]
        subgraph s3["S3 Bucket"]
            KMS["KMS Encryption"]
            Shards["Recovery Shards"]
        end
    end

    MPC -->|"1. AssumeRole<br/>(with External ID)"| IAM
    IAM -->|"2. PutObject<br/>(encrypted)"| s3
    KMS -.->|encrypts| Shards
```

### Data structure

```
your-backup-bucket/
├── <key_id_1>/
│   ├── recovery_shard-0.txt
│   ├── recovery_shard-1.txt
│   └── recovery_shard-2.txt
└── <key_id_2>/
    ├── recovery_shard-0.txt
    └── recovery_shard-1.txt
```

Wallet-as-a-Service names each folder after the wallet's key ID (the same UUID as the wallet ID). Despite the name, each `recovery_shard` file is a **complete recovery kit** (base64-encoded JSON) for one of the recovery public keys in your backup kit, not a partial shard. To recover from it, you also need the wallet's quorum ID, which the file doesn't store. If your kit has multiple recovery public keys, match files to keys by the `recoveryPublicKeyHex` field inside each file rather than by the filename index. Wallet-as-a-Service doesn't guarantee that the index stays stable. See [Disaster recovery](/pt-br/products/wallet/admin-guide/disaster-recovery) for the recovery procedure.

## Prerequisites: Generate recovery key pairs

Before setting up your backup strategy, generate RSA-4096 recovery key pairs using either the Wallet-as-a-Service Wallet Recovery CLI (recommended) or OpenSSL.

### Key format requirements

Public key files you upload to Wallet-as-a-Service must be:

- RSA public keys of at least 4096 bits (Wallet-as-a-Service rejects smaller keys)
- DER encoded, in PKIX/SubjectPublicKeyInfo format (the output of `openssl rsa -pubout -outform DER`)
- Either binary DER format OR hex-encoded text (auto-detected)


Format auto-detection
Wallet-as-a-Service automatically detects whether your public key file is binary DER or hex-encoded text and accepts both formats. The UI displays the detected format when you upload a file. The file picker accepts `.der`, `.hex`, and `.pem` file names, but the content must be binary DER or hex. Wallet-as-a-Service rejects PEM content, even in a `.pem` file.

### Option 1: Using the Wallet-as-a-Service Wallet Recovery CLI (recommended)

For key generation, validation, and wallet recovery, use the **Wallet-as-a-Service Wallet Recovery CLI**:

**Repository**: https://github.com/palisadeinc/wallet-recovery-cli

The CLI provides commands for:

- `generate-recovery-keypair`: Generate RSA-4096 recovery key pairs. `--format` selects binary DER (default) or hex output for the public key; the CLI always writes the private key as hex-encoded DER. `--encrypt-private-key` optionally password-protects the private key
- `validate-private-key`: Test that a password can decrypt an encrypted private key file, without modifying it
- `recover`: Recover wallets from encrypted backups
- `decrypt`: Decrypt a private key file that the `recover` command encrypted, or a recovery private key that you encrypted with `generate-recovery-keypair --encrypt-private-key`
- `print-address`: Print the blockchain addresses derived from a recovered private key


See the [wallet-recovery-cli README](https://github.com/palisadeinc/wallet-recovery-cli) for complete documentation.

Critical: Secure your private key
Store your private key securely. Without it, you cannot recover your wallets. Consider using a hardware security module (HSM) or secure offline storage.

### Option 2: Using OpenSSL (manual generation)

If you cannot use the Wallet-as-a-Service CLI, generate keys using OpenSSL:

```bash
# Step 1: Generate an RSA-4096 private key
openssl genrsa -out recovery-private.pem 4096

# Step 2: Extract the public key in binary DER format
openssl rsa -in recovery-private.pem -pubout -outform DER -out recovery-public.der

# Step 3: Convert the private key to hex-encoded DER, the only plain format the recovery CLI reads
openssl rsa -in recovery-private.pem -outform DER | xxd -p | tr -d '\n' > recovery-private.hex
```

Upload `recovery-public.der` (the binary DER file) to the Wallet-as-a-Service UI. The UI auto-detects the format.

Store `recovery-private.hex` offline. The recovery CLI reads the private key either as hex-encoded DER or as a file that its own `generate-recovery-keypair --encrypt-private-key` option produced. Don't encrypt the key with OpenSSL: the CLI can't read an OpenSSL-encrypted key, and `validate-private-key` reports it as invalid.

Optional hex encoding
You can optionally convert to hex-encoded format:

```bash
xxd -p recovery-public.der | tr -d '\n' > recovery-public.hex
```

### Verify your key files

Before uploading, verify your public key file is in the correct format:

```bash
# Check file size
wc -c recovery-public.der
# Expected output for binary DER: 550 recovery-public.der
wc -c recovery-public.hex
# Expected output for hex-encoded: 1100 recovery-public.hex

# Check file type
file recovery-public.der
# Binary DER output: recovery-public.der: data
file recovery-public.hex
# Hex-encoded output: recovery-public.hex: ASCII text, with very long lines (1100), with no line terminators
# (Older versions of file omit the "(1100)" line length.)

# For binary DER, check it starts with ASN.1 SEQUENCE tag (0x30)
xxd recovery-public.der | head -1
# Expected: 00000000: 3082 0222 300d 0609 2a86 4886 f70d 0101  0.."0...*.H.....

# For hex-encoded, check content starts correctly
head -c 48 recovery-public.hex
# Expected: 30820222300d06092a864886f70d01010105000382020f00
```

## Step 1: Create the S3 bucket

1. Create a new S3 bucket with a descriptive name (e.g., `company-palisade-wallet-backups`).
2. Enable versioning to protect against accidental overwrites.
3. Create a KMS Customer Managed Key (CMK) for encryption, or use an existing one.
4. Enable default encryption with AWS KMS (SSE-KMS). With the AWS CLI, pass this configuration to `aws s3api put-bucket-encryption --server-side-encryption-configuration`:


```json
{
  "Rules": [
    {
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "aws:kms",
        "KMSMasterKeyID": "arn:aws:kms:REGION:ACCOUNT:key/KEY-ID"
      },
      "BucketKeyEnabled": true
    }
  ]
}
```

1. Set bucket ownership and block public access:
  - Enable "Block Public Access" for this bucket
  - Set Object ownership to "Bucket owner enforced" to disable ACLs. Wallet-as-a-Service uploads set the `bucket-owner-full-control` canned ACL, which is the one ACL this setting accepts, so uploads remain compatible.
2. Add a bucket policy to enforce TLS, KMS encryption, and block public access:


```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::your-backup-bucket",
        "arn:aws:s3:::your-backup-bucket/*"
      ],
      "Condition": { "Bool": { "aws:SecureTransport": "false" } }
    },
    {
      "Sid": "DenyUnencryptedObjectUploads",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::your-backup-bucket/*",
      "Condition": {
        "StringNotEquals": { "s3:x-amz-server-side-encryption": "aws:kms" }
      }
    },
    {
      "Sid": "DenyWrongKmsKey",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::your-backup-bucket/*",
      "Condition": {
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption-aws-kms-key-id": "arn:aws:kms:REGION:ACCOUNT:key/KEY-ID"
        }
      }
    }
  ]
}
```

1. Configure lifecycle policies for backup retention. With the AWS CLI, pass this configuration to `aws s3api put-bucket-lifecycle-configuration --lifecycle-configuration`:


```json
{
  "Rules": [
    {
      "ID": "RetainBackupsAndVersions",
      "Status": "Enabled",
      "Filter": {},
      "NoncurrentVersionExpiration": {
        "NoncurrentDays": 90
      },
      "AbortIncompleteMultipartUpload": {
        "DaysAfterInitiation": 7
      }
    }
  ]
}
```

## Step 2: Create the IAM role for S3 access

Create an IAM role named `PalisadeS3BackupRole` (or similar) with the following configuration:

### Permission policy

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowS3BackupWrites",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::your-backup-bucket/*",
      "Condition": {
        "StringEquals": {
          "s3:x-amz-server-side-encryption": "aws:kms",
          "s3:x-amz-server-side-encryption-aws-kms-key-id": "arn:aws:kms:REGION:ACCOUNT:key/KEY-ID"
        }
      }
    },
    {
      "Sid": "AllowPreflightCleanup",
      "Effect": "Allow",
      "Action": [
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::your-backup-bucket/.palisade-preflight-check/*"
    },
    {
      "Sid": "AllowKMSEncryption",
      "Effect": "Allow",
      "Action": [
        "kms:GenerateDataKey"
      ],
      "Resource": "arn:aws:kms:REGION:ACCOUNT:key/KEY-ID"
    }
  ]
}
```

These are the only actions the Wallet-as-a-Service MPC service performs against your account: it assumes the role, calls `sts:GetCallerIdentity` to verify the assumed session (this call needs no IAM permission, so the policy doesn't grant one), writes each backup with a single `s3:PutObject` (SSE-KMS encrypted when you configure a KMS key), and deletes only its own pre-flight test object. It never lists or reads your bucket, and it doesn't use multipart uploads.

KMS permissions required
The role needs the `AllowKMSEncryption` statement to encrypt objects with your KMS key. `kms:GenerateDataKey` is the only KMS action S3 needs for SSE-KMS uploads.

### Trust relationship policy

#### For sandbox environment (app.sandbox.palisade.co)

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::187316130931:user/mpc-service"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

#### For production environment (app.palisade.co)

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::663932549156:user/mpc-service"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

### Enhanced security with External ID (optional)

For additional security, require an External ID in your trust policy. This prevents the "confused deputy" problem: every customer's backups use the same Wallet-as-a-Service MPC service identity, so without an External ID, another Wallet-as-a-Service customer who learns your role ARN could point their backup kit at your role. You choose the External ID (up to 256 characters). Set one for every kit, using a long, random value that you keep private.

When you configure an External ID in the Wallet-as-a-Service UI, the MPC service includes it when assuming your role. Your trust policy can then require this specific External ID:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::663932549156:user/mpc-service"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "your-unique-external-id"
        }
      }
    }
  ]
}
```

External ID matching
The External ID you configure in the Wallet-as-a-Service UI must exactly match the value in your trust policy's `sts:ExternalId` condition.

## Step 3: Configure in Wallet-as-a-Service UI

After creating your S3 bucket and IAM role, configure the backup strategy in the Wallet-as-a-Service dashboard.

Go to **Settings → Backup & Recovery → Manage backups**, select **Create backup**, and enter the following:

| Field | What to enter | Example |
|  --- | --- | --- |
| **AWS ARN** | The ARN of the IAM role you created (NOT the bucket ARN). The console accepts only `arn:aws:iam::<12-digit-account>:role/<name>`. It rejects role names with a path (such as `role/service-role/<name>`) and non-`aws` partitions (GovCloud, China). | `arn:aws:iam::123456789012:role/PalisadeS3BackupRole` |
| **AWS Bucket name** | Only the bucket name (no ARN, no s3:// prefix). The console accepts lowercase letters, numbers, and hyphens only, and it rejects bucket names that contain dots. | `company-palisade-wallet-backups` |
| **AWS Region** | The AWS region where your bucket is located (select from the list). | `eu-west-1` |
| **External ID (optional)** | A unique identifier for role assumption security. Recommended (see [Enhanced security with External ID](#enhanced-security-with-external-id-optional)). | `palisade-backup-2024-abc123` |
| **KMS Key ARN (optional)** | The KMS key ARN for server-side encryption. Required if you use the IAM permission policy above. | `arn:aws:kms:eu-west-1:123456789012:key/12345678-1234-1234-1234-123456789012` |


Common mistake
Do not enter the S3 bucket ARN in the **AWS ARN** field. The system expects an IAM Role ARN in the format `arn:aws:iam::<account-id>:role/<role-name>`.

### Pre-flight validation

When you save your backup configuration, Wallet-as-a-Service automatically validates that:

1. The MPC service can assume the specified IAM role
2. The role has permission to write to the S3 bucket
3. If an External ID is configured, it's accepted by the trust policy
4. If a KMS Key ARN is configured, the role can use it for encryption


This validation happens **before** Wallet-as-a-Service provisions any wallets, ensuring you discover configuration issues immediately rather than during wallet creation.

To validate, Wallet-as-a-Service writes a small test object to `.palisade-preflight-check/<uuid>` in your bucket and then deletes it. This has two consequences:

- If your bucket policy restricts writes to specific key prefixes, allow the `.palisade-preflight-check/` prefix as well as the backup paths.
- If the role lacks `s3:DeleteObject` on that prefix, validation still succeeds but Wallet-as-a-Service leaves the test object behind. Orphaned `.palisade-preflight-check/` objects in your bucket are harmless leftovers from validation runs.
- Because the bucket has versioning enabled (Step 1), the delete only adds a delete marker. The test object stays in the bucket as a noncurrent version until your lifecycle rule expires it (after 90 days with the rule in Step 1).


This setup requires the KMS Key ARN
Wallet-as-a-Service sends the SSE-KMS encryption headers only when you configure a KMS Key ARN in the UI. The IAM permission policy above allows `s3:PutObject` only with those headers, and the `DenyUnencryptedObjectUploads` bucket policy denies uploads without them. If you use either policy, the KMS Key ARN field is **required**. Without it, AWS denies every upload (including pre-flight validation).

### ARN format reference

| Type | Format | Example |
|  --- | --- | --- |
| IAM Role ARN (correct) | `arn:aws:iam::<account>:role/<name>` | `arn:aws:iam::123456789012:role/PalisadeS3BackupRole` |
| S3 Bucket ARN (incorrect) | `arn:aws:s3:::<bucket>` | `arn:aws:s3:::my-bucket` |
| KMS Key ARN | `arn:aws:kms:<region>:<account>:key/<key-id>` | `arn:aws:kms:eu-west-1:123456789012:key/...` |


## Step 4: Advanced security configuration (optional)

### Enable CloudTrail logging

```json
{
  "EventSelectors": [
    {
      "ReadWriteType": "All",
      "IncludeManagementEvents": false,
      "DataResources": [
        {
          "Type": "AWS::S3::Object",
          "Values": ["arn:aws:s3:::your-backup-bucket/*"]
        }
      ]
    }
  ]
}
```

### Configure S3 Object Lock (for immutable backups)

```json
{
  "ObjectLockEnabled": "Enabled",
  "Rule": {
    "DefaultRetention": {
      "Mode": "COMPLIANCE",
      "Days": 30
    }
  }
}
```

## Security best practices checklist

- [ ] **Encryption at rest**: Default SSE-KMS with your CMK enforced at bucket and IAM policy
- [ ] **Encryption in transit**: Bucket policy enforces SSL/TLS connections
- [ ] **Access control**: IAM role with minimal required permissions
- [ ] **KMS permissions**: Role has `kms:GenerateDataKey` on your CMK
- [ ] **External ID**: Configure an External ID for enhanced role assumption security
- [ ] **Versioning**: Enabled to protect against accidental overwrites
- [ ] **Logging**: CloudTrail and S3 access logging enabled
- [ ] **Monitoring**: CloudWatch alarms configured
- [ ] **Backup retention**: Lifecycle policies configured, and no rules that could transition or expire recovery data you still need
- [ ] **MFA Delete**: Consider enabling for production buckets
- [ ] **Post-setup hardening**: Only pre-flight validation uses the `s3:DeleteObject` permission, and it runs when you save the kit configuration. After you create the kit and it passes validation, you can remove the `AllowPreflightCleanup` statement from the role for further hardening.


## Troubleshooting

### Access denied errors

| Cause | Solution |
|  --- | --- |
| Trust relationship missing Ripple account | Add correct Ripple account ID to trust policy |
| S3 bucket name mismatch | Ensure bucket name in policies matches exactly |
| Missing KMS permissions | Add required KMS permissions to IAM role |
| External ID mismatch | Verify External ID matches between UI and trust policy |


### Encryption errors

| Cause | Solution |
|  --- | --- |
| SSE not enabled on bucket | Enable server-side encryption on the bucket |
| Bucket policy missing encryption requirement | Add policy requiring encrypted uploads |
| Missing KMS permissions | Add `kms:GenerateDataKey` to IAM role |
| KMS Key ARN mismatch | Ensure KMS Key ARN in UI matches bucket policy |


### Role assumption failures

| Cause | Solution |
|  --- | --- |
| Wrong principal ARN | Verify principal matches environment (Sandbox vs Production) |
| External ID not configured | Configure External ID in both Wallet-as-a-Service UI and trust policy |
| Unknown error | Check CloudTrail logs for detailed error messages |


### Invalid ARN format in Wallet-as-a-Service UI

| Cause | Solution |
|  --- | --- |
| Entered S3 bucket ARN instead of IAM role ARN | Use IAM Role ARN format: `arn:aws:iam::<account-id>:role/<name>` |
| Invalid characters in role name | Use only alphanumeric characters and `+=,.@_-` |


### Pre-flight validation failures

If pre-flight validation fails, the console reports a single generic error, `S3 access validation failed`, whatever the cause. Check each of these causes, and use CloudTrail in your account to see which call AWS denied:

| Cause | Solution |
|  --- | --- |
| Trust policy doesn't allow the Wallet-as-a-Service MPC service | Check the trust policy principal ARN for your environment |
| External ID in the UI doesn't match the trust policy | Ensure the External IDs match exactly |
| Role lacks S3 permissions | Check the IAM role permission policy |
| KMS Key ARN left blank while the IAM or bucket policy requires SSE-KMS | Enter the KMS Key ARN in the UI |
| Role lacks KMS permissions | Add `kms:GenerateDataKey` to the role |


### Public key validation errors

#### Invalid format or file rejected on upload

The UI validates public key files on upload. Common causes of rejection:

| Cause | Solution |
|  --- | --- |
| File is not a valid DER-encoded public key | Ensure the file is RSA-4096 in DER format (PKIX/SubjectPublicKeyInfo) |
| File is PEM format with headers | Convert it to DER: `openssl pkey -pubin -in public.pem -outform DER -out public.der`. Removing the `-----BEGIN PUBLIC KEY-----` headers isn't enough, because the UI rejects the remaining base64 |
| File contains invalid characters | For hex-encoded files, ensure only hex characters (0-9, a-f) |


#### Key too small or key rejected after upload

This error means your public key is smaller than RSA-4096. Common causes:

| Cause | Solution |
|  --- | --- |
| Used RSA-2048 or smaller key size | Generate RSA-4096 keys |
| File was truncated during transfer | Re-generate or re-download the key file |


#### Quick diagnostic

Run this command to check your key file:

```bash
file your-public-key.der && wc -c your-public-key.der
```

Expected output for valid RSA-4096 keys:

```
# Binary DER format (550 bytes):
your-public-key.der: data
550 your-public-key.der

# Hex-encoded format (1100 characters), for a file named your-public-key.hex.
# Older versions of file omit the "(1100)" line length:
your-public-key.hex: ASCII text, with very long lines (1100), with no line terminators
1100 your-public-key.hex
```

## Wallet recovery

This reference covers only the storage side. For the step-by-step recovery procedure, including how to recover your assets independently of Ripple, see the [Disaster recovery](/pt-br/products/wallet/admin-guide/disaster-recovery) guide and the [Wallet-as-a-Service Wallet Recovery CLI](https://github.com/palisadeinc/wallet-recovery-cli) documentation.

## Appendix: Quick reference

### Ripple AWS account IDs

| Environment | Account ID | MPC Service Principal |
|  --- | --- | --- |
| Sandbox | 187316130931 | `arn:aws:iam::187316130931:user/mpc-service` |
| Production | 663932549156 | `arn:aws:iam::663932549156:user/mpc-service` |