Skip to main content
  1. Documentation/
  2. Admin Guide/

Backup and Recovery

Table of Contents
TantoC2 stores all data in SQLite files on the local filesystem. Backup procedures are straightforward but require care around encryption and engagement passphrases.
Backup and recovery flow

What to Back Up
#

DataLocationBackup MethodNotes
Central databasedata/tantoc2.dbFile copyOperators, engagements, access grants
Engagement databasesdata/engagements/<uuid>/engagement.dbFile copy or API archiveAgents, tasks, credentials, audit log
Build artifactsdata/builds/File copyAgent binaries built during engagement
File transfersdata/file_transfers/File copyFiles uploaded/downloaded via agents
TLS certificates/opt/tantoc2/certs/File copyServer cert + key
Configuration~/.tantoc2/config.yamlFile copyServer config
Engagement passphrasesExternalPassword managerCritical — not stored on server

The most critical backup item is the engagement passphrase. Without it, the engagement database cannot be decrypted or re-activated. Store passphrases in a team password manager, separately from the server.


Engagement Backup Methods
#

Method 1: Encrypted Archive (Recommended)#

The archive API creates a portable, encrypted backup of a single engagement. The archive includes the engagement database and all associated metadata, encrypted with the engagement passphrase.

CLI:

1
2
tantoc2> engagements archive <engagement-id> --output /backups/engagement.archive
Engagement passphrase: ********

API:

1
2
3
4
5
6
7
8
curl -X POST \
  "http://localhost:8443/api/v1/engagements/<id>/archive" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "passphrase": "<engagement-passphrase>",
    "output_path": "/opt/tantoc2/backups/engagement-2026-03-23.archive"
  }'

The archive file is self-contained and can be imported on any TantoC2 instance. Keep the archive and the passphrase stored separately — an attacker with both can read all engagement data.

Method 2: Filesystem Copy
#

For a full server backup or a snapshot before upgrades:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Stop or drain the server first (optional but recommended for consistency)
systemctl stop tantoc2

# Copy the entire data directory
cp -r /opt/tantoc2/data /opt/tantoc2/data.backup.$(date +%Y%m%dT%H%M%SZ)

# Or use rsync for incremental backups
rsync -av --delete /opt/tantoc2/data/ backup-host:/backups/tantoc2/

# Resume the server
systemctl start tantoc2
SQLite databases should be backed up with WAL mode aware tools or while the database is idle. The DatabaseManager enables WAL mode on all connections. For consistency, either stop the server or use VACUUM INTO (SQLite online backup) for live copies.

SQLite Online Backup (Live)
#

For live backup without stopping the server, use SQLite’s online backup API:

1
2
3
4
5
6
7
# Backup central database live
sqlite3 /opt/tantoc2/data/tantoc2.db \
  "VACUUM INTO '/opt/tantoc2/backups/tantoc2-$(date +%Y%m%d).db'"

# Backup an engagement database live
sqlite3 /opt/tantoc2/data/engagements/<uuid>/engagement.db \
  "VACUUM INTO '/opt/tantoc2/backups/engagement-<uuid>-$(date +%Y%m%d).db'"

VACUUM INTO creates a consistent snapshot even with concurrent writes.


Engagement Import (Restore)
#

Restore an engagement from an archive on any TantoC2 instance:

CLI:

1
2
tantoc2> engagements import /backups/engagement.archive --name "restored-engagement"
Engagement passphrase: ********

API:

1
2
3
4
5
6
7
8
9
curl -X POST \
  "http://localhost:8443/api/v1/engagements/import" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "archive_path": "/opt/tantoc2/backups/engagement-2026-03-23.archive",
    "passphrase": "<engagement-passphrase>",
    "name": "restored-engagement"
  }'

Notes on import:

  • The engagement receives a new UUID on import — agent IDs and task IDs from the original are preserved, but the engagement identifier changes
  • If a name conflict exists, specify a new name via the name field
  • Schema migrations are applied automatically during import. A pre-migration backup copy is created before any migration runs.
  • Access grants from the original engagement are not imported — re-grant operator access after import

Key Material Backup
#

What Is Key Material?
#

MaterialLocationSensitivity
Engagement passphrasesExternal (password manager)Critical — never stored on server
PBKDF2 saltdata/tantoc2.db (engagement record)Low — included in DB backup
TLS private key/opt/tantoc2/certs/server.keyHigh — protects operator API
Agent private keysPer-engagement DB (encrypted)Protected by engagement passphrase

Passphrase Backup
#

Passphrases must be backed up outside the server:

  1. Create a record in your team password manager immediately when creating the engagement
  2. Share access with at least one other team member — if the engagement creator is unavailable, someone else must be able to re-activate it
  3. Label the record with the engagement name and UUID so it can be found quickly under pressure
  4. Never write passphrases to files on the server, in shell history, or in chat

