Comprehensive security and hardening guide for Mixpost v2.4.0. Mixpost manages OAuth tokens for social networks and publishes content at scale, making security critical. This guide covers OS hardening, container security, network isolation, credential management, and compliance best practices for Linux DevOps teams.
Current Version: v2.4.0
Security Status: β
No known CVEs (as of February 2026)
Last Security Review: February 2026
| Asset | Threat | Mitigation |
|---|---|---|
| OAuth Tokens | Credential theft | Encryption at rest, vault storage, rotation |
| Database | SQL injection, data breach | Parameterized queries, network isolation, encryption |
| Session Data | Session hijacking | Secure cookies, HTTPS enforcement, timeout |
| API Keys | Unauthorized access | Role-based access, audit logging, rotation |
| Media Files | Malware upload | File type validation, sandboxed processing |
| Queue Jobs | Job injection | Input validation, authentication |
# docker-compose.yml
services:
mixpost:
environment:
# Use Docker secrets or external vault
FACEBOOK_CLIENT_ID: ${FACEBOOK_CLIENT_ID}
FACEBOOK_CLIENT_SECRET: ${FACEBOOK_CLIENT_SECRET}
TWITTER_API_KEY: ${TWITTER_API_KEY}
TWITTER_API_SECRET: ${TWITTER_API_SECRET}
# docker-compose.yml
services:
mixpost:
secrets:
- facebook_client_secret
- twitter_api_secret
secrets:
facebook_client_secret:
external: true
twitter_api_secret:
external: true
# Create secrets
echo "your-secret" | docker secret create facebook_client_secret -
echo "your-secret" | docker secret create twitter_api_secret -
# Store secrets in Vault
vault kv put secret/mixpost/oauth \
facebook_client_id="your-id" \
facebook_client_secret="your-secret" \
twitter_api_key="your-key" \
twitter_api_secret="your-secret"
| Event | Action |
|---|---|
| Staff Departure | Rotate all OAuth credentials immediately |
| Suspected Breach | Rotate affected credentials within 1 hour |
| Quarterly Review | Rotate all credentials proactively |
| Token Expiration | Renew 7 days before expiration |
# Exact match required - no wildcards
APP_URL=https://mixpost.example.com
# Facebook Callback
https://mixpost.example.com/callback/facebook
# Twitter Callback
https://mixpost.example.com/callback/twitter
Security Requirements:
-- Identify inactive social accounts
SELECT id, platform, connected_at, last_used_at
FROM social_accounts
WHERE last_used_at < DATE_SUB(NOW(), INTERVAL 90 DAY)
ORDER BY last_used_at;
-- Remove stale connections (via Mixpost UI recommended)
-- Admin β Settings β Social Accounts β Disconnect
| Role | Permissions | Use Case |
|---|---|---|
| Super Admin | Full system access | Platform administrators (max 2-3 users) |
| Workspace Admin | Full workspace access | Team leads, managers |
| Editor | Create, edit, schedule posts | Content creators |
| Viewer | Read-only access | Stakeholders, clients |
# Limit admin accounts
# Recommended: Maximum 2-3 Super Admins
# Audit current admins (via database)
SELECT id, name, email, role, created_at
FROM users
WHERE role = 'admin'
ORDER BY created_at;
Best Practices:
# Separate workspaces for different brands/clients
Workspace Structure:
βββ Brand A Workspace
β βββ Social Accounts (Brand A only)
β βββ Team Members (Brand A team)
β βββ Posts & Calendar
βββ Brand B Workspace
β βββ Social Accounts (Brand B only)
β βββ Team Members (Brand B team)
β βββ Posts & Calendar
βββ Internal Workspace
βββ Company Social Accounts
βββ Internal Team
Draft β Review β Approval β Schedule β Publish
Permission Levels:
- Draft: All team members
- Review: Editors and above
- Approval: Workspace Admins only
- Schedule: Editors and above
- Publish: Workspace Admins only (for immediate publishing)
# Ensure registration is disabled for private instances
# This is typically configured in the application UI
# Admin β Settings β Registration β Disabled
For public instances:
# docker-compose.yml
networks:
mixpost-internal:
internal: true # No external access
services:
mysql:
networks:
- mixpost-internal # Only accessible from mixpost app
ports: [] # No exposed ports
mixpost:
networks:
- mixpost-internal
- mixpost-public # Only app has public access
# UFW Configuration
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow HTTP/HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Allow SSH (with rate limiting)
sudo ufw limit 22/tcp
# Block database from external access
sudo ufw deny 3306/tcp
sudo ufw deny 6379/tcp
# Enable firewall
sudo ufw enable
# /etc/nginx/sites-available/mixpost
# Redirect all HTTP to HTTPS
server {
listen 80;
server_name mixpost.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name mixpost.example.com;
# SSL configuration
ssl_certificate /etc/letsencrypt/live/mixpost.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mixpost.example.com/privkey.pem;
# Force HTTPS for all requests
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
}
# Laravel session configuration
SESSION_SECURE_COOKIE=true
SESSION_HTTP_ONLY=true
SESSION_SAME_SITE=lax
# /etc/nginx/conf.d/rate-limit.conf
# Define rate limit zones
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=general_limit:10m rate=60r/m;
# Apply to server block
server {
location /login {
limit_req zone=auth_limit burst=3 nodelay;
}
location /api/ {
limit_req zone=api_limit burst=10 nodelay;
}
location / {
limit_req zone=general_limit burst=20 nodelay;
}
}
# /etc/fail2ban/jail.d/mixpost.conf
[mixpost-auth]
enabled = true
filter = mixpost-auth
logpath = /var/www/mixpost/storage/logs/laravel.log
maxretry = 5
bantime = 3600
findtime = 600
# Nginx integration
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 5
bantime = 3600
# docker-compose.yml
services:
mixpost:
image: inovector/mixpost:2.4.0
user: "33:33" # www-data user
security_opt:
- no-new-privileges:true
services:
mixpost:
cap_drop:
- ALL
cap_add:
- CHOWN
- SETGID
- SETUID
- NET_BIND_SERVICE
services:
mixpost:
read_only: true
tmpfs:
- /tmp:noexec,nosuid,size=100m
- /var/run:noexec,nosuid,size=10m
volumes:
- ./storage/app:/var/www/html/storage/app:rw
- ./storage/logs:/var/www/html/storage/logs:rw
services:
mixpost:
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
services:
mixpost:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
mysql:
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 20s
timeout: 10s
retries: 5
redis:
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 20s
timeout: 10s
retries: 3
# docker-compose.prod.yml
version: '3.8'
services:
mixpost:
image: inovector/mixpost:2.4.0
user: "33:33"
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- CHOWN
- SETGID
- SETUID
tmpfs:
- /tmp:noexec,nosuid,size=100m
- /var/run:noexec,nosuid,size=10m
volumes:
- ./storage/app:/var/www/html/storage/app:rw
- ./storage/logs:/var/www/html/storage/logs:rw
networks:
- mixpost-internal
- mixpost-proxy
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/api/health"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
mysql:
image: mysql/mysql-server:8.0
user: "999:999"
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
tmpfs:
- /tmp:noexec,nosuid,size=50m
- /var/run:noexec,nosuid,size=10m
volumes:
- ./db:/var/lib/mysql:rw
networks:
- mixpost-internal
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 20s
timeout: 10s
retries: 5
restart: unless-stopped
redis:
image: redis:7-alpine
user: "999:999"
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- ./redis:/data:rw
networks:
- mixpost-internal
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 20s
timeout: 10s
retries: 3
restart: unless-stopped
networks:
mixpost-internal:
internal: true
driver: bridge
mixpost-proxy:
driver: bridge
#!/bin/bash
# /usr/local/bin/harden-mixpost-firewall.sh
# Reset UFW
sudo ufw --force reset
# Default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH (with rate limiting)
sudo ufw limit ssh
# Allow HTTP/HTTPS
sudo ufw allow http
sudo ufw allow https
# Enable logging
sudo ufw logging on
# Enable firewall
sudo ufw --force enable
# Status
sudo ufw status verbose
#!/bin/bash
# /usr/local/bin/harden-mixpost-firewalld.sh
# Reset firewalld
sudo firewall-cmd --panic-on
sudo firewall-cmd --panic-off
sudo firewall-cmd --remove-all-rules
# Default zone
sudo firewall-cmd --set-default-zone=public
# Allow services
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
# Enable logging
sudo firewall-cmd --permanent --add-log-denied=all
# Reload
sudo firewall-cmd --reload
# Status
sudo firewall-cmd --list-all
# /etc/nginx/conf.d/security-headers.conf
# HSTS (HTTP Strict Transport Security)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Prevent clickjacking
add_header X-Frame-Options "SAMEORIGIN" always;
# Prevent MIME-type sniffing
add_header X-Content-Type-Options "nosniff" always;
# XSS Protection
add_header X-XSS-Protection "1; mode=block" always;
# Referrer Policy
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Content Security Policy
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'self';" always;
# Permissions Policy
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()" always;
# Cache control for sensitive pages
location ~* /(login|admin|settings) {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
add_header Pragma "no-cache" always;
add_header Expires "0" always;
}
Network Architecture:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Public Internet β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DMZ / Proxy Layer β
β βββββββββββββββββββββββββββββββββββββββββββββββ β
β β Nginx Reverse Proxy β β
β β Port 80, 443 β β
β βββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Internal Network (Private) β
β ββββββββββββββββ ββββββββββββββββ βββββββββββββ β
β β Mixpost β β MySQL β β Redis β β
β β Container β β Container β β Container β β
β β Port 8080 β β Port 3306 β βPort 6379 β β
β ββββββββββββββββ ββββββββββββββββ βββββββββββββ β
β β β β β
β ββββββββββββββββββββ΄ββββββββββββββββ β
β Internal Docker Network β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Install Certbot
sudo apt-get install -y certbot python3-certbot-nginx
# Obtain certificate
sudo certbot --nginx -d mixpost.example.com
# Auto-renewal (cron)
sudo crontab -e
# Add: 0 3 * * * certbot renew --quiet
# /etc/nginx/conf.d/ssl-config.conf
# Modern TLS configuration (Mozilla SSL Config Generator)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# SSL session settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# DH parameters (generate with: openssl dhparam -out /etc/nginx/dhparam.pem 4096)
ssl_dhparam /etc/nginx/dhparam.pem;
#!/bin/bash
# /usr/local/bin/check-ssl-expiry.sh
DOMAIN="mixpost.example.com"
EXPIRY=$(echo | openssl s_client -connect $DOMAIN:443 -servername $DOMAIN 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( ($EXPIRY_EPOCH - $NOW_EPOCH) / 86400 ))
if [ $DAYS_LEFT -lt 30 ]; then
echo "WARNING: SSL certificate expires in $DAYS_LEFT days"
# Send alert (email, Slack, etc.)
fi
# Application-level password requirements
# Configure in Mixpost UI: Admin β Settings β Security
Minimum Length: 12 characters
Require Uppercase: Yes
Require Lowercase: Yes
Require Numbers: Yes
Require Special Characters: Yes
Password History: Last 5 passwords
Maximum Age: 90 days
| Method | Best For | Complexity |
|---|---|---|
| Environment Variables | Small deployments | Low |
| Docker Secrets | Docker Swarm | Medium |
| HashiCorp Vault | Enterprise | High |
| AWS Secrets Manager | AWS deployments | Medium |
| Kubernetes Secrets | Kubernetes | Medium |
#!/bin/bash
# /usr/local/bin/rotate-mixpost-secrets.sh
set -e
echo "=== Mixpost Credential Rotation ==="
# Generate new passwords
DB_PASSWORD=$(openssl rand -base64 32)
REDIS_PASSWORD=$(openssl rand -base64 32)
APP_KEY=$(docker run --rm php:8.2-cli php -r "echo 'base64:' . base64_encode(random_bytes(32)) . PHP_EOL;")
# Backup current configuration
cp /opt/mixpost/.env /opt/mixpost/.env.backup.$(date +%Y%m%d)
# Update .env file
sed -i "s/^DB_PASSWORD=.*/DB_PASSWORD=$DB_PASSWORD/" /opt/mixpost/.env
sed -i "s/^REDIS_PASSWORD=.*/REDIS_PASSWORD=$REDIS_PASSWORD/" /opt/mixpost/.env
sed -i "s/^APP_KEY=.*/APP_KEY=$APP_KEY/" /opt/mixpost/.env
# Restart services
cd /opt/mixpost
docker compose down
docker compose up -d
echo "Credentials rotated successfully!"
echo "IMPORTANT: Update your password vault with new credentials"
# Log location (Docker)
docker compose exec mixpost tail -f /var/www/html/storage/logs/laravel.log
# Log location (Manual)
sudo tail -f /var/www/mixpost/storage/logs/laravel.log
# Log rotation (Manual)
sudo nano /etc/logrotate.d/mixpost
# /etc/logrotate.d/mixpost
/var/www/mixpost/storage/logs/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 www-data www-data
sharedscripts
postrotate
systemctl reload php8.2-fpm
endscript
}
#!/bin/bash
# /usr/local/bin/monitor-mixpost-security.sh
LOG_FILE="/var/www/mixpost/storage/logs/laravel.log"
# Monitor for failed login attempts
grep -i "failed login" $LOG_FILE | tail -100
# Monitor for OAuth errors
grep -i "oauth.*error" $LOG_FILE | tail -50
# Monitor for SQL errors
grep -i "sql.*error" $LOG_FILE | tail -50
# Monitor for permission errors
grep -i "permission.*denied" $LOG_FILE | tail -50
# docker-compose.yml with Fluentd
services:
mixpost:
logging:
driver: fluentd
options:
fluentd-address: localhost:24224
tag: mixpost.app
fluentd:
image: fluent/fluentd:v1.16
volumes:
- ./fluentd/conf:/fluentd/etc
ports:
- "24224:24224"
#!/bin/bash
# /usr/local/bin/backup-mixpost-secure.sh
set -e
BACKUP_DIR="/backup/mixpost"
DATE=$(date +%Y%m%d_%H%M%S)
ENCRYPTION_KEY="/etc/mixpost/backup-key.gpg"
mkdir -p $BACKUP_DIR
# Database backup
mysqldump -u mixpost -p"$DB_PASSWORD" \
--single-transaction \
mixpost | gzip > $BACKUP_DIR/db-$DATE.sql.gz
# Encrypt database backup
gpg --encrypt --recipient backup@example.com \
--output $BACKUP_DIR/db-$DATE.sql.gz.gpg \
$BACKUP_DIR/db-$DATE.sql.gz
rm $BACKUP_DIR/db-$DATE.sql.gz
# Storage backup (encrypted archive)
tar czf - /opt/mixpost/storage/app | \
gpg --encrypt --recipient backup@example.com \
--output $BACKUP_DIR/storage-$DATE.tar.gz.gpg
# Upload to remote storage (optional)
# aws s3 cp $BACKUP_DIR s3://backup-bucket/mixpost/
# Keep 30 days
find $BACKUP_DIR -name "*.gpg" -mtime +30 -delete
echo "Backup completed: $DATE"
#!/bin/bash
# /usr/local/bin/verify-mixpost-backup.sh
BACKUP_FILE="$1"
# Decrypt and test
gpg --decrypt $BACKUP_FILE | gzip -t
if [ $? -eq 0 ]; then
echo "Backup verification: PASSED"
else
echo "Backup verification: FAILED"
exit 1
fi
| Incident | Response Time | Actions |
|---|---|---|
| Credential Leak | Immediate | Rotate credentials, audit logs, notify affected users |
| Unauthorized Access | 1 hour | Revoke sessions, investigate, patch vulnerability |
| Data Breach | Immediate | Contain, assess scope, notify stakeholders, legal review |
| DDoS Attack | Immediate | Enable rate limiting, contact hosting, activate CDN |
| Malware Detection | 1 hour | Isolate system, scan, restore from clean backup |
Security Incident Response Team:
- Primary: security@example.com
- Secondary: ops@example.com
- Escalation: cto@example.com
External:
- Hosting Provider: support@hosting.com
- Domain Registrar: security@registrar.com
- Legal: legal@example.com
#!/bin/bash
# /usr/local/bin/collect-forensics.sh
EVIDENCE_DIR="/evidence/mixpost-$(date +%Y%m%d-%H%M%S)"
mkdir -p $EVIDENCE_DIR
# System state
ps aux > $EVIDENCE_DIR/processes.txt
netstat -tulpn > $EVIDENCE_DIR/network.txt
last -100 > $EVIDENCE_DIR/logins.txt
# Application logs
cp /var/www/mixpost/storage/logs/*.log $EVIDENCE_DIR/
# Web server logs
cp /var/log/nginx/*.log $EVIDENCE_DIR/
# Database queries (if slow query log enabled)
cp /var/log/mysql/*.log $EVIDENCE_DIR/
# Compress evidence
tar czf $EVIDENCE_DIR.tar.gz $EVIDENCE_DIR
shred -u -z $EVIDENCE_DIR -r
echo "Evidence collected: $EVIDENCE_DIR.tar.gz"
Any questions?
Feel free to contact us. Find all contact information on our contact page.