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:

  1. 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"
    
  2. 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"
    
  3. 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
    
  4. 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:

  1. Strong password policy:
    # In Koha: Administration → System preferences → Admin
    # Set:
    minPasswordLength: 12
    RequireStrongPassword: Require
    EnableExpiredPasswordReset: Allow
    
  2. 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
    
  3. 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
    
  4. 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:

  1. Anonymize patron data:
    Administration → System preferences → Privacy
       
    - AnonymousPatron: Set to anonymous patron account
    - Privacy: Allow patrons to choose
    - PatronPrivacySettings: Enable
    
  2. 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
    
  3. 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:

  1. Never store credit card details in Koha
  2. Use PCI-compliant payment gateways (PayPal, Stripe)
  3. Enable SSL/TLS for all payment pages
  4. Restrict access to payment configuration
  5. 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:

  1. 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
    
  2. 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/
    
  3. Create forensic snapshot:
    aws ec2 create-snapshot \
      --volume-id vol-xxxxx \
      --description "Forensic snapshot - incident $(date +%Y%m%d)"
    
  4. 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"
    
  5. Contact support:
    • Email: security@kohasupport.com
    • Subject: “SECURITY INCIDENT - [Your Library]”
    • Include: Incident details, timeline, affected systems

Post-Incident Actions

  1. Rotate all credentials:
    • SSH keys
    • Database passwords
    • Koha admin passwords
    • AWS access keys
  2. Review and update security:
    • Patch all systems
    • Update security group rules
    • Enable additional logging
    • Implement monitoring alerts
  3. 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]”


Last Updated: December 2025