TLS Certificate Backup
#

1
2
# Include certs in your regular backup
cp -r /opt/tantoc2/certs /opt/tantoc2/backups/certs-$(date +%Y%m%d)/

Keep the private key backup encrypted (gpg or a vault). If a TLS private key is compromised, regenerate the certificate and key and update the config.


Automated Backup
#

Cron-Based Database Backup
#

Add to crontab for the tantoc2 user:

# Daily live backup of central database
0 2 * * * sqlite3 /opt/tantoc2/data/tantoc2.db \
  "VACUUM INTO '/opt/tantoc2/backups/tantoc2-$(date +\%Y\%m\%d).db'"

# Weekly full rsync to backup host
0 3 * * 0 rsync -av --delete /opt/tantoc2/data/ \
  backup-host:/backups/tantoc2/

Backup Script
#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/bash
# /opt/tantoc2/scripts/backup.sh
set -euo pipefail

BACKUP_DIR=/opt/tantoc2/backups
DATA_DIR=/opt/tantoc2/data
TIMESTAMP=$(date +%Y%m%dT%H%M%SZ)

mkdir -p "$BACKUP_DIR/$TIMESTAMP"

# Central database
sqlite3 "$DATA_DIR/tantoc2.db" \
  "VACUUM INTO '$BACKUP_DIR/$TIMESTAMP/tantoc2.db'"

# All engagement databases
find "$DATA_DIR/engagements" -name "engagement.db" | while read -r dbfile; do
  UUID=$(basename "$(dirname "$dbfile")")
  sqlite3 "$dbfile" \
    "VACUUM INTO '$BACKUP_DIR/$TIMESTAMP/engagement-$UUID.db'"
done

# Builds and file transfers (file copy)
cp -r "$DATA_DIR/builds" "$BACKUP_DIR/$TIMESTAMP/"

# Remove backups older than 30 days
find "$BACKUP_DIR" -maxdepth 1 -type d -mtime +30 -exec rm -rf {} \;

echo "Backup complete: $BACKUP_DIR/$TIMESTAMP"

Disaster Recovery Procedures
#

Scenario 1: Server Crash (Data Intact)
#

  1. Restore the server process:
    1
    2
    3
    
    systemctl start tantoc2
    # or
    docker start tantoc2
  2. Operators log in again (in-memory tokens do not survive restarts)
  3. Re-activate engagements (passphrase required for each)
  4. Verify agents are connecting and tasks are processing

Scenario 2: Data Directory Lost
#

  1. Stop the server
  2. Restore from backup:
    1
    
    rsync -av backup-host:/backups/tantoc2/ /opt/tantoc2/data/
  3. Start the server
  4. Re-activate engagements using the passphrases from your password manager
  5. Verify all engagements activate correctly

Scenario 3: Single Engagement Database Corrupted
#

  1. Identify the engagement’s UUID and database path from the central DB:
    1
    2
    
    sqlite3 /opt/tantoc2/data/tantoc2.db \
      "SELECT id, name, db_path FROM engagements;"
  2. Stop the server
  3. Replace the corrupted database with the backup copy:
    1
    2
    
    cp /opt/tantoc2/backups/$TIMESTAMP/engagement-$UUID.db \
      /opt/tantoc2/data/engagements/$UUID/engagement.db
  4. Start the server
  5. Re-activate the engagement

Scenario 4: Restore to New Server (Migration)
#

  1. Install TantoC2 on the new server
  2. Copy the data directory from the old server:
    1
    
    rsync -av old-server:/opt/tantoc2/data/ /opt/tantoc2/data/
  3. Copy TLS certificates and configuration
  4. Start the server — operators and engagements are restored
  5. Re-activate engagements using passphrases from your password manager
  6. Update operator workstations to point to the new server address
  7. Update agent callback addresses if the listener IP/domain changed

Scenario 5: Passphrase Lost
#

If the engagement passphrase is lost and no copy exists in the password manager, the engagement data is unrecoverable. The encryption is AES-256-GCM with a key derived from the passphrase — there is no backdoor.

Mitigation: Always store passphrases in a password manager with at least two team members having access before the engagement starts.


Backup Verification
#

A backup you haven’t tested is not a backup. Schedule periodic restore tests:

  1. Spin up a test TantoC2 instance (Docker or VM)
  2. Copy the backup data to the test instance
  3. Start the server and verify the central DB loads (operators, engagements listed)
  4. Activate an engagement using the stored passphrase — if it opens, the backup is good
  5. Spot-check a few agents, tasks, and credentials to confirm data integrity
  6. Tear down the test instance

Document the test date and result. Run this at least monthly during active engagements.