Skip to content

Security Model

Jagad is designed with a defense-in-depth approach to security, protecting backup data at every stage: at rest in the local database, in transit over the network, and at rest in S3-compatible object storage.


Overview

┌─────────────────────────────────────────────────────────────────────┐
│                        Security Layers                               │
│                                                                     │
│  1. In Transit (TLS)                   ┌────────────────────────┐   │
│     ┌──────────┐   HTTPS   ┌──────────┐│  S3/API over HTTPS     │   │
│     │ Browser  │──────────▶│ Backend  ││  (TLS 1.2+ mandatory)  │   │
│     └──────────┘           └──────────┘│                        │   │
│                                        │  SQLite (local)        │   │
│  2. Authentication                     │  ┌──────────────────┐  │   │
│     ┌───────────────┐                  │  │ Encrypted at     │  │   │
│     │ Session-based  │                  │  │ rest (credentials)│  │   │
│     │ + HttpOnly     │                  │  └──────────────────┘  │   │
│     │   cookies      │                  └────────────────────────┘   │
│     └───────────────┘                                               │
│                                                                     │
│  3. Encryption at Rest (AES-256-GCM)                                │
│     ┌──────────────────────────────────────────────────────────┐    │
│     │  Backup Stream: pg_dump → gzip → AES-256-GCM → S3       │    │
│     │  Master Key: Argon2id KDF → derived key per stream      │    │
│     │  Credentials: AES-256-GCM encrypted in SQLite           │    │
│     └──────────────────────────────────────────────────────────┘    │
│                                                                     │
│  4. Least Privilege                                                │
│     ┌──────────────────────────────────────────────────────────┐    │
│     │  DB users: only needed permissions (SELECT, LOCK, etc.)  │    │
│     │  S3 users: only PutObject/GetObject/ListBucket/Delete    │    │
│     └──────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────┘

Encryption at Rest

Backup Data Encryption (AES-256-GCM)

Backup data is encrypted client-side before being uploaded to S3. The encryption happens in the streaming pipeline, so data never exists in plaintext outside the Go process.

Algorithm: AES-256-GCM (Galois/Counter Mode)

Key Properties:

  • Authenticated encryption: GCM provides both confidentiality and integrity verification
  • Nonce reuse resistance: Counter-based nonces ensure unique nonces per frame
  • Streaming support: Chunked framing enables encrypting data of unknown size

Key Derivation

The master encryption key is provided via the JAGAD_ENCRYPTION_KEY environment variable. Before each backup stream, a random salt is generated, and Argon2id derives the actual encryption key:

go
func (a *aesgcm) deriveKey(salt []byte) []byte {
    return argon2.IDKey(a.masterKey, salt, 1, 64*1024, 4, 32)
}

Argon2id parameters:

ParameterValueRationale
Time (iterations)1Balance security/speed for frequent operations
Memory64 MBModerate memory hardness
Parallelism4Matches common CPU core count
Key length32 bytes (256 bits)AES-256 key size
Salt16 bytes randomUnique per stream

This means:

  • Each backup stream uses a unique derived key from the same master key
  • Compromising one backup does not compromise others (different salt → different key)
  • The master key never leaves the Jagad process

Stream Encryption Format

┌──────────────────────────────────────────────────────────┐
│  Encrypted Stream Layout                                 │
│                                                          │
│  Bytes 0-15:    Salt (16 bytes, random per stream)       │
│  Bytes 16-27:   Frame 1 header (nonce 12B + len 4B)     │
│  Bytes 28-N:    Frame 1 ciphertext (variable)            │
│  ...            More frames...                           │
│  Final 16 bytes: EOF marker (all zeros)                  │
└──────────────────────────────────────────────────────────┘

Each frame is independently decryptable. See Streaming Pipeline for full details.

Credential Encryption at Rest

S3 access keys and secrets are encrypted in the SQLite database:

go
type CredentialEncryptor struct {
    key []byte  // SHA-256 of master key phrase
}

