Koha Security Best Practices

Comprehensive security guide for hardening and securing your Koha AWS deployment.

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: [email protected]
    • 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:



Last Updated: December 2025

Next Steps

More in AWS & Deployment

Was this article helpful?

Thanks for your feedback!