Pandora Cloud takes security seriously. This project handles credentials for three external systems (Deltek GovWin IQ, HubSpot, AWS Partner Central) and routes federal contracting opportunity data, so secure-by-default is a baseline expectation.
| Version | Supported |
|---|---|
| v2.x (end-to-end with direct AWS Partner Central API) | Yes |
| v1.x (GovWin to HubSpot only; ACE submission added in v2) | Security fixes only |
| < v1.0 | No |
Do not open a public GitHub issue for security reports. Use one of these channels instead, in order of preference:
- GitHub private security advisory (preferred). Open one here. This routes to the maintainers without becoming public.
- Encrypted email to pc@pandoracloud.net. Encrypt with our PGP public key:
- Fingerprint:
59B2414BE40D2BC8F1A67745D1165A8FF46CC177 - Public key:
.well-known/security/pandora-cloud-public.ascin this repo, also published tokeys.openpgp.org.
- Fingerprint:
- Plain-text email to pc@pandoracloud.net if encryption is impractical. We will accept the report but will respond using PGP if your message did not include your own public key.
Include in your report:
- A description of the issue and its impact
- Reproduction steps or a proof-of-concept (do not test against systems you don't own)
- Your suggested mitigation if you have one
| Severity | Definition | First response | Fix or mitigation |
|---|---|---|---|
| Critical | Active exploitation; credential or PII disclosure; data loss; ACE submission integrity | 24 hours | 7 calendar days |
| High | Authentication / signature-validation bypass; privilege escalation in IAM; webhook replay defeat | 3 business days | 14 calendar days |
| Medium | Missing rate-limit defenses; secret-handling weaknesses that don't disclose; logging-induced PII leak | 5 business days | 30 calendar days |
| Low | Hardening recommendations; defense-in-depth | 7 business days | Best-effort, often via Renovate / Dependabot |
If you do not receive a response within the first-response window, escalate by emailing pc@pandoracloud.net with [ESCALATION] in the subject.
- Authentication and signature-validation flows (HubSpot
X-HubSpot-Signature-v3, AWS SigV4, GovWin OAuth2) - Secret handling (Secrets Manager paths, env var hygiene, log-output redaction)
- DynamoDB key construction and any user-controllable data flowing into URL paths or DynamoDB pks
- IAM policy scoping (especially the
partnercentral:Catalog: Sandboxcondition) - Webhook receiver hardening (replay window, body-size cap, event-count cap)
- Cross-tenant or cross-deal data leakage in the sync logic
- The integration always defaults
ACE_CATALOG=Sandbox. Production deployments must explicitly opt in. - The Lambda execution role's IAM policy includes a
partnercentral:Catalogcondition that always pins to the configured catalog. Sandbox deployments cannot reach into production and vice versa, even if code accidentally passes the wrong catalog string. - A single customer-managed KMS CMK (
module.kms.aws_kms_key.pipeline) encrypts every at-rest data store the project owns: both DynamoDB tables, the SNS notifications topic, and every SQS queue (operational + DLQs). CloudTrail records key use under the project's own keyId. The key policy grants the matching AWS service principals (SQS, SNS, EventBridge, Scheduler, DynamoDB) withViaService/EncryptionContextscoping so the grants cannot be reused outside this region or project. - The internet-reachable HubSpot UI Extension callbacks run on two Lambdas with separate IAM scopes. The reads Lambda (
<prefix>-ui-ext-reads) is limited topartnercentral:ListSolutionsand the webhook signing-secret read: no HubSpot private app token, no DynamoDB reads, no Partner Central writes. The writes Lambda (<prefix>-ui-ext-writes) handlesUpdateOpportunity+AssociateOpportunity+DisassociateOpportunitybut explicitly excludesCreateOpportunityandStartEngagementFromOpportunityTask(those run only from the trusted shared role behind the SQS pipeline). A parser or dependency CVE on the reads surface cannot reach the update path. - Property-change webhooks for
govwin_aws_cosell_idfire a real-time SNS audit alert when thechangeSourceis anything other thanINTEGRATION, OR when thesourceIddoes not match the configuredHUBSPOT_INTEGRATION_APP_ID. The second case catches a different HubSpot integration installed on the same portal writing to the property. The runtime self-heal verify inupdate_in_acerefuses to mutate AWS if the deal's recovered ACE id resolves to a foreignPartnerOpportunityIdentifierOR if the DDB mapping was already bound to a differenthubspot_deal_id; the SNS audit alert lets the operator reconcile before the next save trips the verify. ACEClient.get_opportunityecho-checks the returnedCatalogfield against the configured catalog and raisesCrossCatalogResponseon mismatch. Defense-in-depth on top of the IAM Catalog condition: a regressed AWS response that returns a cross-catalog opportunity becomes a hard error at read time rather than flowing silently through the scrub-and-update path.- The two PutItem grants on the entity-mappings DynamoDB table (used by
hubspot_webhook_receiverandui_extension_readsfor the signed-request replay reservation) carry adynamodb:LeadingKeysIAM condition restricting the write to theWHK#prefix. A compromised receiver or reads Lambda cannot poison rows underACE#,OPP#,DEAL#, orINV#. - AWS error strings echoed into HubSpot deal properties via the
submit_to_aceandupdate_in_acewriteback paths pass through_redact_hubspot_error_body, which full-redactspropertyValue,localizedErrorMessage,CompanyName,Email,Phone,WebsiteUrl, andReason, and trimsmessage/Message/ErrorMessageto 200 chars. HubSpot deal properties are visible to anyone with deal-read. - Webhook signature replay reservation TTL matches the signature freshness window (300 seconds). Replays past that window already fail the signature timestamp check, so the prior
2 *TTL provided no marginal protection and only widened the window in which a stale fingerprint occupied DDB. - No VPC is required; all calls go to AWS service endpoints, GovWin's public API, and HubSpot's public API.
The project follows least privilege at every identity boundary. Three identities are involved, each with a documented and version-controlled policy.
| Identity | Used by | When | Policy location |
|---|---|---|---|
| Bootstrap operator | Security team, one-time per environment | One-shot setup | terraform/bootstrap/policies/bootstrap-operator.json (S3 state bucket creation + deployer role creation only) |
| Deployer role | terraform apply for the main module |
Every deploy | Inline policies in terraform/bootstrap/deployer_role.tf. Scoped to ${name_prefix}-* resource ARNs across S3, Lambda, API Gateway, SQS, SNS, DynamoDB, Secrets Manager, EventBridge, EventBridge Scheduler, KMS, IAM, and CloudWatch Logs. |
| Lambda execution role | The deployed Lambdas at runtime | Continuous | terraform/modules/lambda/main.tf and terraform/modules/ace/iam.tf. Scoped to specific table / queue / secret ARNs. partnercentral:* actions use Resource = "*" because the Partner Central Selling API does not support resource-level IAM; this is mitigated by the partnercentral:Catalog condition that always pins to the configured catalog. |
The day-to-day deployer's personal IAM identity needs only sts:AssumeRole on the deployer role's ARN. CloudTrail records every assumption.
| Where | Wildcard | Justification |
|---|---|---|
Lambda runtime: partnercentral:* actions |
Resource = "*" |
Partner Central Selling API does not support resource-level IAM. Mitigated by partnercentral:Catalog StringEquals condition pinning to Sandbox or AWS per deployment. |
| Lambda runtime: CloudWatch Logs | arn:aws:logs:...:log-group:/aws/lambda/${name_prefix}-*:* |
The trailing :* is the AWS-required pattern to allow log-stream creation within the project's log groups. |
Deployer role: lambda:*EventSourceMapping |
arn:aws:lambda:...:event-source-mapping:* |
Event source mapping ARNs are not predictable at policy-write time (they're UUIDs); the resource type itself only exists for Lambda, so this is bounded. |
Deployer role: apigateway:* |
arn:aws:apigateway:...::/apis/* |
API Gateway HTTP API resource-level IAM is incomplete; the closest scoping is to all APIs in the account/region. |
Deployer role: read-only sts:GetCallerIdentity and ec2:DescribeRegions |
Resource = "*" |
Both are AWS-required and cannot be scoped. They are read-only and used by Terraform to discover the current account ID and region list. |
No AWS administrator privileges are required at any point. The bootstrap-operator policy is ~30 IAM actions; the deployer policy is ~70 actions across 11 inline policies, all scoped to project-prefix ARNs.
- The integration logs request paths, status codes, and identifiers (deal IDs, GovWin opp IDs, AWS opportunity IDs). Free-text deal content is not logged.
- HubSpot webhook subscriptions ship
active: falseby default in the developer-platform project. They must be explicitly activated. - Default AWS credentials precedence (env vars, profile, instance role). The integration follows boto3 defaults; misconfigured operator credentials are out of scope.