func (e *CredentialEncryptor) Encrypt(plaintext []byte) ([]byte, error) {
    // AES-256-GCM with random nonce
    // Output: [nonce(12)][ciphertext+tag]
}

Key management:

  • The credential encryption key is derived from JAGAD_MASTER_KEY (SHA-256)
  • If no master key is set, a default fallback is used ("jagad-default-credential-key")
  • Recommendation: Always set JAGAD_MASTER_KEY in production to a strong, unique secret
  • Credentials are decrypted only when needed (before S3 operations)

Checksum Verification

Jagad computes a SHA-256 checksum of the compressed backup data (before encryption) during the streaming upload. This checksum is stored in the SQLite database alongside the backup record and can be verified on restore:

go
hashWriter := sha256.New()
// During streaming: sha256 receives compressed data via io.MultiWriter
gw := gzip.NewWriter(io.MultiWriter(hashWriter, encWriter))
io.Copy(gw, dumpReader)
// Store checksum
b.Checksum = hex.EncodeToString(hashWriter.Sum(nil))

Encryption in Transit

API and Web UI (TLS)

In the standard Docker Compose deployment, Nginx terminates TLS for the Web UI and API:

Browser ──▶ Nginx (TLS) ──▶ Go Backend (HTTP)
              :443                :8080

Configuration recommendations:

  • Use TLS 1.2 or higher
  • Disable weak cipher suites
  • Use Let's Encrypt or a trusted CA certificate
  • Enable HSTS headers

S3 Storage (HTTPS)

All S3-compatible storage communication uses HTTPS by default:

go
client, err := minio.New(cfg.Endpoint, &minio.Options{
    Creds:  credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""),
    Secure: true,  // HTTPS enforced
})

The Secure: true setting ensures TLS for all S3 API calls. This protects:

  • Backup data in transit between Jagad and S3
  • Access key and secret key during authentication
  • Integrity verification via TLS MAC

Database Connections

Database connections are established using the native client tools (pg_dump, mysqldump, etc.) with password authentication over TCP.

Recommendations:

  • For PostgreSQL: enable SSL connections with sslmode=require
  • For MySQL: use --ssl-mode=REQUIRED
  • Never use default passwords for database users

Authentication and Authorization

Session-Based Authentication

Jagad uses a simple session-based auth system:

go
type Service struct {
    adminUser string
    adminPass string  // SHA-256 hash
    secretKey string
    sessions  map[string]sessionInfo
}

Login flow:

  1. User submits username/password via POST /api/auth/login
  2. Backend verifies password against SHA-256 hash
  3. On success, generates a random 32-byte session token
  4. Token is stored in an in-memory map with a 24-hour expiry
  5. An HttpOnly cookie is set on the response

Session validation:

  • Token can be sent as a cookie (session) or Authorization header
  • Validated on every API request (except login, health, and static files)
  • Sessions expire after 24 hours of inactivity
  • Logout invalidates the session token

Password Storage

Passwords are stored as SHA-256 hashes:

go
h := sha256.Sum256([]byte(adminPass))
hashedPass := hex.EncodeToString(h[:])

Note: While SHA-256 is used for simplicity in this single-admin tool, production deployments should consider using bcrypt/argon2 for password hashing. This is on the roadmap.

Change Password

Password changes invalidate all existing sessions:

go
func (s *Service) ChangePassword(currentPass, newPass string) error {
    // Verify current password
    // Hash new password
    // Clear all sessions → user must re-login
}

API Protection

The auth middleware protects all API routes:

go
func (s *Service) Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Skip: /api/auth/login, /api/health, /
        // Validate session token from cookie or header
        // Return 401 if invalid
        next.ServeHTTP(w, r)
    })
}

Secure Configuration File Handling

Environment Variables

All sensitive configuration is passed via environment variables, never written to configuration files:

