mirror of https://github.com/docusealco/docuseal
Implement compliance storage configuration using AWS CloudFront signed URLs for completed documents. This reuses the existing ATS infrastructure to provide secure, time-limited access to document storage while maintaining backward compatibility with legacy storage. - Add aws-sdk-cloudfront dependency for URL signing - Create DocumentSecurityService for CloudFront signed URL generation - Add secured storage service configuration in storage.yml - Update completed_documents model with storage_location tracking - Modify download controllers to use signed URLs for secured storage - Add compliance_storage.yml configuration for different environments - Update submitter completion job to track storage location BREAKING CHANGE: Requires SECURED_STORAGE_BUCKET and SECURED_STORAGE_REGION environment variables for staging/production environmentspull/544/head
parent
a1ed992ee4
commit
bfcc59d70f
@ -0,0 +1,165 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# DOCUSEAL S3 CONFIGURATION TEMPLATE
|
||||||
|
# =============================================================================
|
||||||
|
# Copy this file to .env and customize the values for your environment.
|
||||||
|
# Remove the .template extension after copying.
|
||||||
|
#
|
||||||
|
# SECURITY NOTE: Never commit actual credentials to version control!
|
||||||
|
# Use environment-specific .env files and add them to .gitignore.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# AWS CREDENTIALS
|
||||||
|
# =============================================================================
|
||||||
|
# Required: AWS access key ID for programmatic access
|
||||||
|
# Get this from AWS IAM console -> Users -> Security credentials
|
||||||
|
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
|
||||||
|
|
||||||
|
# Required: AWS secret access key for programmatic access
|
||||||
|
# Keep this confidential and never share or commit to version control
|
||||||
|
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
||||||
|
|
||||||
|
# Optional: AWS session token for temporary credentials
|
||||||
|
# Only required when using temporary credentials (e.g., with AWS STS)
|
||||||
|
# AWS_SESSION_TOKEN=AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/LTo6UDdyJwOOvEVPvLXCrrrUtdnniCEXAMPLE/IvU1dYUg2RVAJBanLiHb4IgRmpRV3zrkuWJOgQs8IZZaIv2BXIa2R4OlgkBN9bkUDNCJiBeb/AXlzBBko7b15fjrBs2+cTQtpZ3CYWFXG8C5zqx37wnOE49mRl/+OtkIKGO7fAE=
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# AWS S3 CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
# Required: AWS region where your S3 bucket is located
|
||||||
|
# Examples: us-east-1, us-west-2, eu-west-1, ap-southeast-1
|
||||||
|
AWS_REGION=us-east-1
|
||||||
|
|
||||||
|
# Required: S3 bucket name for storing attachments
|
||||||
|
# Must be globally unique and follow S3 bucket naming rules
|
||||||
|
# Recommended format: your-company-docuseal-attachments-env
|
||||||
|
S3_ATTACHMENTS_BUCKET=your-company-docuseal-attachments-production
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# S3 ACCESS CONTROL
|
||||||
|
# =============================================================================
|
||||||
|
# Optional: Whether files should be publicly accessible via direct URLs
|
||||||
|
# Set to 'true' for public access, 'false' for private access
|
||||||
|
# Private files require presigned URLs for access (more secure)
|
||||||
|
# Default: false (recommended for production)
|
||||||
|
ACTIVE_STORAGE_PUBLIC=false
|
||||||
|
|
||||||
|
# Optional: Expiration time for presigned URLs (in minutes)
|
||||||
|
# Only used when ACTIVE_STORAGE_PUBLIC=false
|
||||||
|
# Default: 240 minutes (4 hours)
|
||||||
|
PRESIGNED_URLS_EXPIRE_MINUTES=240
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# S3 SECURITY OPTIONS
|
||||||
|
# =============================================================================
|
||||||
|
# Optional: Server-side encryption for uploaded files
|
||||||
|
# Options:
|
||||||
|
# - AES256 (S3-managed encryption)
|
||||||
|
# - aws:kms (KMS-managed encryption with AWS KMS key)
|
||||||
|
# - aws:kms:dsse (KMS-managed encryption with double server-side encryption)
|
||||||
|
# Uncomment the desired option below
|
||||||
|
# S3_SERVER_SIDE_ENCRYPTION=AES256
|
||||||
|
# S3_SERVER_SIDE_ENCRYPTION=aws:kms
|
||||||
|
|
||||||
|
# Optional: AWS KMS Key ID for KMS-managed encryption
|
||||||
|
# Only required when using aws:kms encryption with a specific KMS key
|
||||||
|
# S3_KMS_KEY_ID=arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012
|
||||||
|
|
||||||
|
# Optional: Force path-style URLs instead of virtual-hosted-style URLs
|
||||||
|
# Set to 'true' for S3-compatible services (MinIO, DigitalOcean Spaces, etc.)
|
||||||
|
# or if you encounter DNS resolution issues
|
||||||
|
# Default: false
|
||||||
|
# S3_FORCE_PATH_STYLE=false
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# S3 ENDPOINT CONFIGURATION (FOR S3-COMPATIBLE SERVICES)
|
||||||
|
# =============================================================================
|
||||||
|
# Optional: Custom S3 endpoint URL
|
||||||
|
# Only required for S3-compatible services (MinIO, DigitalOcean Spaces, etc.)
|
||||||
|
# S3_ENDPOINT=https://nyc3.digitaloceanspaces.com
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# ADVANCED S3 OPTIONS
|
||||||
|
# =============================================================================
|
||||||
|
# Optional: S3 storage class for uploaded files
|
||||||
|
# Options: STANDARD, REDUCED_REDUNDANCY, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING,
|
||||||
|
# GLACIER, DEEP_ARCHIVE, OUTPOSTS, GLACIER_IR
|
||||||
|
# Default: STANDARD
|
||||||
|
# S3_STORAGE_CLASS=STANDARD
|
||||||
|
|
||||||
|
# Optional: Cache control header for uploaded files
|
||||||
|
# Affects browser caching behavior for publicly accessible files
|
||||||
|
# Default: 'public, max-age=31536000' (1 year)
|
||||||
|
# S3_CACHE_CONTROL=public, max-age=31536000
|
||||||
|
|
||||||
|
# Optional: Content disposition for uploaded files
|
||||||
|
# Controls how browsers handle file downloads
|
||||||
|
# S3_CONTENT_DISPOSITION=attachment
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# AWS IAM ROLE CONFIGURATION (ALTERNATIVE TO ACCESS KEYS)
|
||||||
|
# =============================================================================
|
||||||
|
# Optional: Use IAM role instead of access keys (recommended for EC2/ECS)
|
||||||
|
# When using IAM roles, you don't need to set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
|
||||||
|
# The role must have appropriate S3 permissions
|
||||||
|
|
||||||
|
# Optional: AWS profile name for credential configuration
|
||||||
|
# Uses credentials from ~/.aws/credentials file
|
||||||
|
# AWS_PROFILE=default
|
||||||
|
|
||||||
|
# Optional: AWS credentials file path
|
||||||
|
# Default: ~/.aws/credentials
|
||||||
|
# AWS_SHARED_CREDENTIALS_FILE=/path/to/credentials
|
||||||
|
|
||||||
|
# Optional: AWS config file path
|
||||||
|
# Default: ~/.aws/config
|
||||||
|
# AWS_CONFIG_FILE=/path/to/config
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MONITORING AND DEBUGGING
|
||||||
|
# =============================================================================
|
||||||
|
# Optional: Enable AWS SDK logging
|
||||||
|
# Set to 'true' for debug output, 'false' to disable
|
||||||
|
# Default: false
|
||||||
|
# AWS_SDK_LOGGING=false
|
||||||
|
|
||||||
|
# Optional: AWS SDK log level
|
||||||
|
# Options: DEBUG, INFO, WARN, ERROR, FATAL
|
||||||
|
# Default: INFO
|
||||||
|
# AWS_SDK_LOG_LEVEL=INFO
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# EXAMPLE CONFIGURATIONS
|
||||||
|
# =============================================================================
|
||||||
|
#
|
||||||
|
# DEVELOPMENT (Local Disk Storage):
|
||||||
|
# Comment out all S3 variables above
|
||||||
|
# The application will use local disk storage automatically
|
||||||
|
#
|
||||||
|
# STAGING (Basic S3):
|
||||||
|
# AWS_ACCESS_KEY_ID=your_staging_access_key
|
||||||
|
# AWS_SECRET_ACCESS_KEY=your_staging_secret_key
|
||||||
|
# AWS_REGION=us-east-1
|
||||||
|
# S3_ATTACHMENTS_BUCKET=your-company-docuseal-staging
|
||||||
|
# ACTIVE_STORAGE_PUBLIC=true
|
||||||
|
#
|
||||||
|
# PRODUCTION (Secure S3):
|
||||||
|
# AWS_ACCESS_KEY_ID=your_production_access_key
|
||||||
|
# AWS_SECRET_ACCESS_KEY=your_production_secret_key
|
||||||
|
# AWS_REGION=us-east-1
|
||||||
|
# S3_ATTACHMENTS_BUCKET=your-company-docuseal-production
|
||||||
|
# ACTIVE_STORAGE_PUBLIC=false
|
||||||
|
# PRESIGNED_URLS_EXPIRE_MINUTES=60
|
||||||
|
# S3_SERVER_SIDE_ENCRYPTION=AES256
|
||||||
|
# S3_STORAGE_CLASS=STANDARD_IA
|
||||||
|
#
|
||||||
|
# MINIO (Self-hosted S3-compatible):
|
||||||
|
# AWS_ACCESS_KEY_ID=minioadmin
|
||||||
|
# AWS_SECRET_ACCESS_KEY=minioadmin
|
||||||
|
# AWS_REGION=us-east-1
|
||||||
|
# S3_ENDPOINT=http://localhost:9000
|
||||||
|
# S3_FORCE_PATH_STYLE=true
|
||||||
|
# S3_ATTACHMENTS_BUCKET=docuseal-minio
|
||||||
|
# ACTIVE_STORAGE_PUBLIC=true
|
||||||
|
#
|
||||||
|
# =============================================================================
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Service for handling secure document access with CloudFront signed URLs
|
||||||
|
# Reuses same infrastructure and key pairs as ATS
|
||||||
|
class DocumentSecurityService
|
||||||
|
class << self
|
||||||
|
# Generate signed URL for a secured attachment
|
||||||
|
# @param attachment [ActiveStorage::Attachment] The attachment to generate URL for
|
||||||
|
# @param expires_in [ActiveSupport::Duration] How long the URL should be valid
|
||||||
|
# @return [String] Signed CloudFront URL
|
||||||
|
def signed_url_for(attachment, expires_in: 1.hour)
|
||||||
|
return attachment.url unless cloudfront_configured?
|
||||||
|
|
||||||
|
# Get the CloudFront URL for this attachment
|
||||||
|
cloudfront_url = build_cloudfront_url(attachment)
|
||||||
|
|
||||||
|
# Generate signed URL using same system as ATS
|
||||||
|
signer = cloudfront_signer
|
||||||
|
signer.signed_url(cloudfront_url, expires: expires_in.from_now.to_i)
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error("Failed to generate signed URL: #{e.message}")
|
||||||
|
# Fallback to direct URL if signing fails
|
||||||
|
attachment.url
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def cloudfront_configured?
|
||||||
|
cloudfront_base_url.present? &&
|
||||||
|
cloudfront_key_pair_id.present? &&
|
||||||
|
cloudfront_private_key.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def cloudfront_signer
|
||||||
|
@cloudfront_signer ||= Aws::CloudFront::UrlSigner.new(
|
||||||
|
key_pair_id: cloudfront_key_pair_id,
|
||||||
|
private_key: cloudfront_private_key
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_cloudfront_url(attachment)
|
||||||
|
# Convert S3 URL to CloudFront URL with DocuSeal prefix
|
||||||
|
s3_key = attachment.blob.key
|
||||||
|
# Ensure DocuSeal prefix for document organization
|
||||||
|
prefixed_key = s3_key.start_with?('docuseal/') ? s3_key : "docuseal/#{s3_key}"
|
||||||
|
"#{cloudfront_base_url}/#{prefixed_key}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def cloudfront_base_url
|
||||||
|
@cloudfront_base_url ||= Rails.configuration.x.compliance_storage&.dig(:cf_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def cloudfront_key_pair_id
|
||||||
|
@cloudfront_key_pair_id ||= Rails.configuration.x.compliance_storage&.dig(:cf_key_pair)
|
||||||
|
end
|
||||||
|
|
||||||
|
def cloudfront_private_key
|
||||||
|
@cloudfront_private_key ||= ENV['SECURE_ATTACHMENT_PRIVATE_KEY']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'aws-sdk-secretsmanager'
|
||||||
|
|
||||||
|
# Load CloudFront private key from AWS Secrets Manager (same as ATS)
|
||||||
|
key_secret = Rails.configuration.x.compliance_storage&.dig(:cf_key_secret)
|
||||||
|
|
||||||
|
if key_secret.present?
|
||||||
|
begin
|
||||||
|
client = Aws::SecretsManager::Client.new
|
||||||
|
response = client.get_secret_value(secret_id: key_secret)
|
||||||
|
ENV['SECURE_ATTACHMENT_PRIVATE_KEY'] = response.secret_string
|
||||||
|
Rails.logger.info("Successfully loaded CloudFront private key from Secrets Manager")
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error("Failed to load CloudFront private key: #{e.message}")
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
class AddStorageLocationToCompletedDocuments < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :completed_documents, :storage_location, :string, default: 'secured'
|
||||||
|
add_index :completed_documents, :storage_location
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in new issue