mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-08-10 12:23:20 +03:00
Add 30 new production-grade cybersecurity skills: AI security, supply chain, firmware, cloud-native, compliance, deception, crypto, threat hunting, purple team, OT, privacy
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to the Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by the Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding any notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. Please do not remove or change
|
||||
the license header comment from a contributed file except when
|
||||
necessary.
|
||||
|
||||
Copyright 2026 mukul975
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,444 @@
|
||||
---
|
||||
name: implementing-aws-nitro-enclave-security
|
||||
description: >
|
||||
Implements AWS Nitro Enclave-based confidential computing environments with cryptographic attestation,
|
||||
KMS policy integration using PCR-based condition keys, and secure vsock communication channels. The
|
||||
practitioner builds enclave images, configures attestation-aware KMS policies, validates attestation
|
||||
documents against the AWS Nitro PKI root of trust, and establishes isolated computation pipelines
|
||||
for processing sensitive data such as PII, cryptographic keys, and healthcare records. Activates for
|
||||
requests involving Nitro Enclave setup, enclave attestation validation, confidential computing on AWS,
|
||||
or KMS enclave policy configuration.
|
||||
domain: cybersecurity
|
||||
subdomain: cloud-security
|
||||
tags: [AWS-Nitro-Enclaves, confidential-computing, attestation, KMS, enclave-isolation, vsock, PCR]
|
||||
version: 1.0.0
|
||||
author: mukul975
|
||||
license: Apache-2.0
|
||||
---
|
||||
# Implementing AWS Nitro Enclave Security
|
||||
|
||||
## When to Use
|
||||
|
||||
- Processing sensitive data (PII, PHI, financial records, cryptographic secrets) that must be isolated from EC2 instance operators and administrators
|
||||
- Building confidential computing pipelines where even root-level access on the parent instance cannot read enclave memory or state
|
||||
- Implementing cryptographic attestation workflows that tie KMS decryption rights to a specific, verified enclave image hash
|
||||
- Deploying multi-party computation environments where two or more enclaves authenticate each other via attestation before exchanging data
|
||||
- Hardening existing workloads that currently decrypt secrets on the parent instance by migrating decryption into an enclave boundary
|
||||
|
||||
**Do not use** when the workload does not handle sensitive data that requires hardware-level isolation, when the instance type does not support Nitro Enclaves (requires Nitro-based instances with at least 4 vCPUs), or when latency constraints make the vsock communication overhead unacceptable.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An AWS account with permissions to launch Nitro-capable EC2 instances (m5.xlarge or larger, C5, R5, M6i families)
|
||||
- AWS CLI v2 and the `nitro-cli` toolset installed on the parent EC2 instance (Amazon Linux 2 or AL2023)
|
||||
- Docker installed on the parent instance for building enclave image files (EIF)
|
||||
- An AWS KMS symmetric key with key policy permissions for the enclave's IAM role
|
||||
- The `aws-nitro-enclaves-sdk-c` or Python `aws-encryption-sdk` for enclave-side KMS operations
|
||||
- The Nitro Enclaves allocator service configured with sufficient memory and vCPU allocation in `/etc/nitro_enclaves/allocator.yaml`
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Configure the Nitro Enclaves Environment
|
||||
|
||||
Set up the parent EC2 instance to support enclave launches:
|
||||
|
||||
- **Install the Nitro Enclaves CLI**: On Amazon Linux 2, install the tools and allocator:
|
||||
```bash
|
||||
sudo amazon-linux-extras install aws-nitro-enclaves-cli
|
||||
sudo yum install aws-nitro-enclaves-cli-devel -y
|
||||
sudo systemctl enable --now nitro-enclaves-allocator.service
|
||||
sudo systemctl enable --now docker
|
||||
sudo usermod -aG ne ec2-user
|
||||
sudo usermod -aG docker ec2-user
|
||||
```
|
||||
- **Configure memory and CPU allocation**: Edit `/etc/nitro_enclaves/allocator.yaml` to reserve resources for the enclave. The enclave requires dedicated memory that is carved from the parent instance:
|
||||
```yaml
|
||||
---
|
||||
memory_mib: 4096
|
||||
cpu_count: 2
|
||||
```
|
||||
Restart the allocator: `sudo systemctl restart nitro-enclaves-allocator.service`
|
||||
- **Verify setup**: Run `nitro-cli describe-enclaves` to confirm the CLI can communicate with the Nitro hypervisor. An empty JSON array `[]` indicates no enclaves are running and the setup is correct.
|
||||
|
||||
### Step 2: Build the Enclave Image File (EIF)
|
||||
|
||||
Package the sensitive workload into a signed enclave image:
|
||||
|
||||
- **Create the application Dockerfile**: The enclave runs a minimal Linux environment. The application communicates exclusively through vsock:
|
||||
```dockerfile
|
||||
FROM amazonlinux:2
|
||||
|
||||
RUN yum install -y python3 python3-pip && \
|
||||
pip3 install boto3 cbor2 cryptography requests
|
||||
|
||||
COPY enclave_app.py /app/enclave_app.py
|
||||
|
||||
WORKDIR /app
|
||||
CMD ["python3", "enclave_app.py"]
|
||||
```
|
||||
- **Build the EIF with nitro-cli**: Convert the Docker image into an enclave image file, capturing the PCR measurements:
|
||||
```bash
|
||||
docker build -t enclave-app:latest .
|
||||
nitro-cli build-enclave \
|
||||
--docker-uri enclave-app:latest \
|
||||
--output-file enclave-app.eif
|
||||
```
|
||||
The output contains three critical PCR values:
|
||||
- **PCR0**: SHA-384 hash of the enclave image file (the full image digest)
|
||||
- **PCR1**: SHA-384 hash of the Linux kernel and bootstrap process
|
||||
- **PCR2**: SHA-384 hash of the application code
|
||||
Record these values; they are used in KMS key policies for attestation-based access control.
|
||||
|
||||
- **Build a signed EIF** (recommended for production): Generate a signing certificate and use it to produce PCR8:
|
||||
```bash
|
||||
openssl ecparam -name secp384r1 -genkey -noout -out enclave_key.pem
|
||||
openssl req -new -key enclave_key.pem -sha384 \
|
||||
-nodes -subj "/CN=Enclave Signer" -out enclave_csr.pem
|
||||
openssl x509 -req -days 365 -in enclave_csr.pem \
|
||||
-signkey enclave_key.pem -sha384 -out enclave_cert.pem
|
||||
|
||||
nitro-cli build-enclave \
|
||||
--docker-uri enclave-app:latest \
|
||||
--output-file enclave-app.eif \
|
||||
--private-key enclave_key.pem \
|
||||
--signing-certificate enclave_cert.pem
|
||||
```
|
||||
PCR8 (the signing certificate hash) enables KMS policies that trust any image signed by a specific certificate, allowing image updates without changing the policy.
|
||||
|
||||
### Step 3: Configure KMS Attestation-Based Key Policies
|
||||
|
||||
Create a KMS key policy that restricts decryption to a verified enclave:
|
||||
|
||||
- **Policy using PCR0 (image hash)**: This locks the key to a specific enclave build. Any code change produces a new PCR0, requiring a policy update:
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "AllowEnclaveDecrypt",
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": "arn:aws:iam::111122223333:role/EnclaveParentRole"
|
||||
},
|
||||
"Action": [
|
||||
"kms:Decrypt",
|
||||
"kms:GenerateDataKey"
|
||||
],
|
||||
"Resource": "*",
|
||||
"Condition": {
|
||||
"StringEqualsIgnoreCase": {
|
||||
"kms:RecipientAttestation:ImageSha384": "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
- **Policy using PCR8 (signing certificate)**: Trusts any enclave signed with a specific certificate, enabling image rotation without policy changes:
|
||||
```json
|
||||
{
|
||||
"Condition": {
|
||||
"StringEqualsIgnoreCase": {
|
||||
"kms:RecipientAttestation:PCR8": "ab3456789012345678901234567890123456789012345678901234567890123456789012345678901234567890abcdef"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Multi-PCR policy for defense in depth**: Combine PCR0 (image) and PCR1 (kernel) to ensure both the application and the boot environment match expected values:
|
||||
```json
|
||||
{
|
||||
"Condition": {
|
||||
"StringEqualsIgnoreCase": {
|
||||
"kms:RecipientAttestation:PCR0": "<pcr0-hex>",
|
||||
"kms:RecipientAttestation:PCR1": "<pcr1-hex>"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- **IAM role policy**: The parent instance's IAM role must have `kms:Decrypt` permission, but the KMS key policy condition ensures the actual decryption only succeeds when the request originates from a valid enclave with the correct attestation document attached.
|
||||
|
||||
### Step 4: Implement Secure Vsock Communication
|
||||
|
||||
Establish the parent-to-enclave communication channel:
|
||||
|
||||
- **Vsock architecture**: The only way an enclave communicates with the outside world is through a vsock (virtual socket). Vsock uses a CID (Context Identifier) and port number. The parent instance CID is always `3`, and the enclave CID is assigned at launch.
|
||||
- **Parent-side proxy server**: The parent runs a proxy that forwards KMS API calls from the enclave through the vsock to the AWS KMS endpoint:
|
||||
```python
|
||||
import socket
|
||||
import json
|
||||
import boto3
|
||||
|
||||
VSOCK_CID = 3 # Parent CID
|
||||
VSOCK_PORT = 5000
|
||||
|
||||
def start_proxy():
|
||||
sock = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM)
|
||||
sock.bind((VSOCK_CID, VSOCK_PORT))
|
||||
sock.listen(5)
|
||||
|
||||
kms_client = boto3.client('kms', region_name='us-east-1')
|
||||
|
||||
while True:
|
||||
conn, addr = sock.accept()
|
||||
data = conn.recv(65536)
|
||||
request = json.loads(data.decode())
|
||||
|
||||
if request['action'] == 'decrypt':
|
||||
response = kms_client.decrypt(
|
||||
CiphertextBlob=bytes.fromhex(request['ciphertext']),
|
||||
Recipient={
|
||||
'KeyEncryptionAlgorithm': 'RSAES_OAEP_SHA_256',
|
||||
'AttestationDocument': bytes.fromhex(request['attestation_doc'])
|
||||
}
|
||||
)
|
||||
conn.sendall(json.dumps({
|
||||
'ciphertext_for_recipient': response['CiphertextForRecipient'].hex()
|
||||
}).encode())
|
||||
conn.close()
|
||||
```
|
||||
- **Enclave-side client**: The enclave application requests an attestation document from the Nitro Security Module (NSM) device at `/dev/nsm`, attaches it to KMS decrypt requests, and receives data encrypted to the enclave's ephemeral public key:
|
||||
```python
|
||||
import socket
|
||||
import json
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa, padding
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
|
||||
PARENT_CID = 3
|
||||
VSOCK_PORT = 5000
|
||||
|
||||
def get_attestation_document(public_key_der):
|
||||
"""Request attestation document from NSM device."""
|
||||
# Uses the aws-nitro-enclaves-nsm-api
|
||||
# NSM provides: module_id, digest (SHA384), timestamp, PCRs,
|
||||
# certificate (from Nitro PKI), cabundle, public_key, user_data, nonce
|
||||
import nsm_util
|
||||
nsm_fd = nsm_util.nsm_lib_init()
|
||||
attestation_doc = nsm_util.nsm_get_attestation_doc(
|
||||
nsm_fd,
|
||||
public_key=public_key_der,
|
||||
user_data=None,
|
||||
nonce=None
|
||||
)
|
||||
return attestation_doc
|
||||
|
||||
def decrypt_via_parent(ciphertext_hex):
|
||||
"""Send decrypt request through vsock to parent proxy."""
|
||||
private_key = rsa.generate_private_key(
|
||||
public_exponent=65537, key_size=2048
|
||||
)
|
||||
public_key_der = private_key.public_key().public_bytes(
|
||||
serialization.Encoding.DER,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
|
||||
attestation_doc = get_attestation_document(public_key_der)
|
||||
|
||||
sock = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM)
|
||||
sock.connect((PARENT_CID, VSOCK_PORT))
|
||||
sock.sendall(json.dumps({
|
||||
'action': 'decrypt',
|
||||
'ciphertext': ciphertext_hex,
|
||||
'attestation_doc': attestation_doc.hex()
|
||||
}).encode())
|
||||
|
||||
response = json.loads(sock.recv(65536).decode())
|
||||
sock.close()
|
||||
|
||||
# KMS encrypted the plaintext to the enclave's public key
|
||||
# Only the enclave's private key can decrypt it
|
||||
ciphertext_for_recipient = bytes.fromhex(
|
||||
response['ciphertext_for_recipient']
|
||||
)
|
||||
plaintext = private_key.decrypt(
|
||||
ciphertext_for_recipient,
|
||||
padding.OAEP(
|
||||
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
||||
algorithm=hashes.SHA256(),
|
||||
label=None
|
||||
)
|
||||
)
|
||||
return plaintext
|
||||
```
|
||||
|
||||
### Step 5: Validate Attestation Documents
|
||||
|
||||
Verify attestation documents from enclaves to establish trust:
|
||||
|
||||
- **Attestation document structure**: The document is CBOR-encoded and COSE-signed (COSE_Sign1). It contains:
|
||||
- `module_id`: Identifier for the NSM module
|
||||
- `digest`: Hashing algorithm (SHA-384)
|
||||
- `timestamp`: Unix epoch milliseconds when the document was created
|
||||
- `pcrs`: Map of PCR index to measurement value (PCR0-PCR15)
|
||||
- `certificate`: The NSM's x509 certificate, signed by the Nitro PKI
|
||||
- `cabundle`: Certificate chain from the NSM certificate to the AWS Nitro root CA
|
||||
- `public_key`: The enclave's ephemeral public key (provided at attestation request time)
|
||||
- `user_data`: Optional application-defined data (up to 512 bytes)
|
||||
- `nonce`: Optional nonce for freshness verification
|
||||
|
||||
- **Validation steps**:
|
||||
1. Decode the COSE_Sign1 structure and extract the payload and certificate
|
||||
2. Verify the COSE signature using the public key from the embedded certificate
|
||||
3. Validate the certificate chain from the NSM certificate through the CA bundle to the AWS Nitro Attestation PKI root certificate (available at `https://aws-nitro-enclaves.amazonaws.com/AWS_NitroEnclaves_Root-G1.zip`)
|
||||
4. Check that the root CA certificate matches the expected AWS root: `aws.nitro-enclaves` CN
|
||||
5. Verify that no certificate in the chain is expired at the document's timestamp
|
||||
6. Compare PCR0, PCR1, PCR2 values against expected measurements from the enclave build output
|
||||
7. If a nonce was provided, verify it matches to prevent replay attacks
|
||||
|
||||
- **Attestation validation code**:
|
||||
```python
|
||||
import cbor2
|
||||
from cose import CoseMessage
|
||||
from cryptography import x509
|
||||
from cryptography.x509.oid import NameOID
|
||||
|
||||
def validate_attestation(attestation_bytes, expected_pcrs, expected_nonce=None):
|
||||
cose_msg = CoseMessage.decode(attestation_bytes)
|
||||
payload = cbor2.loads(cose_msg.payload)
|
||||
|
||||
# Verify certificate chain
|
||||
cert = x509.load_der_x509_certificate(payload['certificate'])
|
||||
cabundle = [x509.load_der_x509_certificate(c) for c in payload['cabundle']]
|
||||
|
||||
# Check root CA is AWS Nitro
|
||||
root = cabundle[-1]
|
||||
cn = root.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
|
||||
assert cn == 'aws.nitro-enclaves', f'Unexpected root CA: {cn}'
|
||||
|
||||
# Verify PCR measurements
|
||||
pcrs = payload['pcrs']
|
||||
for idx, expected_value in expected_pcrs.items():
|
||||
actual = pcrs.get(idx, b'').hex()
|
||||
assert actual == expected_value, f'PCR{idx} mismatch: {actual}'
|
||||
|
||||
# Verify nonce freshness
|
||||
if expected_nonce:
|
||||
assert payload.get('nonce') == expected_nonce, 'Nonce mismatch'
|
||||
|
||||
return payload
|
||||
```
|
||||
|
||||
### Step 6: Launch and Monitor the Enclave
|
||||
|
||||
Run the enclave and implement operational monitoring:
|
||||
|
||||
- **Launch the enclave**:
|
||||
```bash
|
||||
nitro-cli run-enclave \
|
||||
--eif-path enclave-app.eif \
|
||||
--cpu-count 2 \
|
||||
--memory 4096 \
|
||||
--enclave-cid 16 \
|
||||
--debug-mode
|
||||
```
|
||||
Note: `--debug-mode` enables the enclave console for development. Remove it in production as it allows reading enclave output, which breaks the isolation guarantee.
|
||||
|
||||
- **Verify enclave status**:
|
||||
```bash
|
||||
nitro-cli describe-enclaves
|
||||
```
|
||||
Expected output includes `"State": "RUNNING"`, the assigned `EnclaveCID`, memory, CPU count, and enclave flags.
|
||||
|
||||
- **Read enclave console** (debug mode only):
|
||||
```bash
|
||||
nitro-cli console --enclave-id <enclave-id>
|
||||
```
|
||||
|
||||
- **Terminate the enclave**:
|
||||
```bash
|
||||
nitro-cli terminate-enclave --enclave-id <enclave-id>
|
||||
```
|
||||
|
||||
- **CloudWatch monitoring**: Configure the parent instance to report enclave health metrics. Since the enclave has no network access, health checks must go through the vsock proxy:
|
||||
```python
|
||||
# Parent-side health check over vsock
|
||||
def check_enclave_health(enclave_cid, port=5001):
|
||||
try:
|
||||
sock = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM)
|
||||
sock.settimeout(5)
|
||||
sock.connect((enclave_cid, port))
|
||||
sock.sendall(b'HEALTH_CHECK')
|
||||
response = sock.recv(1024)
|
||||
sock.close()
|
||||
return response == b'OK'
|
||||
except (socket.timeout, ConnectionRefusedError):
|
||||
return False
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
| Term | Definition |
|
||||
|------|------------|
|
||||
| **Nitro Enclave** | An isolated virtual machine created by the Nitro Hypervisor on a Nitro-based EC2 instance with no persistent storage, no network access, and no interactive access, even from the parent instance's root user |
|
||||
| **Attestation Document** | A CBOR-encoded, COSE-signed document generated by the Nitro Security Module containing PCR measurements, a certificate chain to the AWS Nitro root CA, and optional user-provided data |
|
||||
| **PCR (Platform Configuration Register)** | SHA-384 hash measurements that uniquely identify an enclave's image (PCR0), kernel/bootstrap (PCR1), application (PCR2), IAM role (PCR4), instance ID (PCR3), and signing certificate (PCR8) |
|
||||
| **Vsock** | A virtual socket providing the sole communication channel between a parent EC2 instance and its enclave, using CID (Context Identifier) and port addressing |
|
||||
| **EIF (Enclave Image File)** | The packaged enclave image built by nitro-cli from a Docker image, containing the kernel, ramdisk, and application, producing PCR measurements at build time |
|
||||
| **Nitro Security Module (NSM)** | A custom Linux device (`/dev/nsm`) inside the enclave that provides attestation document generation and hardware random number generation |
|
||||
| **COSE_Sign1** | CBOR Object Signing and Encryption single-signer structure used to sign the attestation document with the NSM's private key |
|
||||
| **kms:RecipientAttestation** | AWS KMS condition key prefix that enables key policies to enforce that decrypt/generate operations only succeed when a valid attestation document with matching PCR values is presented |
|
||||
|
||||
## Tools & Systems
|
||||
|
||||
- **nitro-cli**: AWS CLI tool for building enclave image files, launching/terminating enclaves, and reading enclave console output
|
||||
- **AWS KMS**: Key Management Service that natively supports attestation-based condition keys for Nitro Enclaves, encrypting responses to the enclave's ephemeral public key
|
||||
- **aws-nitro-enclaves-sdk-c**: C SDK for enclave-side KMS operations that handles attestation document generation and vsock proxy communication
|
||||
- **kmstool-enclave-cli**: Pre-built CLI tool (from the SDK) that runs inside the enclave to perform KMS Decrypt and GenerateRandom operations with attestation
|
||||
- **Nitro Enclaves ACM**: AWS Certificate Manager integration that provisions TLS certificates inside enclaves for establishing HTTPS endpoints
|
||||
- **CloudTrail**: Logs KMS API calls including `Decrypt` and `GenerateDataKey` operations that include `Recipient` parameters, enabling auditing of enclave-originated cryptographic operations
|
||||
|
||||
## Common Scenarios
|
||||
|
||||
### Scenario: Implementing a PII Tokenization Service in a Nitro Enclave
|
||||
|
||||
**Context**: A healthcare SaaS company processes patient records containing PHI. Regulations require that the decryption and tokenization of PHI never occurs on an instance accessible to operators. The company deploys a Nitro Enclave that receives encrypted patient records, decrypts them inside the enclave using KMS with attestation, tokenizes the PII fields, and returns only the tokenized records through the vsock.
|
||||
|
||||
**Approach**:
|
||||
1. Build the tokenization application into a Docker image containing the tokenization logic, the `kmstool-enclave-cli` binary, and a vsock server that accepts encrypted records
|
||||
2. Build the EIF with `nitro-cli build-enclave` and record PCR0, PCR1, PCR2 from the build output
|
||||
3. Create a KMS key with a key policy that includes a `kms:RecipientAttestation:ImageSha384` condition matching PCR0, allowing only this specific enclave build to decrypt patient records
|
||||
4. Deploy the parent instance with an IAM role that has `kms:Decrypt` on the key, but the KMS condition ensures decryption only succeeds inside the attested enclave
|
||||
5. The parent application receives encrypted patient records over HTTPS, passes them to the enclave over vsock port 5000, and receives tokenized records back
|
||||
6. The enclave requests an attestation document from the NSM, attaches it to the KMS Decrypt call, receives the plaintext encrypted to its ephemeral RSA key, decrypts locally, tokenizes PII (SSN, DOB, name), and returns `{ssn: "tok_a8f3...", dob: "tok_b2e1...", name: "tok_c9d4..."}`
|
||||
7. CloudTrail logs show `Decrypt` calls with `RecipientAttestation` parameters, confirming all decryption occurs within the enclave boundary
|
||||
|
||||
**Pitfalls**:
|
||||
- Running the enclave in debug mode in production, which allows console access and breaks the confidentiality guarantee that regulators require
|
||||
- Setting the KMS key policy to use only the IAM role without attestation conditions, which allows the parent instance to decrypt directly without the enclave
|
||||
- Failing to reserve sufficient memory in `allocator.yaml`, causing the enclave to fail at launch with an opaque "resource not available" error
|
||||
- Not implementing vsock message framing, causing large records to be truncated at the 64KB socket buffer boundary
|
||||
- Forgetting that PCR0 changes with every code rebuild, requiring a KMS policy update for each deployment; use PCR8 (signing certificate) for production to decouple builds from policy updates
|
||||
|
||||
## Output Format
|
||||
|
||||
```
|
||||
## Nitro Enclave Security Assessment
|
||||
|
||||
**Enclave Image**: enclave-tokenizer.eif
|
||||
**Build Date**: 2026-03-19T14:30:00Z
|
||||
**Instance Type**: m5.2xlarge
|
||||
**Allocated Resources**: 2 vCPUs, 4096 MiB memory
|
||||
|
||||
### PCR Measurements
|
||||
| PCR | Value | Bound in KMS Policy |
|
||||
|-----|-------|---------------------|
|
||||
| PCR0 (Image) | a1b2c3d4e5f6... | Yes |
|
||||
| PCR1 (Kernel) | f6e5d4c3b2a1... | Yes |
|
||||
| PCR2 (Application) | 1a2b3c4d5e6f... | No |
|
||||
| PCR8 (Signing Cert) | 9f8e7d6c5b4a... | Yes (production) |
|
||||
|
||||
### KMS Key Policy Verification
|
||||
- Key ARN: arn:aws:kms:us-east-1:111122223333:key/mrk-abc123
|
||||
- Attestation condition: kms:RecipientAttestation:ImageSha384 = PCR0
|
||||
- Signing cert condition: kms:RecipientAttestation:PCR8 = <cert-hash>
|
||||
- Parent role: arn:aws:iam::111122223333:role/EnclaveParentRole
|
||||
- Direct decrypt from parent: BLOCKED (attestation required)
|
||||
- Decrypt from verified enclave: ALLOWED
|
||||
|
||||
### Security Posture
|
||||
- [PASS] Debug mode disabled in production launch command
|
||||
- [PASS] Vsock is the only communication channel (no network interface)
|
||||
- [PASS] Attestation document nonce verification implemented
|
||||
- [PASS] Certificate chain validates to AWS Nitro root CA
|
||||
- [WARN] PCR0 used in policy; consider PCR8 for deployment flexibility
|
||||
- [FAIL] Health check endpoint does not verify enclave attestation freshness
|
||||
```
|
||||
@@ -0,0 +1,88 @@
|
||||
# API Reference: AWS Nitro Enclave Security Agent
|
||||
|
||||
## Overview
|
||||
|
||||
Assesses the security posture of AWS Nitro Enclave deployments by auditing KMS key policies for attestation conditions, verifying IAM role permissions, validating attestation document structure, and searching CloudTrail for enclave-related security events. For authorized cloud security assessments only.
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| boto3 | >=1.26 | AWS API access for EC2, KMS, IAM, CloudTrail, SSM |
|
||||
| cbor2 | >=5.4 | CBOR decoding of Nitro Enclave attestation documents |
|
||||
| cryptography | >=38.0 | X.509 certificate parsing and signature verification |
|
||||
|
||||
## CLI Usage
|
||||
|
||||
```bash
|
||||
# Full assessment
|
||||
python agent.py --region us-east-1 --kms-key-ids alias/enclave-key mrk-abc123 \
|
||||
--iam-roles EnclaveParentRole --cloudtrail-days 14 --output report.json
|
||||
|
||||
# Validate a specific attestation document
|
||||
python agent.py --attestation-doc <base64-encoded-doc> --output attestation_report.json
|
||||
|
||||
# Quick scan of enclave-enabled instances only
|
||||
python agent.py --region us-west-2 --output instances_report.json
|
||||
```
|
||||
|
||||
## Arguments
|
||||
|
||||
| Argument | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `--region` | No | AWS region to assess (default: us-east-1) |
|
||||
| `--kms-key-ids` | No | One or more KMS key IDs or aliases to audit for attestation conditions |
|
||||
| `--iam-roles` | No | IAM role names to audit for enclave-appropriate permissions |
|
||||
| `--attestation-doc` | No | Base64-encoded attestation document to validate structure |
|
||||
| `--cloudtrail-days` | No | Number of days of CloudTrail history to search (default: 7) |
|
||||
| `--output` | No | Output file path (default: `nitro_enclave_security_report.json`) |
|
||||
|
||||
## Key Functions
|
||||
|
||||
### `get_nitro_instances(ec2_client, region)`
|
||||
Discovers all EC2 instances with Nitro Enclave support enabled by filtering on `enclave-options.enabled=true`. Returns instance IDs, types, IAM roles, and launch times.
|
||||
|
||||
### `audit_kms_key_policy(kms_client, key_id)`
|
||||
Parses KMS key policies to verify the presence of `kms:RecipientAttestation:ImageSha384` and `kms:RecipientAttestation:PCR*` condition keys. Flags keys that allow Decrypt/GenerateDataKey without attestation conditions.
|
||||
|
||||
### `audit_iam_role_for_enclave(iam_client, role_name)`
|
||||
Checks an IAM role for KMS permissions, wildcard resources, and overprivileged policies (AdministratorAccess). Audits both attached managed policies and inline policies.
|
||||
|
||||
### `validate_attestation_document_structure(attestation_b64)`
|
||||
Decodes a base64-encoded COSE_Sign1 attestation document, extracts PCR measurements, module ID, timestamps, certificate chain, and public key. Validates structural completeness.
|
||||
|
||||
### `audit_cloudtrail_enclave_events(cloudtrail_client, days_back)`
|
||||
Searches CloudTrail for enclave-related events including instance launches with enclave options and KMS operations with Recipient (attestation) parameters.
|
||||
|
||||
### `check_enclave_allocator_config(instance_id, ssm_client)`
|
||||
Uses SSM Run Command to read the enclave allocator configuration from `/etc/nitro_enclaves/allocator.yaml` and checks for adequate memory and CPU allocation.
|
||||
|
||||
## Output Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"report_type": "Nitro Enclave Security Assessment",
|
||||
"generated_at": "ISO-8601 timestamp",
|
||||
"summary": {
|
||||
"enclave_instances": 0,
|
||||
"kms_keys_audited": 0,
|
||||
"iam_roles_audited": 0,
|
||||
"cloudtrail_events": 0,
|
||||
"total_issues": 0,
|
||||
"critical_issues": 0
|
||||
},
|
||||
"critical_findings": ["string"],
|
||||
"instances": [{"instance_id": "", "instance_type": "", "enclave_enabled": true}],
|
||||
"kms_policy_audits": [{"key_id": "", "has_attestation_condition": false, "pcr_conditions": [], "issues": []}],
|
||||
"iam_role_audits": [{"role_name": "", "has_kms_permissions": false, "overprivileged": false, "issues": []}],
|
||||
"cloudtrail_events": [{"event": "", "time": "", "user": "", "detail": ""}],
|
||||
"attestation_validation": {"valid_structure": false, "pcrs": {}, "issues": []}
|
||||
}
|
||||
```
|
||||
|
||||
## Exit Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 0 | No critical issues found |
|
||||
| 1 | Critical issues detected (missing attestation conditions or overprivileged roles) |
|
||||
@@ -0,0 +1,510 @@
|
||||
#!/usr/bin/env python3
|
||||
# For authorized cloud security assessments only
|
||||
"""AWS Nitro Enclave Security Agent - Validates enclave attestation, audits KMS policies, and verifies enclave isolation."""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
try:
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
except ImportError:
|
||||
print("ERROR: boto3 required. Install with: pip install boto3")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
import cbor2
|
||||
except ImportError:
|
||||
cbor2 = None
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_nitro_instances(ec2_client, region):
|
||||
"""Find EC2 instances with Nitro Enclave support enabled."""
|
||||
findings = []
|
||||
paginator = ec2_client.get_paginator("describe_instances")
|
||||
for page in paginator.paginate(
|
||||
Filters=[{"Name": "enclave-options.enabled", "Values": ["true"]}]
|
||||
):
|
||||
for reservation in page["Reservations"]:
|
||||
for instance in reservation["Instances"]:
|
||||
instance_info = {
|
||||
"instance_id": instance["InstanceId"],
|
||||
"instance_type": instance["InstanceType"],
|
||||
"state": instance["State"]["Name"],
|
||||
"enclave_enabled": True,
|
||||
"iam_role": None,
|
||||
"launch_time": instance.get("LaunchTime", "").isoformat() if instance.get("LaunchTime") else None,
|
||||
"region": region,
|
||||
}
|
||||
if instance.get("IamInstanceProfile"):
|
||||
instance_info["iam_role"] = instance["IamInstanceProfile"]["Arn"]
|
||||
findings.append(instance_info)
|
||||
logger.info("Found %d Nitro Enclave-enabled instances in %s", len(findings), region)
|
||||
return findings
|
||||
|
||||
|
||||
def audit_kms_key_policy(kms_client, key_id):
|
||||
"""Audit a KMS key policy for Nitro Enclave attestation conditions."""
|
||||
result = {
|
||||
"key_id": key_id,
|
||||
"has_attestation_condition": False,
|
||||
"pcr_conditions": [],
|
||||
"image_sha_condition": False,
|
||||
"allowed_principals": [],
|
||||
"allowed_actions": [],
|
||||
"issues": [],
|
||||
}
|
||||
try:
|
||||
key_meta = kms_client.describe_key(KeyId=key_id)
|
||||
result["key_arn"] = key_meta["KeyMetadata"]["Arn"]
|
||||
result["key_state"] = key_meta["KeyMetadata"]["KeyState"]
|
||||
result["key_usage"] = key_meta["KeyMetadata"]["KeyUsage"]
|
||||
|
||||
policy_json = kms_client.get_key_policy(KeyId=key_id, PolicyName="default")["Policy"]
|
||||
policy = json.loads(policy_json)
|
||||
|
||||
for statement in policy.get("Statement", []):
|
||||
principals = statement.get("Principal", {})
|
||||
actions = statement.get("Action", [])
|
||||
if isinstance(actions, str):
|
||||
actions = [actions]
|
||||
conditions = statement.get("Condition", {})
|
||||
|
||||
for action in actions:
|
||||
if action not in result["allowed_actions"]:
|
||||
result["allowed_actions"].append(action)
|
||||
|
||||
if isinstance(principals, dict) and "AWS" in principals:
|
||||
aws_principals = principals["AWS"]
|
||||
if isinstance(aws_principals, str):
|
||||
aws_principals = [aws_principals]
|
||||
result["allowed_principals"].extend(aws_principals)
|
||||
|
||||
# Check for attestation conditions
|
||||
for operator_key, operator_conditions in conditions.items():
|
||||
for cond_key, cond_value in operator_conditions.items():
|
||||
if "RecipientAttestation" in cond_key:
|
||||
result["has_attestation_condition"] = True
|
||||
if "ImageSha384" in cond_key:
|
||||
result["image_sha_condition"] = True
|
||||
result["pcr_conditions"].append({
|
||||
"type": "ImageSha384 (PCR0)",
|
||||
"operator": operator_key,
|
||||
"value": cond_value[:32] + "..." if len(str(cond_value)) > 32 else cond_value,
|
||||
})
|
||||
elif "PCR" in cond_key:
|
||||
pcr_id = cond_key.split(":")[-1]
|
||||
result["pcr_conditions"].append({
|
||||
"type": pcr_id,
|
||||
"operator": operator_key,
|
||||
"value": cond_value[:32] + "..." if len(str(cond_value)) > 32 else cond_value,
|
||||
})
|
||||
|
||||
# Check for missing attestation on decrypt actions
|
||||
has_decrypt = any("Decrypt" in a or "GenerateDataKey" in a for a in actions)
|
||||
if has_decrypt and not any("RecipientAttestation" in str(conditions)):
|
||||
if statement.get("Effect") == "Allow":
|
||||
result["issues"].append(
|
||||
f"Statement '{statement.get('Sid', 'unnamed')}' allows Decrypt/GenerateDataKey "
|
||||
f"without kms:RecipientAttestation condition - parent instance can decrypt directly"
|
||||
)
|
||||
|
||||
if not result["has_attestation_condition"]:
|
||||
result["issues"].append(
|
||||
"KMS key policy has no RecipientAttestation conditions - "
|
||||
"decryption is not restricted to verified enclaves"
|
||||
)
|
||||
|
||||
except ClientError as e:
|
||||
result["issues"].append(f"Error accessing key: {e.response['Error']['Message']}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def audit_iam_role_for_enclave(iam_client, role_name):
|
||||
"""Check if an IAM role has appropriate permissions for enclave operations."""
|
||||
result = {
|
||||
"role_name": role_name,
|
||||
"has_kms_permissions": False,
|
||||
"kms_actions": [],
|
||||
"has_ec2_enclave_permissions": False,
|
||||
"overprivileged": False,
|
||||
"issues": [],
|
||||
}
|
||||
try:
|
||||
# Check attached policies
|
||||
attached = iam_client.list_attached_role_policies(RoleName=role_name)
|
||||
for policy in attached["AttachedPolicies"]:
|
||||
if policy["PolicyName"] == "AdministratorAccess":
|
||||
result["overprivileged"] = True
|
||||
result["issues"].append(
|
||||
"Role has AdministratorAccess - violates least privilege for enclave workloads"
|
||||
)
|
||||
|
||||
policy_version = iam_client.get_policy(PolicyArn=policy["PolicyArn"])
|
||||
version_id = policy_version["Policy"]["DefaultVersionId"]
|
||||
policy_doc = iam_client.get_policy_version(
|
||||
PolicyArn=policy["PolicyArn"], VersionId=version_id
|
||||
)
|
||||
for stmt in policy_doc["PolicyVersion"]["Document"].get("Statement", []):
|
||||
actions = stmt.get("Action", [])
|
||||
if isinstance(actions, str):
|
||||
actions = [actions]
|
||||
for action in actions:
|
||||
if "kms:" in action:
|
||||
result["has_kms_permissions"] = True
|
||||
result["kms_actions"].append(action)
|
||||
if action in ("kms:*", "*"):
|
||||
result["overprivileged"] = True
|
||||
result["issues"].append(
|
||||
f"Role has wildcard KMS permissions ({action}) - should restrict to specific keys"
|
||||
)
|
||||
|
||||
# Check inline policies
|
||||
inline = iam_client.list_role_policies(RoleName=role_name)
|
||||
for policy_name in inline["PolicyNames"]:
|
||||
policy_doc = iam_client.get_role_policy(RoleName=role_name, PolicyName=policy_name)
|
||||
for stmt in policy_doc["PolicyDocument"].get("Statement", []):
|
||||
actions = stmt.get("Action", [])
|
||||
if isinstance(actions, str):
|
||||
actions = [actions]
|
||||
resources = stmt.get("Resource", [])
|
||||
if isinstance(resources, str):
|
||||
resources = [resources]
|
||||
for action in actions:
|
||||
if "kms:" in action:
|
||||
result["has_kms_permissions"] = True
|
||||
result["kms_actions"].append(action)
|
||||
if "*" in resources:
|
||||
result["issues"].append(
|
||||
f"Inline policy '{policy_name}' uses wildcard Resource - restrict to specific KMS key ARNs"
|
||||
)
|
||||
|
||||
if not result["has_kms_permissions"]:
|
||||
result["issues"].append("Role has no KMS permissions - cannot perform enclave-side decryption")
|
||||
|
||||
except ClientError as e:
|
||||
result["issues"].append(f"Error auditing role: {e.response['Error']['Message']}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def check_enclave_allocator_config(instance_id, ssm_client):
|
||||
"""Check enclave allocator configuration via SSM (if available)."""
|
||||
result = {
|
||||
"instance_id": instance_id,
|
||||
"allocator_configured": False,
|
||||
"memory_mib": None,
|
||||
"cpu_count": None,
|
||||
"issues": [],
|
||||
}
|
||||
try:
|
||||
response = ssm_client.send_command(
|
||||
InstanceIds=[instance_id],
|
||||
DocumentName="AWS-RunShellScript",
|
||||
Parameters={
|
||||
"commands": ["cat /etc/nitro_enclaves/allocator.yaml 2>/dev/null || echo 'NOT_FOUND'"]
|
||||
},
|
||||
)
|
||||
command_id = response["Command"]["CommandId"]
|
||||
|
||||
import time
|
||||
time.sleep(3)
|
||||
|
||||
output = ssm_client.get_command_invocation(
|
||||
CommandId=command_id, InstanceId=instance_id
|
||||
)
|
||||
stdout = output.get("StandardOutputContent", "")
|
||||
|
||||
if "NOT_FOUND" in stdout:
|
||||
result["issues"].append("Allocator config not found at /etc/nitro_enclaves/allocator.yaml")
|
||||
else:
|
||||
result["allocator_configured"] = True
|
||||
for line in stdout.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("memory_mib:"):
|
||||
result["memory_mib"] = int(line.split(":")[1].strip())
|
||||
elif line.startswith("cpu_count:"):
|
||||
result["cpu_count"] = int(line.split(":")[1].strip())
|
||||
|
||||
if result["memory_mib"] and result["memory_mib"] < 512:
|
||||
result["issues"].append(
|
||||
f"Allocated memory ({result['memory_mib']} MiB) is very low - may cause enclave launch failures"
|
||||
)
|
||||
if result["cpu_count"] and result["cpu_count"] < 2:
|
||||
result["issues"].append(
|
||||
f"Allocated CPUs ({result['cpu_count']}) is minimal - consider 2+ for production"
|
||||
)
|
||||
|
||||
except ClientError as e:
|
||||
result["issues"].append(f"SSM access failed: {e.response['Error']['Message']}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def validate_attestation_document_structure(attestation_b64):
|
||||
"""Validate the structure of a base64-encoded attestation document."""
|
||||
if cbor2 is None:
|
||||
return {"error": "cbor2 package required for attestation validation. Install with: pip install cbor2"}
|
||||
|
||||
result = {
|
||||
"valid_structure": False,
|
||||
"pcrs": {},
|
||||
"module_id": None,
|
||||
"digest": None,
|
||||
"timestamp": None,
|
||||
"has_certificate": False,
|
||||
"has_cabundle": False,
|
||||
"has_public_key": False,
|
||||
"issues": [],
|
||||
}
|
||||
try:
|
||||
attestation_bytes = base64.b64decode(attestation_b64)
|
||||
|
||||
# COSE_Sign1 is a CBOR array: [protected, unprotected, payload, signature]
|
||||
cose_structure = cbor2.loads(attestation_bytes)
|
||||
if hasattr(cose_structure, "tag") and cose_structure.tag == 18:
|
||||
cose_array = cose_structure.value
|
||||
elif isinstance(cose_structure, list) and len(cose_structure) == 4:
|
||||
cose_array = cose_structure
|
||||
else:
|
||||
result["issues"].append("Not a valid COSE_Sign1 structure")
|
||||
return result
|
||||
|
||||
payload = cbor2.loads(cose_array[2])
|
||||
|
||||
result["module_id"] = payload.get("module_id")
|
||||
result["digest"] = payload.get("digest")
|
||||
result["timestamp"] = payload.get("timestamp")
|
||||
|
||||
if result["timestamp"]:
|
||||
ts = datetime.fromtimestamp(result["timestamp"] / 1000, tz=timezone.utc)
|
||||
result["timestamp_human"] = ts.isoformat()
|
||||
|
||||
pcrs = payload.get("pcrs", {})
|
||||
for idx, value in pcrs.items():
|
||||
result["pcrs"][f"PCR{idx}"] = value.hex() if isinstance(value, bytes) else str(value)
|
||||
|
||||
result["has_certificate"] = "certificate" in payload and payload["certificate"] is not None
|
||||
result["has_cabundle"] = "cabundle" in payload and len(payload.get("cabundle", [])) > 0
|
||||
result["has_public_key"] = "public_key" in payload and payload["public_key"] is not None
|
||||
|
||||
result["valid_structure"] = True
|
||||
|
||||
if not result["has_cabundle"]:
|
||||
result["issues"].append("Missing CA bundle - cannot verify certificate chain to AWS root")
|
||||
if not result["has_public_key"]:
|
||||
result["issues"].append("No public key in attestation - KMS cannot encrypt response to enclave")
|
||||
if "PCR0" not in result["pcrs"]:
|
||||
result["issues"].append("PCR0 (image hash) not present in attestation document")
|
||||
|
||||
except Exception as e:
|
||||
result["issues"].append(f"Attestation parsing error: {str(e)}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def audit_cloudtrail_enclave_events(cloudtrail_client, days_back=7):
|
||||
"""Search CloudTrail for enclave-related security events."""
|
||||
from datetime import timedelta
|
||||
end_time = datetime.now(timezone.utc)
|
||||
start_time = end_time - timedelta(days=days_back)
|
||||
|
||||
events_of_interest = [
|
||||
"RunInstances",
|
||||
"TerminateInstances",
|
||||
"ModifyInstanceAttribute",
|
||||
]
|
||||
kms_events = ["Decrypt", "GenerateDataKey", "GenerateDataKeyPair", "GenerateRandom"]
|
||||
|
||||
findings = []
|
||||
|
||||
# Check for instance launches with enclave options
|
||||
for event_name in events_of_interest:
|
||||
try:
|
||||
response = cloudtrail_client.lookup_events(
|
||||
LookupAttributes=[
|
||||
{"AttributeKey": "EventName", "AttributeValue": event_name}
|
||||
],
|
||||
StartTime=start_time,
|
||||
EndTime=end_time,
|
||||
MaxResults=50,
|
||||
)
|
||||
for event in response.get("Events", []):
|
||||
ct_event = json.loads(event.get("CloudTrailEvent", "{}"))
|
||||
req_params = ct_event.get("requestParameters", {})
|
||||
|
||||
if event_name == "RunInstances":
|
||||
enclave_opts = req_params.get("enclaveOptions", {})
|
||||
if enclave_opts.get("enabled"):
|
||||
findings.append({
|
||||
"event": event_name,
|
||||
"time": event["EventTime"].isoformat(),
|
||||
"user": event.get("Username"),
|
||||
"detail": "Enclave-enabled instance launched",
|
||||
"source_ip": ct_event.get("sourceIPAddress"),
|
||||
})
|
||||
except ClientError:
|
||||
continue
|
||||
|
||||
# Check for KMS calls with Recipient parameter (enclave attestation)
|
||||
for event_name in kms_events:
|
||||
try:
|
||||
response = cloudtrail_client.lookup_events(
|
||||
LookupAttributes=[
|
||||
{"AttributeKey": "EventName", "AttributeValue": event_name}
|
||||
],
|
||||
StartTime=start_time,
|
||||
EndTime=end_time,
|
||||
MaxResults=50,
|
||||
)
|
||||
for event in response.get("Events", []):
|
||||
ct_event = json.loads(event.get("CloudTrailEvent", "{}"))
|
||||
req_params = ct_event.get("requestParameters", {})
|
||||
if "recipient" in req_params or "Recipient" in req_params:
|
||||
findings.append({
|
||||
"event": event_name,
|
||||
"time": event["EventTime"].isoformat(),
|
||||
"user": event.get("Username"),
|
||||
"detail": "KMS operation with enclave attestation document",
|
||||
"key_id": req_params.get("keyId"),
|
||||
"source_ip": ct_event.get("sourceIPAddress"),
|
||||
})
|
||||
except ClientError:
|
||||
continue
|
||||
|
||||
logger.info("Found %d enclave-related CloudTrail events", len(findings))
|
||||
return findings
|
||||
|
||||
|
||||
def generate_report(instances, kms_audits, iam_audits, cloudtrail_events, attestation_results=None):
|
||||
"""Generate comprehensive Nitro Enclave security assessment report."""
|
||||
total_issues = 0
|
||||
critical_issues = []
|
||||
|
||||
for audit in kms_audits:
|
||||
total_issues += len(audit.get("issues", []))
|
||||
if not audit.get("has_attestation_condition"):
|
||||
critical_issues.append(f"KMS key {audit['key_id']} has no attestation conditions")
|
||||
|
||||
for audit in iam_audits:
|
||||
total_issues += len(audit.get("issues", []))
|
||||
if audit.get("overprivileged"):
|
||||
critical_issues.append(f"IAM role {audit['role_name']} is overprivileged")
|
||||
|
||||
report = {
|
||||
"report_type": "Nitro Enclave Security Assessment",
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"summary": {
|
||||
"enclave_instances": len(instances),
|
||||
"kms_keys_audited": len(kms_audits),
|
||||
"iam_roles_audited": len(iam_audits),
|
||||
"cloudtrail_events": len(cloudtrail_events),
|
||||
"total_issues": total_issues,
|
||||
"critical_issues": len(critical_issues),
|
||||
},
|
||||
"critical_findings": critical_issues,
|
||||
"instances": instances,
|
||||
"kms_policy_audits": kms_audits,
|
||||
"iam_role_audits": iam_audits,
|
||||
"cloudtrail_events": cloudtrail_events,
|
||||
}
|
||||
|
||||
if attestation_results:
|
||||
report["attestation_validation"] = attestation_results
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="AWS Nitro Enclave Security Assessment Agent")
|
||||
parser.add_argument("--region", default="us-east-1", help="AWS region")
|
||||
parser.add_argument("--kms-key-ids", nargs="+", help="KMS key IDs to audit")
|
||||
parser.add_argument("--iam-roles", nargs="+", help="IAM role names to audit for enclave permissions")
|
||||
parser.add_argument("--attestation-doc", help="Base64-encoded attestation document to validate")
|
||||
parser.add_argument("--cloudtrail-days", type=int, default=7, help="Days of CloudTrail history to search")
|
||||
parser.add_argument("--output", default="nitro_enclave_security_report.json", help="Output report file")
|
||||
args = parser.parse_args()
|
||||
|
||||
session = boto3.Session(region_name=args.region)
|
||||
ec2_client = session.client("ec2")
|
||||
kms_client = session.client("kms")
|
||||
iam_client = session.client("iam")
|
||||
cloudtrail_client = session.client("cloudtrail")
|
||||
|
||||
logger.info("Starting Nitro Enclave security assessment in %s", args.region)
|
||||
|
||||
# Step 1: Find enclave-enabled instances
|
||||
instances = get_nitro_instances(ec2_client, args.region)
|
||||
|
||||
# Step 2: Audit KMS key policies
|
||||
kms_audits = []
|
||||
if args.kms_key_ids:
|
||||
for key_id in args.kms_key_ids:
|
||||
logger.info("Auditing KMS key: %s", key_id)
|
||||
kms_audits.append(audit_kms_key_policy(kms_client, key_id))
|
||||
else:
|
||||
# Auto-discover KMS keys
|
||||
try:
|
||||
keys_response = kms_client.list_keys(Limit=100)
|
||||
for key in keys_response.get("Keys", []):
|
||||
audit = audit_kms_key_policy(kms_client, key["KeyId"])
|
||||
if audit.get("has_attestation_condition") or audit.get("allowed_actions"):
|
||||
kms_audits.append(audit)
|
||||
except ClientError as e:
|
||||
logger.warning("Cannot list KMS keys: %s", e)
|
||||
|
||||
# Step 3: Audit IAM roles
|
||||
iam_audits = []
|
||||
if args.iam_roles:
|
||||
for role_name in args.iam_roles:
|
||||
logger.info("Auditing IAM role: %s", role_name)
|
||||
iam_audits.append(audit_iam_role_for_enclave(iam_client, role_name))
|
||||
|
||||
# Step 4: Search CloudTrail events
|
||||
cloudtrail_events = audit_cloudtrail_enclave_events(cloudtrail_client, args.cloudtrail_days)
|
||||
|
||||
# Step 5: Validate attestation document if provided
|
||||
attestation_results = None
|
||||
if args.attestation_doc:
|
||||
logger.info("Validating attestation document")
|
||||
attestation_results = validate_attestation_document_structure(args.attestation_doc)
|
||||
|
||||
# Generate report
|
||||
report = generate_report(instances, kms_audits, iam_audits, cloudtrail_events, attestation_results)
|
||||
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2, default=str)
|
||||
logger.info("Report saved to %s", args.output)
|
||||
|
||||
# Print summary
|
||||
summary = report["summary"]
|
||||
logger.info(
|
||||
"Assessment complete: %d instances, %d KMS keys, %d IAM roles, %d issues (%d critical)",
|
||||
summary["enclave_instances"],
|
||||
summary["kms_keys_audited"],
|
||||
summary["iam_roles_audited"],
|
||||
summary["total_issues"],
|
||||
summary["critical_issues"],
|
||||
)
|
||||
|
||||
if report["critical_findings"]:
|
||||
logger.warning("CRITICAL FINDINGS:")
|
||||
for finding in report["critical_findings"]:
|
||||
logger.warning(" - %s", finding)
|
||||
|
||||
return 0 if summary["critical_issues"] == 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user