VariableSensitivityDescription
JAGAD_ADMIN_PASSHighAdmin password
JAGAD_SECRET_KEYHighSession signing secret
JAGAD_ENCRYPTION_KEYCriticalMaster key for backup encryption
JAGAD_MASTER_KEYCriticalMaster key for credential encryption
JAGAD_S3_ACCESS_KEYHighS3 access key
JAGAD_S3_SECRET_KEYCriticalS3 secret key

Docker Secrets

When using Docker Compose, pass sensitive values via environment files or Docker secrets:

yaml
# docker-compose.yml
services:
  backend:
    environment:
      - JAGAD_ENCRYPTION_KEY=${JAGAD_ENCRYPTION_KEY}
      - JAGAD_MASTER_KEY=${JAGAD_MASTER_KEY}
    secrets:
      - admin_pass
      - s3_secret_key

SQLite Database Protection

The SQLite database file (/data/jagad.db) contains:

  • Encrypted S3 credentials
  • Backup metadata with storage paths
  • Schedule configurations

Protection recommendations:

  • Set file permissions to 0600 (owner read/write only)
  • Run Jagad in a Docker container with restricted filesystem access
  • Consider filesystem-level encryption for the data directory
  • Back up the SQLite database itself regularly

Least Privilege Recommendations

PostgreSQL User

sql
-- Minimal permissions for pg_dump
CREATE USER jagad WITH PASSWORD 'strong_password';
GRANT CONNECT ON DATABASE mydb TO jagad;
GRANT USAGE ON SCHEMA public TO jagad;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO jagad;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO jagad;

-- For pgBackRest (incremental), additional privileges needed:
GRANT EXECUTE ON FUNCTION pg_start_backup(text, boolean) TO jagad;
GRANT EXECUTE ON FUNCTION pg_stop_backup() TO jagad;

MySQL/MariaDB User

sql
-- Minimal permissions for mysqldump
CREATE USER 'jagad'@'%' IDENTIFIED BY 'strong_password';
GRANT SELECT, LOCK TABLES, SHOW VIEW, TRIGGER ON mydb.* TO 'jagad'@'%';
GRANT PROCESS, REPLICATION CLIENT ON *.* TO 'jagad'@'%';

-- For XtraBackup/Mariabackup (incremental), additional privileges:
-- RELOAD is needed for FLUSH TABLES WITH READ LOCK
GRANT RELOAD ON *.* TO 'jagad'@'%';

S3 IAM Policy

json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:DeleteObject"
            ],
            "Resource": [
                "arn:aws:s3:::your-backup-bucket",
                "arn:aws:s3:::your-backup-bucket/*"
            ]
        }
    ]
}

Never use root credentials for S3 access. Create a dedicated IAM user with only the permissions above.


Audit Logging

Jagad records all backup and restore operations with timestamps, status, and error details in the SQLite database. Each record includes:

FieldDescription
idUnique backup/restore identifier
connection_idWhich database server
database_idWhich database
backup_typeFull or incremental
statusRunning, success, or failed
started_atStart timestamp
completed_atCompletion timestamp
duration_msExecution duration
log_outputFull operation log
storage_pathS3 key for the backup file

This provides a complete, immutable audit trail of all backup operations.


Security Roadmap

FeatureStatusPriority
TLS for Web UI + API✅ Implemented (Nginx)High
AES-256-GCM backup encryption✅ ImplementedHigh
Credential encryption at rest✅ ImplementedHigh
Session-based authentication✅ ImplementedHigh
HttpOnly cookies✅ ImplementedMedium
Argon2id key derivation✅ ImplementedHigh
Checksum verification✅ ImplementedMedium
bcrypt/argon2 password hashing🔜 PlannedMedium
Rate limiting🔜 PlannedMedium
IP allowlisting🔜 PlannedLow
Multi-user RBAC🔜 Planned (v2.0)Low
API key authentication🔜 PlannedLow
Audit log export🔜 PlannedLow

Released under the Apache 2.0 License.