Complete guide for securing your Koha deployment on AWS, covering access control, encryption, compliance, and security monitoring.
Security Overview
Security Layers
┌─────────────────────────────────────────┐
│ Application Layer (Koha) │ ← Koha security settings
├─────────────────────────────────────────┤
│ Web Server Layer (Apache) │ ← SSL/TLS, headers
├─────────────────────────────────────────┤
│ Database Layer (MySQL/Aurora) │ ← Encryption, access control
├─────────────────────────────────────────┤
│ Instance Layer (EC2) │ ← Patching, hardening
├─────────────────────────────────────────┤
│ Network Layer (VPC, SG) │ ← Firewalls, isolation
├─────────────────────────────────────────┤
│ Infrastructure Layer (AWS) │ ← IAM, encryption
└─────────────────────────────────────────┘
Security Responsibility Model
AWS Responsibilities:
- Physical data center security
- Network infrastructure security
- Hypervisor security
- Managed service security (RDS, S3)
Your Responsibilities:
- Operating system patching
- Application security (Koha)
- Network configuration (security groups)
- Access management (IAM, SSH keys)
- Data encryption
- Backup security
Network Security
Security Groups Configuration
Basic/Standard Tier
Recommended security group rules:
# SSH access (restrict to your IP)
Type: SSH (22)
Protocol: TCP
Port: 22
Source: YOUR_IP_ADDRESS/32
Description: Admin SSH access
# HTTP (redirect to HTTPS)
Type: HTTP (80)
Protocol: TCP
Port: 80
Source: 0.0.0.0/0
Description: Public HTTP (redirects to HTTPS)
# HTTPS
Type: HTTPS (443)
Protocol: TCP
Port: 443
Source: 0.0.0.0/0
Description: Public HTTPS access
# Outbound (all traffic)
Type: All traffic
Protocol: All
Port: All
Destination: 0.0.0.0/0
Description: Allow all outbound
Update security group via CLI:
# Get security group ID
SG_ID=$(aws ec2 describe-instances \
--instance-ids i-xxxxx \
--query 'Reservations[0].Instances[0].SecurityGroups[0].GroupId' \
--output text)
# Remove overly permissive SSH rule (if exists)
aws ec2 revoke-security-group-ingress \
--group-id $SG_ID \
--protocol tcp \
--port 22 \
--cidr 0.0.0.0/0
# Add restricted SSH rule
aws ec2 authorize-security-group-ingress \
--group-id $SG_ID \
--protocol tcp \
--port 22 \
--cidr YOUR_IP/32 \
--group-name "Admin SSH"
Enterprise Tier
Additional considerations:
- ALB security group: Only 80/443 from 0.0.0.0/0
- Instance security group: Only 80/443 from ALB security group
- No direct SSH access: Use Session Manager or EC2 Instance Connect
- Aurora security group: Only 3306 from instance security group
Verify Enterprise security:
# List all security groups
aws ec2 describe-security-groups \
--filters "Name=tag:aws:cloudformation:stack-name,Values=your-stack-name" \
--query 'SecurityGroups[*].[GroupName,GroupId]' \
--output table
# Check ALB security group rules
aws ec2 describe-security-groups \
--group-ids sg-xxxxx \
--query 'SecurityGroups[0].IpPermissions'
VPC Configuration
Default VPC considerations:
- Public subnet: Instances get public IPs
- Internet Gateway: Allows outbound internet access
- Route table: Routes traffic to internet
Enhanced VPC security (Enterprise):
# Create private subnet for database
aws ec2 create-subnet \
--vpc-id vpc-xxxxx \
--cidr-block 10.0.2.0/24 \
--availability-zone us-east-1a
# Move Aurora to private subnet
# (During initial deployment via CloudFormation)
Network Access Control Lists (NACLs)
For additional security layer:
# Create NACL
aws ec2 create-network-acl --vpc-id vpc-xxxxx
# Allow HTTPS inbound
aws ec2 create-network-acl-entry \
--network-acl-id acl-xxxxx \
--ingress \
--rule-number 100 \
--protocol tcp \
--port-range From=443,To=443 \
--cidr-block 0.0.0.0/0 \
--rule-action allow
# Allow SSH from specific IP
aws ec2 create-network-acl-entry \
--network-acl-id acl-xxxxx \
--ingress \
--rule-number 110 \
--protocol tcp \
--port-range From=22,To=22 \
--cidr-block YOUR_IP/32 \
--rule-action allow
Access Control
SSH Key Management
Best practices:
- Use separate keys for each environment:
# Generate environment-specific keys ssh-keygen -t ed25519 -f ~/.ssh/koha-production -C "koha-prod" ssh-keygen -t ed25519 -f ~/.ssh/koha-staging -C "koha-stage" - Rotate keys regularly (every 90 days):
# Generate new key ssh-keygen -t ed25519 -f ~/.ssh/koha-production-new # Add new key to instance ssh -i ~/.ssh/koha-production ubuntu@instance \ "echo 'NEW_PUBLIC_KEY' >> ~/.ssh/authorized_keys" # Test new key ssh -i ~/.ssh/koha-production-new ubuntu@instance # Remove old key ssh -i ~/.ssh/koha-production-new ubuntu@instance \ "sed -i '/OLD_KEY_FINGERPRINT/d' ~/.ssh/authorized_keys" - Protect private keys:
chmod 600 ~/.ssh/koha-production chmod 644 ~/.ssh/koha-production.pub # Encrypt private key openssl enc -aes-256-cbc -in ~/.ssh/koha-production \ -out ~/.ssh/koha-production.enc - Use SSH config file:
cat >> ~/.ssh/config << EOF Host koha-prod HostName X.X.X.X User ubuntu IdentityFile ~/.ssh/koha-production IdentitiesOnly yes EOF chmod 600 ~/.ssh/config # Connect simply ssh koha-prod
AWS IAM Security
Create dedicated IAM user for Koha management:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:StartInstances",
"ec2:StopInstances",
"ec2-instance-connect:SendSSHPublicKey",
"cloudformation:DescribeStacks",
"cloudformation:DescribeStackResources",
"cloudwatch:GetMetricStatistics",
"cloudwatch:ListMetrics",
"s3:ListBucket",
"s3:GetObject",
"s3:PutObject"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:RequestedRegion": "us-east-1"
}
}
}
]
}
Enable MFA for IAM users:
# Generate QR code for MFA setup
aws iam create-virtual-mfa-device \
--virtual-mfa-device-name koha-admin \
--outfile /tmp/QRCode.png \
--bootstrap-method QRCodePNG
# Enable MFA
aws iam enable-mfa-device \
--user-name koha-admin \
--serial-number arn:aws:iam::ACCOUNT:mfa/koha-admin \
--authentication-code1 123456 \
--authentication-code2 789012
Koha User Access Control
Staff interface security:
- Strong password policy:
# In Koha: Administration → System preferences → Admin # Set: minPasswordLength: 12 RequireStrongPassword: Require EnableExpiredPasswordReset: Allow - Review user permissions regularly:
-- List superlibrarians sudo koha-mysql library << EOF SELECT borrowernumber, userid, firstname, surname, email FROM borrowers WHERE flags = 1; EOF -- List users with specific permissions sudo koha-mysql library << EOF SELECT b.borrowernumber, b.userid, b.firstname, b.surname, up.code FROM borrowers b JOIN user_permissions up ON b.borrowernumber = up.borrower_number WHERE up.code IN ('superlibrarian', 'parameters', 'manage_sysprefs'); EOF - Disable unused accounts:
# Find inactive accounts sudo koha-mysql library << EOF SELECT borrowernumber, userid, firstname, surname, DATE(lastseen) as last_seen FROM borrowers WHERE categorycode = 'STAFF' AND lastseen < DATE_SUB(NOW(), INTERVAL 90 DAY) ORDER BY lastseen; EOF # Disable account in staff interface: # Patrons → Search for user → Edit → Set expired - Enable session timeout:
# In Koha: Administration → System preferences → Admin # Set: timeout: 600 # 10 minutes (in seconds)
Data Encryption
Encryption at Rest
EC2 EBS Volumes
Check if volumes are encrypted:
aws ec2 describe-volumes \
--filters "Name=attachment.instance-id,Values=i-xxxxx" \
--query 'Volumes[*].[VolumeId,Encrypted,Size]' \
--output table
Encrypt existing unencrypted volume:
# 1. Create snapshot
aws ec2 create-snapshot \
--volume-id vol-xxxxx \
--description "Snapshot before encryption"
# 2. Copy snapshot with encryption
aws ec2 copy-snapshot \
--source-region us-east-1 \
--source-snapshot-id snap-xxxxx \
--destination-region us-east-1 \
--encrypted \
--kms-key-id alias/aws/ebs
# 3. Create volume from encrypted snapshot
aws ec2 create-volume \
--snapshot-id snap-yyyyy \
--availability-zone us-east-1a \
--encrypted
# 4. Stop instance, detach old volume, attach new encrypted volume
# (Requires downtime - plan accordingly)
Aurora Database (Enterprise)
Encryption is enabled by default in templates.
Verify encryption:
aws rds describe-db-clusters \
--db-cluster-identifier your-cluster \
--query 'DBClusters[0].[DBClusterIdentifier,StorageEncrypted,KmsKeyId]'
S3 Bucket Encryption (Standard/Enterprise)
Enable default encryption:
# Get bucket name from CloudFormation outputs
BUCKET_NAME="your-backup-bucket"
# Enable encryption
aws s3api put-bucket-encryption \
--bucket $BUCKET_NAME \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
},
"BucketKeyEnabled": true
}]
}'
# Verify
aws s3api get-bucket-encryption --bucket $BUCKET_NAME
Encryption in Transit
SSL/TLS Configuration
For Standard tier with custom domain:
# Verify SSL certificate
echo | openssl s_client -connect library.yourdomain.com:443 2>/dev/null | \
openssl x509 -noout -dates
# Check SSL configuration
sudo apache2ctl -M | grep ssl
sudo apache2ctl -t -D DUMP_VHOSTS | grep 443
Enforce HTTPS:
# Add redirect in Apache config
sudo tee -a /etc/apache2/sites-available/000-default.conf > /dev/null << EOF
<VirtualHost *:80>
ServerName library.yourdomain.com
Redirect permanent / https://library.yourdomain.com/
</VirtualHost>
EOF
sudo systemctl reload apache2
Strengthen SSL/TLS:
# Edit Koha SSL config
sudo nano /etc/apache2/sites-available/koha-library-ssl.conf
# Add/modify:
SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLCipherSuite HIGH:!aNULL:!MD5:!3DES
SSLHonorCipherOrder on
SSLCompression off
# Enable HSTS
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
# Test configuration
sudo apache2ctl configtest
# Apply
sudo systemctl reload apache2
Database Connection Encryption
For Enterprise Aurora:
# Aurora connections are encrypted by default
# Verify in RDS console or via CLI
aws rds describe-db-clusters \
--db-cluster-identifier your-cluster \
--query 'DBClusters[0].EnabledCloudwatchLogsExports'
For Basic/Standard local MySQL:
# Enable SSL for MySQL connections
sudo mysql << EOF
CREATE USER 'koha_library'@'localhost' REQUIRE SSL;
GRANT ALL ON koha_library.* TO 'koha_library'@'localhost';
FLUSH PRIVILEGES;
EOF
Application Security
Apache Hardening
Security headers:
# Enable headers module
sudo a2enmod headers
# Edit Apache config
sudo nano /etc/apache2/conf-available/security.conf
# Add security headers:
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
# Remove server signature
ServerTokens Prod
ServerSignature Off
# Enable configuration
sudo a2enconf security
sudo systemctl reload apache2
Verify headers:
curl -I https://library.yourdomain.com/ | grep -E "X-Frame|X-Content|X-XSS|Server:"
Koha Security Settings
System preferences to review:
Administration → System preferences → Security
- AutoLocation: Turn off (use manual IP mapping if needed)
- AllowMultipleIssuesOnABiblio: Consider disabling
- AllowNotForLoanOverride: Consider restricting
- AllowRenewalLimitOverride: Consider restricting
- IndependentBranches: Enable if using multiple branches
- IndependentBranchesPatronModifications: Enable
- SessionRestrictionByIP: Enable
- TrackLastPatronActivity: Enable (for security auditing)
Enable audit logging:
Administration → System preferences → Logs
- BorrowersLog: Track
- CataloguingLog: Track
- IssueLog: Track
- LetterLog: Track
- FinesLog: Track
- AuthoritiesLog: Track
- ReportsLog: Track
- AuthSuccessLog: Track
- AuthFailureLog: Track
Database Security
Basic/Standard tier:
# Secure MySQL installation
sudo mysql_secure_installation
# Remove test databases
sudo mysql << EOF
DROP DATABASE IF EXISTS test;
DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';
FLUSH PRIVILEGES;
EOF
# Restrict remote access (should be localhost only)
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
# Ensure: bind-address = 127.0.0.1
# Review MySQL users
sudo mysql -e "SELECT User, Host FROM mysql.user;"
# Remove unused users
sudo mysql -e "DROP USER 'username'@'host';"
Aurora (Enterprise):
# Verify security group only allows access from app instances
aws ec2 describe-security-groups \
--group-ids sg-xxxxx \
--query 'SecurityGroups[0].IpPermissions'
# Enable audit logging
aws rds modify-db-cluster \
--db-cluster-identifier your-cluster \
--cloudwatch-logs-export-configuration '{"EnableLogTypes":["audit","error","general","slowquery"]}'
Security Monitoring
CloudTrail Logging
Enable CloudTrail for API auditing:
# Create S3 bucket for logs
aws s3 mb s3://koha-cloudtrail-logs-$RANDOM
# Create trail
aws cloudtrail create-trail \
--name koha-audit-trail \
--s3-bucket-name koha-cloudtrail-logs-xxxxx
# Start logging
aws cloudtrail start-logging --name koha-audit-trail
# Verify
aws cloudtrail get-trail-status --name koha-audit-trail
VPC Flow Logs
Monitor network traffic:
# Create CloudWatch log group
aws logs create-log-group --log-group-name /aws/vpc/flowlogs
# Enable VPC flow logs
aws ec2 create-flow-logs \
--resource-type VPC \
--resource-ids vpc-xxxxx \
--traffic-type ALL \
--log-destination-type cloud-watch-logs \
--log-group-name /aws/vpc/flowlogs \
--deliver-logs-permission-arn arn:aws:iam::ACCOUNT:role/flowlogsRole
Security Scanning
Install and configure fail2ban:
# Install
sudo apt-get install -y fail2ban
# Configure for Koha
sudo tee /etc/fail2ban/jail.local > /dev/null << EOF
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
[sshd]
enabled = true
port = 22
logpath = /var/log/auth.log
[apache-auth]
enabled = true
port = http,https
logpath = /var/log/apache2/*error.log
maxretry = 3
[koha-opac]
enabled = true
port = http,https
logpath = /var/log/koha/library/opac-error.log
maxretry = 10
findtime = 300
EOF
# Start service
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
# Check status
sudo fail2ban-client status
Vulnerability scanning:
# Install lynis
sudo apt-get install -y lynis
# Run security audit
sudo lynis audit system
# Review results
sudo cat /var/log/lynis.log
Intrusion Detection
Install OSSEC (optional):
# Download and install
wget https://github.com/ossec/ossec-hids/archive/3.7.0.tar.gz
tar -xvzf 3.7.0.tar.gz
cd ossec-hids-3.7.0
sudo ./install.sh
# Configure
sudo nano /var/ossec/etc/ossec.conf
# Start
sudo /var/ossec/bin/ossec-control start
# Check alerts
sudo tail -f /var/ossec/logs/alerts/alerts.log
Compliance & Data Protection
GDPR Compliance
Koha GDPR features:
- Anonymize patron data:
Administration → System preferences → Privacy - AnonymousPatron: Set to anonymous patron account - Privacy: Allow patrons to choose - PatronPrivacySettings: Enable - Configure data retention:
# Cronjob to anonymize old issues sudo crontab -e -u root # Add: 0 2 * * * /usr/share/koha/bin/cronjobs/batch_anonymise.pl --days 90 - Export patron data (GDPR requests):
sudo koha-mysql library << EOF SELECT * FROM borrowers WHERE borrowernumber = PATRON_ID; SELECT * FROM issues WHERE borrowernumber = PATRON_ID; SELECT * FROM old_issues WHERE borrowernumber = PATRON_ID; EOF
PCI DSS (If Accepting Payments)
If using payment processing:
- Never store credit card details in Koha
- Use PCI-compliant payment gateways (PayPal, Stripe)
- Enable SSL/TLS for all payment pages
- Restrict access to payment configuration
- Log all transactions
Data Backup Security
Encrypt backups:
# For Basic/Standard tier
# Encrypt mysqldump backup
sudo mysqldump koha_library | \
openssl enc -aes-256-cbc -pbkdf2 -out backup.sql.enc
# Decrypt when needed
openssl enc -d -aes-256-cbc -pbkdf2 -in backup.sql.enc -out backup.sql
S3 bucket security (Standard/Enterprise):
# Enable versioning
aws s3api put-bucket-versioning \
--bucket your-backup-bucket \
--versioning-configuration Status=Enabled
# Enable MFA delete (highly recommended)
aws s3api put-bucket-versioning \
--bucket your-backup-bucket \
--versioning-configuration Status=Enabled,MFADelete=Enabled \
--mfa "arn:aws:iam::ACCOUNT:mfa/root-account-mfa-device 123456"
# Block public access
aws s3api put-public-access-block \
--bucket your-backup-bucket \
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
Incident Response
Security Incident Checklist
If you suspect a security breach:
- Isolate affected systems:
# Remove from internet (if safe to do so) # Update security groups to restrict access aws ec2 modify-instance-attribute \ --instance-id i-xxxxx \ --groups sg-xxxxxxxx # Emergency restricted SG - Collect evidence:
# Capture logs before they rotate sudo tar -czf /tmp/incident-logs-$(date +%Y%m%d).tar.gz \ /var/log/koha/ \ /var/log/apache2/ \ /var/log/mysql/ \ /var/log/auth.log \ /var/log/syslog # Upload to secure location aws s3 cp /tmp/incident-logs-*.tar.gz s3://security-incidents/ - Create forensic snapshot:
aws ec2 create-snapshot \ --volume-id vol-xxxxx \ --description "Forensic snapshot - incident $(date +%Y%m%d)" - Review access logs:
# Check for unauthorized SSH logins sudo grep "Accepted publickey" /var/log/auth.log # Check for sudo usage sudo grep "sudo:" /var/log/auth.log # Check Apache access logs for suspicious activity sudo grep -E "POST|DELETE" /var/log/apache2/access.log | \ grep -vE "cgi-bin/koha" - Contact support:
- Email: security@kohasupport.com
- Subject: “SECURITY INCIDENT - [Your Library]”
- Include: Incident details, timeline, affected systems
Post-Incident Actions
- Rotate all credentials:
- SSH keys
- Database passwords
- Koha admin passwords
- AWS access keys
- Review and update security:
- Patch all systems
- Update security group rules
- Enable additional logging
- Implement monitoring alerts
- Document lessons learned:
- What happened
- How it was detected
- How it was resolved
- How to prevent future incidents
Security Checklist
Daily
- Review CloudWatch alarms
- Check fail2ban logs
- Monitor unusual activity
Weekly
- Review access logs
- Check for security updates
- Verify backups are encrypted
Monthly
- Review IAM permissions
- Audit user accounts
- Update security documentation
- Test backup restoration
Quarterly
- Rotate SSH keys
- Run vulnerability scan
- Review security group rules
- Security awareness training
- Disaster recovery drill
Annually
- Comprehensive security audit
- Penetration testing (if required)
- Update incident response plan
- Review compliance requirements
Security Resources
AWS Security Resources
Koha Security Resources
Getting Help
For security assistance:
- General: support@kohasupport.com
- Security incidents: security@kohasupport.com
- Subject: “Security - [Your Library]”
Related Documentation
Last Updated: December 2025