mirror of https://github.com/docusealco/docuseal
Merge pull request #25 from CareerPlug/CP-11289
CP-11289 - Add CloudFront signed URLs for secured document storagepull/544/head
commit
d5e21021d6
@ -0,0 +1,199 @@
|
||||
# =============================================================================
|
||||
# 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
|
||||
|
||||
# =============================================================================
|
||||
# CLOUDFRONT SIGNED URLs (SECURED STORAGE)
|
||||
# =============================================================================
|
||||
# Optional: CloudFront distribution URL for secure document access
|
||||
# Required for secured storage with signed URLs (production deployments)
|
||||
# Format: https://your-cloudfront-domain.cloudfront.net
|
||||
# SECURITY NOTE: Set via cpdocuseal deployment config, not committed to repo
|
||||
# CF_URL=https://example.cloudfront.net
|
||||
|
||||
# Optional: CloudFront key pair ID for signing URLs
|
||||
# Required when using CloudFront signed URLs for document access
|
||||
# SECURITY NOTE: Set via cpdocuseal deployment config, not committed to repo
|
||||
# CF_KEY_PAIR_ID=K1234567890ABC
|
||||
|
||||
# Optional: CloudFront private key secret path in AWS Secrets Manager
|
||||
# The initializer will load the private key from this secret location
|
||||
# Format: environment/cloudfront/private_key
|
||||
# SECURITY NOTE: Set via cpdocuseal deployment config, not committed to repo
|
||||
# CF_KEY_SECRET=production/cloudfront/private_key
|
||||
|
||||
# Optional: Secured storage bucket name (shared with ATS for compliance)
|
||||
# Required when using secured CloudFront storage
|
||||
# SECURITY NOTE: Set via cpdocuseal deployment config, not committed to repo
|
||||
# SECURED_STORAGE_BUCKET=your-company-compliance-documents
|
||||
|
||||
# Optional: Secured storage region
|
||||
# Default: us-east-1
|
||||
# SECURED_STORAGE_REGION=us-east-1
|
||||
|
||||
# Optional: Disable secured storage in development
|
||||
# Set to 'true' to use local disk storage instead of secured S3/CloudFront
|
||||
# Only applies in development environment
|
||||
# DOCUSEAL_DISABLE_SECURED_STORAGE=true
|
||||
|
||||
# =============================================================================
|
||||
# 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 ||= ENV.fetch('CF_URL', nil)
|
||||
end
|
||||
|
||||
def cloudfront_key_pair_id
|
||||
@cloudfront_key_pair_id ||= ENV.fetch('CF_KEY_PAIR_ID', nil)
|
||||
end
|
||||
|
||||
def cloudfront_private_key
|
||||
@cloudfront_private_key ||= ENV.fetch('SECURE_ATTACHMENT_PRIVATE_KEY', nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'aws-sdk-secretsmanager'
|
||||
|
||||
# Load CloudFront private key from AWS Secrets Manager (same as ATS)
|
||||
# Configuration loaded from environment variables (set in cpdocuseal deployment)
|
||||
key_secret = ENV.fetch('CF_KEY_SECRET', nil)
|
||||
|
||||
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