How to Set Up a Dead Man’s Switch for Data
A dead man’s switch (DMS) releases information or sends alerts automatically if you fail to check in within a set period. Uses include: releasing encrypted credentials to trusted people if you’re incapacitated, notifying contacts if you go silent, or deleting sensitive data if a device isn’t checked within a timeframe.
This guide builds a Python-based DMS with configurable check-in periods, escalating alerts, and encrypted payload release.
Design Principles
A good DMS has:
- Regular check-in requirement: You must actively confirm you’re alive at intervals
- Escalating response: First sends a reminder, then alerts, then releases/acts
- Tamper resistance: Stored on a system you don’t fully control (or on multiple systems)
- Encrypted payload: The released data is encrypted until needed
- Clear recovery: Easy for intended recipients to use the released data
Architecture
[You] ← regularly send check-in token → [DMS Server/Script]
|
(if no check-in within window)
|
[1] Email reminder to you
[2] Alert to trusted contact
[3] Release encrypted payload
[4] Delete local sensitive data
Core DMS Script
#!/usr/bin/env python3
"""
Dead Man's Switch — checks if you've checked in recently.
Run via cron every hour on a server or trusted machine.
"""
import os
import json
import time
import hashlib
import smtplib
import logging
from datetime import datetime, timedelta
from email.message import EmailMessage
from pathlib import Path
# Configuration
CONFIG = {
"check_in_file": "/var/dms/last_checkin.json",
"state_file": "/var/dms/state.json",
"payload_file": "/var/dms/payload.enc", # Encrypted payload to release
"log_file": "/var/log/dms.log",
# Time windows (hours)
"reminder_after_hours": 48, # Send you a reminder after 2 days no check-in
"alert_after_hours": 96, # Alert trusted contacts after 4 days
"release_after_hours": 168, # Release payload after 7 days
# Your email (where reminder goes)
"owner_email": "you@protonmail.com",
# Trusted contacts (receive alert and payload)
"trusted_contacts": [
{"name": "Alice", "email": "alice@example.com"},
{"name": "Bob", "email": "bob@example.com"},
],
# SMTP settings (use an email relay or local MTA)
"smtp_host": "localhost",
"smtp_port": 25,
"smtp_from": "dms@yourserver.com",
"smtp_user": "", # leave empty for unauthenticated local relay
"smtp_pass": "",
}
logging.basicConfig(
filename=CONFIG["log_file"],
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
def load_last_checkin():
"""Return datetime of last check-in, or epoch if no record."""
p = Path(CONFIG["check_in_file"])
if not p.exists():
return datetime.fromtimestamp(0)
data = json.loads(p.read_text())
return datetime.fromtimestamp(data.get("timestamp", 0))
def load_state():
p = Path(CONFIG["state_file"])
if not p.exists():
return {}
return json.loads(p.read_text())
def save_state(state: dict):
Path(CONFIG["state_file"]).write_text(json.dumps(state, indent=2))
def send_email(to: str, subject: str, body: str, attachment_path: str = None):
msg = EmailMessage()
msg["From"] = CONFIG["smtp_from"]
msg["To"] = to
msg["Subject"] = subject
msg.set_content(body)
if attachment_path and Path(attachment_path).exists():
with open(attachment_path, "rb") as f:
msg.add_attachment(f.read(), maintype="application", subtype="octet-stream",
filename=Path(attachment_path).name)
try:
with smtplib.SMTP(CONFIG["smtp_host"], CONFIG["smtp_port"]) as s:
if CONFIG["smtp_user"]:
s.login(CONFIG["smtp_user"], CONFIG["smtp_pass"])
s.send_message(msg)
logging.info(f"Email sent to {to}: {subject}")
except Exception as e:
logging.error(f"Failed to send email to {to}: {e}")
def run_check():
last_checkin = load_last_checkin()
now = datetime.now()
hours_since = (now - last_checkin).total_seconds() / 3600
state = load_state()
logging.info(f"Check-in status: last={last_checkin}, hours_since={hours_since:.1f}")
# Stage 1: Send owner reminder
if hours_since >= CONFIG["reminder_after_hours"] and not state.get("reminder_sent"):
send_email(
CONFIG["owner_email"],
"DMS: Check-in required",
f"Your dead man's switch hasn't received a check-in in {hours_since:.0f} hours.\n"
f"Check in now: curl -X POST https://yourserver.com/dms/checkin?token=YOUR_TOKEN\n"
f"Next escalation in {CONFIG['alert_after_hours'] - hours_since:.0f} hours."
)
state["reminder_sent"] = True
state["reminder_time"] = now.isoformat()
save_state(state)
# Stage 2: Alert trusted contacts
if hours_since >= CONFIG["alert_after_hours"] and not state.get("alert_sent"):
for contact in CONFIG["trusted_contacts"]:
send_email(
contact["email"],
f"DMS Alert: {CONFIG['owner_email']} has not checked in",
f"This is an automated alert from {CONFIG['owner_email']}'s dead man's switch.\n\n"
f"There has been no check-in for {hours_since:.0f} hours.\n"
f"Please attempt to contact them directly.\n\n"
f"If no check-in occurs, encrypted data will be released in "
f"{CONFIG['release_after_hours'] - hours_since:.0f} more hours."
)
state["alert_sent"] = True
state["alert_time"] = now.isoformat()
save_state(state)
# Stage 3: Release encrypted payload
if hours_since >= CONFIG["release_after_hours"] and not state.get("payload_released"):
for contact in CONFIG["trusted_contacts"]:
send_email(
contact["email"],
f"DMS: Encrypted payload from {CONFIG['owner_email']}",
f"No check-in received for {hours_since:.0f} hours. Releasing encrypted payload.\n\n"
f"The attached file is encrypted. Decryption instructions were given to you separately.\n"
f"Use: gpg --decrypt payload.enc",
attachment_path=CONFIG["payload_file"]
)
state["payload_released"] = True
state["release_time"] = now.isoformat()
save_state(state)
logging.warning(f"PAYLOAD RELEASED after {hours_since:.0f}h without check-in")
if __name__ == "__main__":
run_check()
Check-In Server (Simple HTTP Endpoint)
#!/usr/bin/env python3
"""Simple Flask endpoint to accept check-in tokens."""
from flask import Flask, request, jsonify
import json, hashlib, time
from pathlib import Path
app = Flask(__name__)
# Set your check-in token (share only with yourself)
# Generate: python3 -c "import secrets; print(secrets.token_urlsafe(32))"
VALID_TOKEN = "YOUR_SECRET_CHECK_IN_TOKEN"
CHECK_IN_FILE = "/var/dms/last_checkin.json"
@app.route("/dms/checkin", methods=["POST", "GET"])
def checkin():
token = request.args.get("token") or request.form.get("token")
if not token:
return jsonify({"error": "Missing token"}), 400
# Constant-time comparison to prevent timing attacks
if not hmac.compare_digest(token, VALID_TOKEN):
return jsonify({"error": "Invalid token"}), 403
# Record check-in
data = {"timestamp": time.time(), "ip": request.remote_addr}
Path(CHECK_IN_FILE).write_text(json.dumps(data))
return jsonify({"status": "ok", "message": "Check-in recorded"}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
# Import hmac (fix the script):
# Add "import hmac" at the top of the Flask script
# Run with gunicorn behind nginx:
pip install flask gunicorn
gunicorn -b 127.0.0.1:5000 checkin_server:app
Encrypt the Payload
# Create your payload (credentials, instructions, etc.)
nano /tmp/dms-payload.txt
# Write what trusted contacts need to know
# Encrypt with their GPG public keys
gpg --import alice-public-key.asc
gpg --import bob-public-key.asc
gpg --encrypt \
--recipient alice@example.com \
--recipient bob@example.com \
--output /var/dms/payload.enc \
/tmp/dms-payload.txt
# Verify encryption
gpg --list-packets /var/dms/payload.enc
# Shred plaintext
shred -uz /tmp/dms-payload.txt
Crontab Setup
# Create DMS directory with restricted permissions
sudo mkdir -p /var/dms
sudo chown youruser:youruser /var/dms
chmod 700 /var/dms
# Add to crontab (check every 6 hours)
crontab -e
# Add:
0 */6 * * * /usr/bin/python3 /usr/local/bin/dms-check.py >> /var/log/dms.log 2>&1
Check-In from Anywhere
# Check in via curl (save this as a shell alias)
alias checkin='curl -s -X POST "https://yourserver.com/dms/checkin?token=YOUR_TOKEN"'
# Or from a phone, open:
# https://yourserver.com/dms/checkin?token=YOUR_TOKEN
# From anywhere with internet access:
curl "https://yourserver.com/dms/checkin?token=YOUR_TOKEN"
Reset After Release
If the DMS fires and you’re fine:
# Update the check-in timestamp immediately
curl "https://yourserver.com/dms/checkin?token=YOUR_TOKEN"
# Reset the state file (clear "released" flags)
echo "{}" > /var/dms/state.json
# Contact trusted contacts to confirm you're OK
Hardening the Check-In Server
The simple Flask check-in server works for personal use, but a production deployment on an internet-facing server needs additional hardening to prevent the token from being brute-forced or the server from being disrupted.
Rate limiting: Add rate limiting to the check-in endpoint to prevent brute-force token guessing. With a 32-character random token, brute force is computationally infeasible, but rate limiting reduces noise in your logs and prevents your server from being used as part of a larger scanning campaign:
from flask import Flask, request, jsonify, abort
from functools import wraps
import time
import hmac
app = Flask(__name__)
# Simple in-memory rate limiter (use Redis for multi-process deployments)
request_times = {}
def rate_limit(max_requests=5, window_seconds=60):
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
ip = request.remote_addr
now = time.time()
times = request_times.get(ip, [])
# Remove old timestamps outside the window
times = [t for t in times if now - t < window_seconds]
if len(times) >= max_requests:
abort(429)
times.append(now)
request_times[ip] = times
return f(*args, **kwargs)
return wrapper
return decorator
@app.route("/dms/checkin", methods=["POST", "GET"])
@rate_limit(max_requests=10, window_seconds=60)
def checkin():
token = request.args.get("token") or request.form.get("token")
if not token:
return jsonify({"error": "Missing token"}), 400
if not hmac.compare_digest(token.encode(), VALID_TOKEN.encode()):
time.sleep(1) # Slow down invalid attempts
return jsonify({"error": "Invalid token"}), 403
# ... rest of check-in logic
Nginx authentication layer: Put nginx in front of the Flask server with an additional IP allowlist if you always check in from known locations:
location /dms/checkin {
# Only allow check-ins from your known IP ranges
allow 203.0.113.0/24; # Home IP range
allow 198.51.100.5; # Mobile carrier NAT exit
deny all;
proxy_pass http://127.0.0.1:5000;
proxy_set_header X-Real-IP $remote_addr;
}
This provides defense-in-depth: even if your token is compromised, attackers from unknown IPs cannot use it to reset the DMS state.
Distributing DMS Across Multiple Servers
A single-server DMS has a significant failure mode: if that server goes down—due to payment lapse, infrastructure failure, or targeted attack—the DMS stops checking and either fires prematurely or fails to fire at all. For high-assurance use cases, distribute the check-in across multiple independent servers.
The simplest approach runs the same DMS script on two servers from different providers (e.g., Hetzner and Vultr). Both receive check-ins. The payload releases only when both servers agree the threshold has been exceeded:
# Distributed check-in: sends the check-in token to multiple servers simultaneously
import requests
import concurrent.futures
DMS_ENDPOINTS = [
"https://dms1.yourserver.com/dms/checkin",
"https://dms2.backupserver.net/dms/checkin",
]
TOKEN = "YOUR_SECRET_TOKEN"
def checkin_server(url):
try:
r = requests.post(url, params={"token": TOKEN}, timeout=10)
return url, r.status_code == 200
except Exception as e:
return url, False
def checkin_all():
with concurrent.futures.ThreadPoolExecutor() as executor:
results = list(executor.map(checkin_server, DMS_ENDPOINTS))
all_ok = all(ok for _, ok in results)
for url, ok in results:
status = "OK" if ok else "FAILED"
print(f"{status}: {url}")
if not all_ok:
print("WARNING: Not all DMS servers received check-in")
return all_ok
if __name__ == "__main__":
checkin_all()
Run this script as your check-in command instead of a raw curl call. The console output tells you immediately if a server failed to receive the check-in, giving you time to investigate the issue before the DMS timer expires.
Related Reading
- How to Set Up Dead Man’s Switch Email That Sends Credentials
- How to Set Up Dead Man’s Switch Using Cron Job
- LUKS Encrypted Container Guide
Built by theluckystrike — More at zovo.one