ops: add integration secret rotation and offsite backup alerting
This commit is contained in:
@@ -22,9 +22,12 @@ const paymentSchema = z.object({
|
|||||||
default_provider: z.enum(["paystack", "flutterwave", "manual"]).default("paystack"),
|
default_provider: z.enum(["paystack", "flutterwave", "manual"]).default("paystack"),
|
||||||
paystack_public: z.string().optional(),
|
paystack_public: z.string().optional(),
|
||||||
paystack_secret: z.string().optional(),
|
paystack_secret: z.string().optional(),
|
||||||
|
paystack_secret_previous: z.string().optional(),
|
||||||
flutterwave_public: z.string().optional(),
|
flutterwave_public: z.string().optional(),
|
||||||
flutterwave_secret: z.string().optional(),
|
flutterwave_secret: z.string().optional(),
|
||||||
|
flutterwave_secret_previous: z.string().optional(),
|
||||||
flutterwave_webhook_hash: z.string().optional(),
|
flutterwave_webhook_hash: z.string().optional(),
|
||||||
|
flutterwave_webhook_hash_previous: z.string().optional(),
|
||||||
callback_url: z.string().optional()
|
callback_url: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,12 @@ type PaymentSettings = {
|
|||||||
default_provider?: "paystack" | "flutterwave" | "manual";
|
default_provider?: "paystack" | "flutterwave" | "manual";
|
||||||
paystack_public?: string;
|
paystack_public?: string;
|
||||||
paystack_secret?: string;
|
paystack_secret?: string;
|
||||||
|
paystack_secret_previous?: string;
|
||||||
flutterwave_public?: string;
|
flutterwave_public?: string;
|
||||||
flutterwave_secret?: string;
|
flutterwave_secret?: string;
|
||||||
|
flutterwave_secret_previous?: string;
|
||||||
flutterwave_webhook_hash?: string;
|
flutterwave_webhook_hash?: string;
|
||||||
|
flutterwave_webhook_hash_previous?: string;
|
||||||
callback_url?: string;
|
callback_url?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,18 +138,24 @@ export async function handleManualInvoicePayment(invoiceId: string, reference: s
|
|||||||
export async function verifyPaystackSignature(signature: string | undefined, rawBody: string | undefined) {
|
export async function verifyPaystackSignature(signature: string | undefined, rawBody: string | undefined) {
|
||||||
if (!signature || !rawBody) return false;
|
if (!signature || !rawBody) return false;
|
||||||
const settings = await getPaymentSettings();
|
const settings = await getPaymentSettings();
|
||||||
if (!settings.paystack_secret) return false;
|
const secrets = [settings.paystack_secret, settings.paystack_secret_previous].filter(
|
||||||
const expected = crypto
|
(value): value is string => typeof value === "string" && value.trim().length > 0
|
||||||
.createHmac("sha512", settings.paystack_secret)
|
);
|
||||||
.update(rawBody)
|
if (secrets.length === 0) return false;
|
||||||
.digest("hex");
|
|
||||||
|
return secrets.some((secret) => {
|
||||||
|
const expected = crypto.createHmac("sha512", secret).update(rawBody).digest("hex");
|
||||||
return expected === signature;
|
return expected === signature;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyFlutterwaveSignature(signature: string | undefined) {
|
export async function verifyFlutterwaveSignature(signature: string | undefined) {
|
||||||
const settings = await getPaymentSettings();
|
const settings = await getPaymentSettings();
|
||||||
if (!settings.flutterwave_webhook_hash) return false;
|
const validHashes = [settings.flutterwave_webhook_hash, settings.flutterwave_webhook_hash_previous].filter(
|
||||||
return settings.flutterwave_webhook_hash === signature;
|
(value): value is string => typeof value === "string" && value.trim().length > 0
|
||||||
|
);
|
||||||
|
if (validHashes.length === 0 || !signature) return false;
|
||||||
|
return validHashes.includes(signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processPaystackWebhook(payload: any) {
|
export async function processPaystackWebhook(payload: any) {
|
||||||
|
|||||||
20
infra/deploy/.backup.env.example
Normal file
20
infra/deploy/.backup.env.example
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
BACKUP_ENCRYPTION_KEY=replace_with_128_hex_chars
|
||||||
|
|
||||||
|
# Offsite replication (S3-compatible: AWS S3, Backblaze B2 S3, Wasabi)
|
||||||
|
OFFSITE_BACKUP_ENABLED=false
|
||||||
|
OFFSITE_S3_BUCKET=
|
||||||
|
OFFSITE_S3_REGION=us-east-1
|
||||||
|
OFFSITE_S3_PREFIX=proxpanel/db
|
||||||
|
OFFSITE_S3_ENDPOINT_URL=
|
||||||
|
OFFSITE_S3_ACCESS_KEY_ID=
|
||||||
|
OFFSITE_S3_SECRET_ACCESS_KEY=
|
||||||
|
OFFSITE_S3_SESSION_TOKEN=
|
||||||
|
OFFSITE_S3_SSE=
|
||||||
|
OFFSITE_REPLICA_RETENTION_DAYS=30
|
||||||
|
|
||||||
|
# Alerting for backup / restore failures
|
||||||
|
BACKUP_ALERT_WEBHOOK_URL=
|
||||||
|
BACKUP_ALERT_EMAIL_WEBHOOK_URL=
|
||||||
|
BACKUP_ALERT_EMAIL_TO=
|
||||||
|
BACKUP_ALERT_SUBJECT_PREFIX=[ProxPanel_Backup]
|
||||||
|
BACKUP_ALERT_SEND_SUCCESS=false
|
||||||
82
infra/deploy/OFFSITE_BACKUP_AND_ALERTING.md
Normal file
82
infra/deploy/OFFSITE_BACKUP_AND_ALERTING.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Offsite Backup Replication + Failure Alerting
|
||||||
|
|
||||||
|
This runbook configures:
|
||||||
|
|
||||||
|
1. Encrypted local DB backups
|
||||||
|
2. Replication to S3-compatible object storage (AWS S3, Backblaze B2 S3, Wasabi)
|
||||||
|
3. Webhook/email alerting on backup or restore-test failure
|
||||||
|
|
||||||
|
## 1) Configure secrets file
|
||||||
|
|
||||||
|
Create or edit:
|
||||||
|
|
||||||
|
`/opt/proxpanel/.backup.env`
|
||||||
|
|
||||||
|
Reference template:
|
||||||
|
|
||||||
|
`/opt/proxpanel/infra/deploy/.backup.env.example`
|
||||||
|
|
||||||
|
## 2) Example provider configs
|
||||||
|
|
||||||
|
### AWS S3
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OFFSITE_BACKUP_ENABLED=true
|
||||||
|
OFFSITE_S3_BUCKET=my-proxpanel-backups
|
||||||
|
OFFSITE_S3_REGION=us-east-1
|
||||||
|
OFFSITE_S3_PREFIX=proxpanel/db
|
||||||
|
OFFSITE_S3_ACCESS_KEY_ID=AKIA...
|
||||||
|
OFFSITE_S3_SECRET_ACCESS_KEY=...
|
||||||
|
OFFSITE_S3_ENDPOINT_URL=
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wasabi
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OFFSITE_BACKUP_ENABLED=true
|
||||||
|
OFFSITE_S3_BUCKET=my-proxpanel-backups
|
||||||
|
OFFSITE_S3_REGION=us-east-1
|
||||||
|
OFFSITE_S3_PREFIX=proxpanel/db
|
||||||
|
OFFSITE_S3_ENDPOINT_URL=https://s3.us-east-1.wasabisys.com
|
||||||
|
OFFSITE_S3_ACCESS_KEY_ID=...
|
||||||
|
OFFSITE_S3_SECRET_ACCESS_KEY=...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backblaze B2 (S3 Compatible)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OFFSITE_BACKUP_ENABLED=true
|
||||||
|
OFFSITE_S3_BUCKET=my-proxpanel-backups
|
||||||
|
OFFSITE_S3_REGION=us-west-002
|
||||||
|
OFFSITE_S3_PREFIX=proxpanel/db
|
||||||
|
OFFSITE_S3_ENDPOINT_URL=https://s3.us-west-002.backblazeb2.com
|
||||||
|
OFFSITE_S3_ACCESS_KEY_ID=...
|
||||||
|
OFFSITE_S3_SECRET_ACCESS_KEY=...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3) Configure alerting
|
||||||
|
|
||||||
|
Set one or both:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BACKUP_ALERT_WEBHOOK_URL=https://hooks.example.com/proxpanel-backup
|
||||||
|
BACKUP_ALERT_EMAIL_WEBHOOK_URL=https://mailer.example.com/send
|
||||||
|
BACKUP_ALERT_EMAIL_TO=ops@votcloud.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4) Apply cron schedule
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash /opt/proxpanel/infra/deploy/configure-db-backup-cron.sh --run-now
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5) Verification
|
||||||
|
|
||||||
|
1. Check local encrypted backup exists in `/opt/proxpanel-backups/daily/<timestamp>/`.
|
||||||
|
2. Check offsite files:
|
||||||
|
- `proxpanel.sql.enc`
|
||||||
|
- `proxpanel.sql.enc.sha256`
|
||||||
|
3. Check logs:
|
||||||
|
- `/var/log/proxpanel-db-backup.log`
|
||||||
|
- `/var/log/proxpanel-db-restore-test.log`
|
||||||
|
4. Trigger controlled failure and confirm alert received (webhook/email).
|
||||||
@@ -69,7 +69,21 @@ Rollback entrypoint:
|
|||||||
6. System Management page can load branding/policy/CMS data.
|
6. System Management page can load branding/policy/CMS data.
|
||||||
7. Proxmox sync returns success (or actionable credential/SSL error message).
|
7. Proxmox sync returns success (or actionable credential/SSL error message).
|
||||||
|
|
||||||
## 6) Incident Rollback Criteria
|
## 6) Backup Hardening (Offsite + Alerts)
|
||||||
|
|
||||||
|
1. Configure `/opt/proxpanel/.backup.env`:
|
||||||
|
- `OFFSITE_BACKUP_ENABLED=true`
|
||||||
|
- `OFFSITE_S3_BUCKET`, `OFFSITE_S3_ACCESS_KEY_ID`, `OFFSITE_S3_SECRET_ACCESS_KEY`
|
||||||
|
- `OFFSITE_S3_ENDPOINT_URL` (required for Wasabi/B2 S3)
|
||||||
|
- `BACKUP_ALERT_WEBHOOK_URL` and/or `BACKUP_ALERT_EMAIL_WEBHOOK_URL`
|
||||||
|
2. Apply cron wiring:
|
||||||
|
- `sudo bash /opt/proxpanel/infra/deploy/configure-db-backup-cron.sh --run-now`
|
||||||
|
3. Validate offsite object upload:
|
||||||
|
- `aws s3 ls s3://<bucket>/<prefix>/<timestamp>/`
|
||||||
|
4. Validate restore-test success and alert pipeline:
|
||||||
|
- force a controlled failure and verify webhook/email delivery
|
||||||
|
|
||||||
|
## 7) Incident Rollback Criteria
|
||||||
|
|
||||||
Rollback immediately if any of the following persists > 10 minutes:
|
Rollback immediately if any of the following persists > 10 minutes:
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ Recommended:
|
|||||||
2. Proxmox API token secret
|
2. Proxmox API token secret
|
||||||
3. Payment/webhook secrets
|
3. Payment/webhook secrets
|
||||||
|
|
||||||
|
Enterprise hardening:
|
||||||
|
|
||||||
|
1. Keep one grace window for webhook secret rotation (`*_previous`) to avoid dropped payment events during provider cutover.
|
||||||
|
2. Validate new Proxmox token directly against `/api2/json/version` before applying it in panel settings.
|
||||||
|
|
||||||
## Runbook (Safe Order)
|
## Runbook (Safe Order)
|
||||||
|
|
||||||
1. Create timestamped app/env/db backup.
|
1. Create timestamped app/env/db backup.
|
||||||
@@ -47,6 +52,22 @@ Script guarantees:
|
|||||||
3. Post-rotation health + login verified
|
3. Post-rotation health + login verified
|
||||||
4. Summary written to `/root/proxpanel-secret-rotation-<timestamp>.txt`
|
4. Summary written to `/root/proxpanel-secret-rotation-<timestamp>.txt`
|
||||||
|
|
||||||
|
For integration secrets (Proxmox + payment/webhook + alerting endpoints), use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash /opt/proxpanel/infra/deploy/rotate-integration-secrets.sh \
|
||||||
|
--proxmox-token-secret 'new_token_secret' \
|
||||||
|
--paystack-secret 'new_paystack_secret' \
|
||||||
|
--flutterwave-webhook-hash 'new_hash'
|
||||||
|
```
|
||||||
|
|
||||||
|
After external provider cutover is confirmed, clear grace secrets:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash /opt/proxpanel/infra/deploy/rotate-integration-secrets.sh \
|
||||||
|
--finalize-payment-webhook-grace
|
||||||
|
```
|
||||||
|
|
||||||
## Rollback Plan
|
## Rollback Plan
|
||||||
|
|
||||||
If post-rotation checks fail:
|
If post-rotation checks fail:
|
||||||
|
|||||||
@@ -13,8 +13,11 @@ Usage:
|
|||||||
sudo bash infra/deploy/configure-db-backup-cron.sh [--run-now]
|
sudo bash infra/deploy/configure-db-backup-cron.sh [--run-now]
|
||||||
|
|
||||||
Default schedule (UTC):
|
Default schedule (UTC):
|
||||||
- 02:15 daily: encrypted DB backup
|
- 02:15 daily: encrypted DB backup + optional offsite replication
|
||||||
- 02:45 daily: restore test against latest encrypted backup
|
- 02:45 daily: restore test against latest encrypted backup
|
||||||
|
|
||||||
|
Alerting:
|
||||||
|
- backup/restore failures dispatch webhook/email alerts if configured in /opt/proxpanel/.backup.env
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +34,14 @@ random_key() {
|
|||||||
openssl rand -hex 64 | tr -d '\n'
|
openssl rand -hex 64 | tr -d '\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensure_secret_setting() {
|
||||||
|
local key="$1"
|
||||||
|
local value="$2"
|
||||||
|
if ! grep -q "^${key}=" "$SECRET_FILE"; then
|
||||||
|
printf '%s=%s\n' "$key" "$value" >>"$SECRET_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
local run_now="false"
|
local run_now="false"
|
||||||
if [[ "${1:-}" == "--run-now" ]]; then
|
if [[ "${1:-}" == "--run-now" ]]; then
|
||||||
@@ -50,14 +61,49 @@ main() {
|
|||||||
umask 077
|
umask 077
|
||||||
cat >"$SECRET_FILE" <<EOF
|
cat >"$SECRET_FILE" <<EOF
|
||||||
BACKUP_ENCRYPTION_KEY=$(random_key)
|
BACKUP_ENCRYPTION_KEY=$(random_key)
|
||||||
|
OFFSITE_BACKUP_ENABLED=false
|
||||||
|
OFFSITE_S3_BUCKET=
|
||||||
|
OFFSITE_S3_REGION=us-east-1
|
||||||
|
OFFSITE_S3_PREFIX=proxpanel/db
|
||||||
|
OFFSITE_S3_ENDPOINT_URL=
|
||||||
|
OFFSITE_S3_ACCESS_KEY_ID=
|
||||||
|
OFFSITE_S3_SECRET_ACCESS_KEY=
|
||||||
|
OFFSITE_S3_SESSION_TOKEN=
|
||||||
|
OFFSITE_S3_SSE=
|
||||||
|
OFFSITE_REPLICA_RETENTION_DAYS=30
|
||||||
|
BACKUP_ALERT_WEBHOOK_URL=
|
||||||
|
BACKUP_ALERT_EMAIL_WEBHOOK_URL=
|
||||||
|
BACKUP_ALERT_EMAIL_TO=
|
||||||
|
BACKUP_ALERT_SUBJECT_PREFIX=[ProxPanel_Backup]
|
||||||
|
BACKUP_ALERT_SEND_SUCCESS=false
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
chmod 600 "$SECRET_FILE"
|
chmod 600 "$SECRET_FILE"
|
||||||
|
|
||||||
|
ensure_secret_setting "OFFSITE_BACKUP_ENABLED" "false"
|
||||||
|
ensure_secret_setting "OFFSITE_S3_BUCKET" ""
|
||||||
|
ensure_secret_setting "OFFSITE_S3_REGION" "us-east-1"
|
||||||
|
ensure_secret_setting "OFFSITE_S3_PREFIX" "proxpanel/db"
|
||||||
|
ensure_secret_setting "OFFSITE_S3_ENDPOINT_URL" ""
|
||||||
|
ensure_secret_setting "OFFSITE_S3_ACCESS_KEY_ID" ""
|
||||||
|
ensure_secret_setting "OFFSITE_S3_SECRET_ACCESS_KEY" ""
|
||||||
|
ensure_secret_setting "OFFSITE_S3_SESSION_TOKEN" ""
|
||||||
|
ensure_secret_setting "OFFSITE_S3_SSE" ""
|
||||||
|
ensure_secret_setting "OFFSITE_REPLICA_RETENTION_DAYS" "30"
|
||||||
|
ensure_secret_setting "BACKUP_ALERT_WEBHOOK_URL" ""
|
||||||
|
ensure_secret_setting "BACKUP_ALERT_EMAIL_WEBHOOK_URL" ""
|
||||||
|
ensure_secret_setting "BACKUP_ALERT_EMAIL_TO" ""
|
||||||
|
ensure_secret_setting "BACKUP_ALERT_SUBJECT_PREFIX" "[ProxPanel_Backup]"
|
||||||
|
ensure_secret_setting "BACKUP_ALERT_SEND_SUCCESS" "false"
|
||||||
|
|
||||||
log "Making scripts executable"
|
log "Making scripts executable"
|
||||||
chmod +x \
|
chmod +x \
|
||||||
"$APP_DIR/infra/deploy/db-backup-encrypted.sh" \
|
"$APP_DIR/infra/deploy/db-backup-encrypted.sh" \
|
||||||
"$APP_DIR/infra/deploy/db-restore-test.sh"
|
"$APP_DIR/infra/deploy/db-restore-test.sh" \
|
||||||
|
"$APP_DIR/infra/deploy/db-backup-replicate-offsite.sh" \
|
||||||
|
"$APP_DIR/infra/deploy/notify-backup-alert.sh" \
|
||||||
|
"$APP_DIR/infra/deploy/db-backup-job.sh" \
|
||||||
|
"$APP_DIR/infra/deploy/db-restore-test-job.sh"
|
||||||
|
|
||||||
log "Installing cron schedule at $CRON_FILE"
|
log "Installing cron schedule at $CRON_FILE"
|
||||||
cat >"$CRON_FILE" <<'EOF'
|
cat >"$CRON_FILE" <<'EOF'
|
||||||
@@ -65,10 +111,10 @@ SHELL=/bin/bash
|
|||||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
|
||||||
# Encrypted PostgreSQL backup every day at 02:15 UTC
|
# Encrypted PostgreSQL backup every day at 02:15 UTC
|
||||||
15 2 * * * root APP_DIR=/opt/proxpanel /opt/proxpanel/infra/deploy/db-backup-encrypted.sh >> /var/log/proxpanel-db-backup.log 2>&1
|
15 2 * * * root APP_DIR=/opt/proxpanel /opt/proxpanel/infra/deploy/db-backup-job.sh >> /var/log/proxpanel-db-backup.log 2>&1
|
||||||
|
|
||||||
# Restore test every day at 02:45 UTC
|
# Restore test every day at 02:45 UTC
|
||||||
45 2 * * * root APP_DIR=/opt/proxpanel /opt/proxpanel/infra/deploy/db-restore-test.sh >> /var/log/proxpanel-db-restore-test.log 2>&1
|
45 2 * * * root APP_DIR=/opt/proxpanel /opt/proxpanel/infra/deploy/db-restore-test-job.sh >> /var/log/proxpanel-db-restore-test.log 2>&1
|
||||||
EOF
|
EOF
|
||||||
chmod 644 "$CRON_FILE"
|
chmod 644 "$CRON_FILE"
|
||||||
|
|
||||||
@@ -77,8 +123,8 @@ EOF
|
|||||||
|
|
||||||
if [[ "$run_now" == "true" ]]; then
|
if [[ "$run_now" == "true" ]]; then
|
||||||
log "Running immediate backup + restore test"
|
log "Running immediate backup + restore test"
|
||||||
APP_DIR="$APP_DIR" "$APP_DIR/infra/deploy/db-backup-encrypted.sh"
|
APP_DIR="$APP_DIR" "$APP_DIR/infra/deploy/db-backup-job.sh"
|
||||||
APP_DIR="$APP_DIR" "$APP_DIR/infra/deploy/db-restore-test.sh"
|
APP_DIR="$APP_DIR" "$APP_DIR/infra/deploy/db-restore-test-job.sh"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "DB backup/restore cron configured successfully."
|
log "DB backup/restore cron configured successfully."
|
||||||
|
|||||||
48
infra/deploy/db-backup-job.sh
Normal file
48
infra/deploy/db-backup-job.sh
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
APP_DIR="${APP_DIR:-/opt/proxpanel}"
|
||||||
|
SECRET_FILE="${SECRET_FILE:-$APP_DIR/.backup.env}"
|
||||||
|
|
||||||
|
BACKUP_SCRIPT="${APP_DIR}/infra/deploy/db-backup-encrypted.sh"
|
||||||
|
REPLICATE_SCRIPT="${APP_DIR}/infra/deploy/db-backup-replicate-offsite.sh"
|
||||||
|
NOTIFY_SCRIPT="${APP_DIR}/infra/deploy/notify-backup-alert.sh"
|
||||||
|
|
||||||
|
job_failed() {
|
||||||
|
local line="$1"
|
||||||
|
local message="Daily backup job failed (line ${line}) on host $(hostname -f 2>/dev/null || hostname)"
|
||||||
|
APP_DIR="$APP_DIR" "$NOTIFY_SCRIPT" \
|
||||||
|
--event backup_failed \
|
||||||
|
--severity critical \
|
||||||
|
--status failed \
|
||||||
|
--source db-backup-job \
|
||||||
|
--message "$message" \
|
||||||
|
--context-json "{\"line\":${line}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
trap 'job_failed $LINENO' ERR
|
||||||
|
|
||||||
|
APP_DIR="$APP_DIR" "$BACKUP_SCRIPT"
|
||||||
|
APP_DIR="$APP_DIR" "$REPLICATE_SCRIPT"
|
||||||
|
|
||||||
|
local send_success="false"
|
||||||
|
if [[ -f "$SECRET_FILE" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$SECRET_FILE"
|
||||||
|
send_success="${BACKUP_ALERT_SEND_SUCCESS:-false}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$send_success" == "true" ]]; then
|
||||||
|
APP_DIR="$APP_DIR" "$NOTIFY_SCRIPT" \
|
||||||
|
--event backup_success \
|
||||||
|
--severity info \
|
||||||
|
--status ok \
|
||||||
|
--source db-backup-job \
|
||||||
|
--message "Daily backup + offsite replication completed successfully"
|
||||||
|
fi
|
||||||
|
|
||||||
|
trap - ERR
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
163
infra/deploy/db-backup-replicate-offsite.sh
Normal file
163
infra/deploy/db-backup-replicate-offsite.sh
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
APP_DIR="${APP_DIR:-/opt/proxpanel}"
|
||||||
|
SECRET_FILE="${SECRET_FILE:-$APP_DIR/.backup.env}"
|
||||||
|
BACKUP_ROOT="${BACKUP_ROOT:-/opt/proxpanel-backups/daily}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
APP_DIR=/opt/proxpanel /opt/proxpanel/infra/deploy/db-backup-replicate-offsite.sh [--backup-dir <dir>]
|
||||||
|
|
||||||
|
Reads replication settings from /opt/proxpanel/.backup.env.
|
||||||
|
Required keys for S3-compatible replication:
|
||||||
|
OFFSITE_BACKUP_ENABLED=true
|
||||||
|
OFFSITE_S3_BUCKET=<bucket-name>
|
||||||
|
OFFSITE_S3_ACCESS_KEY_ID=<access-key>
|
||||||
|
OFFSITE_S3_SECRET_ACCESS_KEY=<secret-key>
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
OFFSITE_S3_REGION=us-east-1
|
||||||
|
OFFSITE_S3_PREFIX=proxpanel/db
|
||||||
|
OFFSITE_S3_ENDPOINT_URL=https://s3.us-west-1.wasabisys.com
|
||||||
|
OFFSITE_S3_SSE=AES256
|
||||||
|
OFFSITE_REPLICA_RETENTION_DAYS=30
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[%s] %s\n' "$(date -u +'%Y-%m-%d %H:%M:%S UTC')" "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die() {
|
||||||
|
printf '[ERROR] %s\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_file() {
|
||||||
|
[[ -f "$1" ]] || die "Missing required file: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_command() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
find_latest_backup_dir() {
|
||||||
|
find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d | sort | tail -n 1
|
||||||
|
}
|
||||||
|
|
||||||
|
prune_remote_retention() {
|
||||||
|
local bucket="$1"
|
||||||
|
local prefix="$2"
|
||||||
|
local endpoint_flag=()
|
||||||
|
if [[ -n "${OFFSITE_S3_ENDPOINT_URL:-}" ]]; then
|
||||||
|
endpoint_flag=(--endpoint-url "$OFFSITE_S3_ENDPOINT_URL")
|
||||||
|
fi
|
||||||
|
|
||||||
|
local days="${OFFSITE_REPLICA_RETENTION_DAYS:-}"
|
||||||
|
if [[ -z "$days" || ! "$days" =~ ^[0-9]+$ || "$days" -le 0 ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local cutoff
|
||||||
|
cutoff="$(date -u -d "-${days} days" +%Y%m%d-%H%M%S)"
|
||||||
|
log "Applying offsite retention policy (${days} days; cutoff=${cutoff})"
|
||||||
|
|
||||||
|
local listing prefixes old_prefix
|
||||||
|
listing="$(aws "${endpoint_flag[@]}" s3 ls "s3://${bucket}/${prefix}/" || true)"
|
||||||
|
prefixes="$(printf '%s\n' "$listing" | awk '/PRE [0-9]{8}-[0-9]{6}\// {print $2}' | tr -d '/')"
|
||||||
|
|
||||||
|
while IFS= read -r old_prefix; do
|
||||||
|
[[ -n "$old_prefix" ]] || continue
|
||||||
|
if [[ "$old_prefix" < "$cutoff" ]]; then
|
||||||
|
log "Pruning remote backup prefix ${old_prefix}"
|
||||||
|
aws "${endpoint_flag[@]}" s3 rm "s3://${bucket}/${prefix}/${old_prefix}/" --recursive >/dev/null
|
||||||
|
fi
|
||||||
|
done <<<"$prefixes"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
local forced_backup_dir=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--backup-dir)
|
||||||
|
forced_backup_dir="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "Unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
require_file "$SECRET_FILE"
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$SECRET_FILE"
|
||||||
|
|
||||||
|
if [[ "${OFFSITE_BACKUP_ENABLED:-false}" != "true" ]]; then
|
||||||
|
log "Offsite replication disabled (OFFSITE_BACKUP_ENABLED != true)."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_command aws
|
||||||
|
|
||||||
|
[[ -n "${OFFSITE_S3_BUCKET:-}" ]] || die "OFFSITE_S3_BUCKET is required."
|
||||||
|
[[ -n "${OFFSITE_S3_ACCESS_KEY_ID:-}" ]] || die "OFFSITE_S3_ACCESS_KEY_ID is required."
|
||||||
|
[[ -n "${OFFSITE_S3_SECRET_ACCESS_KEY:-}" ]] || die "OFFSITE_S3_SECRET_ACCESS_KEY is required."
|
||||||
|
|
||||||
|
export AWS_ACCESS_KEY_ID="$OFFSITE_S3_ACCESS_KEY_ID"
|
||||||
|
export AWS_SECRET_ACCESS_KEY="$OFFSITE_S3_SECRET_ACCESS_KEY"
|
||||||
|
if [[ -n "${OFFSITE_S3_SESSION_TOKEN:-}" ]]; then
|
||||||
|
export AWS_SESSION_TOKEN="$OFFSITE_S3_SESSION_TOKEN"
|
||||||
|
fi
|
||||||
|
export AWS_DEFAULT_REGION="${OFFSITE_S3_REGION:-us-east-1}"
|
||||||
|
|
||||||
|
local backup_dir
|
||||||
|
if [[ -n "$forced_backup_dir" ]]; then
|
||||||
|
backup_dir="$forced_backup_dir"
|
||||||
|
else
|
||||||
|
backup_dir="$(find_latest_backup_dir)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -n "$backup_dir" && -d "$backup_dir" ]] || die "Unable to locate backup directory."
|
||||||
|
|
||||||
|
local encrypted_file checksum_file stamp prefix endpoint_flag sse_flag destination
|
||||||
|
encrypted_file="${backup_dir}/proxpanel.sql.enc"
|
||||||
|
checksum_file="${encrypted_file}.sha256"
|
||||||
|
require_file "$encrypted_file"
|
||||||
|
require_file "$checksum_file"
|
||||||
|
|
||||||
|
stamp="$(basename "$backup_dir")"
|
||||||
|
prefix="${OFFSITE_S3_PREFIX:-proxpanel/db}"
|
||||||
|
destination="s3://${OFFSITE_S3_BUCKET}/${prefix}/${stamp}/"
|
||||||
|
|
||||||
|
endpoint_flag=()
|
||||||
|
if [[ -n "${OFFSITE_S3_ENDPOINT_URL:-}" ]]; then
|
||||||
|
endpoint_flag=(--endpoint-url "$OFFSITE_S3_ENDPOINT_URL")
|
||||||
|
fi
|
||||||
|
|
||||||
|
sse_flag=()
|
||||||
|
if [[ -n "${OFFSITE_S3_SSE:-}" ]]; then
|
||||||
|
sse_flag=(--sse "$OFFSITE_S3_SSE")
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Replicating encrypted backup to ${destination}"
|
||||||
|
aws "${endpoint_flag[@]}" s3 cp "$encrypted_file" "${destination}proxpanel.sql.enc" "${sse_flag[@]}" --only-show-errors
|
||||||
|
aws "${endpoint_flag[@]}" s3 cp "$checksum_file" "${destination}proxpanel.sql.enc.sha256" "${sse_flag[@]}" --only-show-errors
|
||||||
|
|
||||||
|
log "Verifying offsite object presence"
|
||||||
|
aws "${endpoint_flag[@]}" s3 ls "${destination}proxpanel.sql.enc" >/dev/null
|
||||||
|
aws "${endpoint_flag[@]}" s3 ls "${destination}proxpanel.sql.enc.sha256" >/dev/null
|
||||||
|
|
||||||
|
prune_remote_retention "$OFFSITE_S3_BUCKET" "$prefix"
|
||||||
|
log "Offsite replication completed successfully."
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
46
infra/deploy/db-restore-test-job.sh
Normal file
46
infra/deploy/db-restore-test-job.sh
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
APP_DIR="${APP_DIR:-/opt/proxpanel}"
|
||||||
|
SECRET_FILE="${SECRET_FILE:-$APP_DIR/.backup.env}"
|
||||||
|
|
||||||
|
RESTORE_SCRIPT="${APP_DIR}/infra/deploy/db-restore-test.sh"
|
||||||
|
NOTIFY_SCRIPT="${APP_DIR}/infra/deploy/notify-backup-alert.sh"
|
||||||
|
|
||||||
|
job_failed() {
|
||||||
|
local line="$1"
|
||||||
|
local message="Daily restore test failed (line ${line}) on host $(hostname -f 2>/dev/null || hostname)"
|
||||||
|
APP_DIR="$APP_DIR" "$NOTIFY_SCRIPT" \
|
||||||
|
--event restore_test_failed \
|
||||||
|
--severity critical \
|
||||||
|
--status failed \
|
||||||
|
--source db-restore-test-job \
|
||||||
|
--message "$message" \
|
||||||
|
--context-json "{\"line\":${line}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
trap 'job_failed $LINENO' ERR
|
||||||
|
|
||||||
|
APP_DIR="$APP_DIR" "$RESTORE_SCRIPT"
|
||||||
|
|
||||||
|
local send_success="false"
|
||||||
|
if [[ -f "$SECRET_FILE" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$SECRET_FILE"
|
||||||
|
send_success="${BACKUP_ALERT_SEND_SUCCESS:-false}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$send_success" == "true" ]]; then
|
||||||
|
APP_DIR="$APP_DIR" "$NOTIFY_SCRIPT" \
|
||||||
|
--event restore_test_success \
|
||||||
|
--severity info \
|
||||||
|
--status ok \
|
||||||
|
--source db-restore-test-job \
|
||||||
|
--message "Daily restore test completed successfully"
|
||||||
|
fi
|
||||||
|
|
||||||
|
trap - ERR
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -121,7 +121,7 @@ install_prereqs() {
|
|||||||
log "Installing OS prerequisites..."
|
log "Installing OS prerequisites..."
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
apt-get update -y
|
apt-get update -y
|
||||||
apt-get install -y ca-certificates curl git openssl jq rsync
|
apt-get install -y ca-certificates curl git openssl jq rsync awscli
|
||||||
}
|
}
|
||||||
|
|
||||||
install_docker_if_needed() {
|
install_docker_if_needed() {
|
||||||
|
|||||||
168
infra/deploy/notify-backup-alert.sh
Normal file
168
infra/deploy/notify-backup-alert.sh
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
APP_DIR="${APP_DIR:-/opt/proxpanel}"
|
||||||
|
SECRET_FILE="${SECRET_FILE:-$APP_DIR/.backup.env}"
|
||||||
|
|
||||||
|
EVENT="${EVENT:-}"
|
||||||
|
SEVERITY="${SEVERITY:-warning}"
|
||||||
|
STATUS="${STATUS:-failed}"
|
||||||
|
MESSAGE="${MESSAGE:-}"
|
||||||
|
SOURCE="${SOURCE:-backup-jobs}"
|
||||||
|
CONTEXT_JSON="${CONTEXT_JSON:-{}}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
APP_DIR=/opt/proxpanel /opt/proxpanel/infra/deploy/notify-backup-alert.sh \
|
||||||
|
--event <backup_failed|restore_test_failed|...> \
|
||||||
|
--severity <info|warning|critical> \
|
||||||
|
--status <ok|failed> \
|
||||||
|
--message "<human-readable-message>" \
|
||||||
|
[--source backup-cron] \
|
||||||
|
[--context-json '{"key":"value"}']
|
||||||
|
|
||||||
|
Alert destinations are read from /opt/proxpanel/.backup.env:
|
||||||
|
BACKUP_ALERT_WEBHOOK_URL=
|
||||||
|
BACKUP_ALERT_EMAIL_WEBHOOK_URL=
|
||||||
|
BACKUP_ALERT_EMAIL_TO=
|
||||||
|
BACKUP_ALERT_SUBJECT_PREFIX=[ProxPanel Backup]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[%s] %s\n' "$(date -u +'%Y-%m-%d %H:%M:%S UTC')" "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_command() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || {
|
||||||
|
printf '[WARN] Missing command: %s\n' "$1" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--event)
|
||||||
|
EVENT="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--severity)
|
||||||
|
SEVERITY="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--status)
|
||||||
|
STATUS="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--message)
|
||||||
|
MESSAGE="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--source)
|
||||||
|
SOURCE="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--context-json)
|
||||||
|
CONTEXT_JSON="${2:-{}}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf '[WARN] Ignoring unknown argument: %s\n' "$1" >&2
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
parse_args "$@"
|
||||||
|
require_command curl || exit 0
|
||||||
|
require_command jq || exit 0
|
||||||
|
|
||||||
|
if [[ -f "$SECRET_FILE" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$SECRET_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -n "$EVENT" ]] || EVENT="backup_job_event"
|
||||||
|
[[ -n "$MESSAGE" ]] || MESSAGE="Backup alert event raised"
|
||||||
|
|
||||||
|
local context
|
||||||
|
context="$(printf '%s' "$CONTEXT_JSON" | jq -c '.' 2>/dev/null || printf '{}')"
|
||||||
|
|
||||||
|
local payload now
|
||||||
|
now="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
||||||
|
payload="$(
|
||||||
|
jq -n \
|
||||||
|
--arg type "backup.alert" \
|
||||||
|
--arg event "$EVENT" \
|
||||||
|
--arg severity "$SEVERITY" \
|
||||||
|
--arg status "$STATUS" \
|
||||||
|
--arg message "$MESSAGE" \
|
||||||
|
--arg source "$SOURCE" \
|
||||||
|
--arg timestamp "$now" \
|
||||||
|
--argjson context "$context" \
|
||||||
|
'{
|
||||||
|
type: $type,
|
||||||
|
event: $event,
|
||||||
|
severity: $severity,
|
||||||
|
status: $status,
|
||||||
|
source: $source,
|
||||||
|
message: $message,
|
||||||
|
timestamp: $timestamp,
|
||||||
|
context: $context
|
||||||
|
}'
|
||||||
|
)"
|
||||||
|
|
||||||
|
local webhook_url email_webhook email_to subject_prefix subject status_webhook status_email
|
||||||
|
webhook_url="${BACKUP_ALERT_WEBHOOK_URL:-}"
|
||||||
|
email_webhook="${BACKUP_ALERT_EMAIL_WEBHOOK_URL:-}"
|
||||||
|
email_to="${BACKUP_ALERT_EMAIL_TO:-${OPS_EMAIL:-}}"
|
||||||
|
subject_prefix="${BACKUP_ALERT_SUBJECT_PREFIX:-[ProxPanel Backup]}"
|
||||||
|
subject="${subject_prefix} ${EVENT} (${SEVERITY})"
|
||||||
|
status_webhook="skipped"
|
||||||
|
status_email="skipped"
|
||||||
|
|
||||||
|
if [[ -n "$webhook_url" ]]; then
|
||||||
|
if curl -fsS -X POST "$webhook_url" -H "Content-Type: application/json" -d "$payload" >/dev/null; then
|
||||||
|
status_webhook="sent"
|
||||||
|
else
|
||||||
|
status_webhook="failed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$email_webhook" && -n "$email_to" ]]; then
|
||||||
|
local email_payload
|
||||||
|
email_payload="$(
|
||||||
|
jq -n \
|
||||||
|
--arg type "backup.alert.email" \
|
||||||
|
--arg to "$email_to" \
|
||||||
|
--arg subject "$subject" \
|
||||||
|
--arg message "$MESSAGE" \
|
||||||
|
--argjson payload "$payload" \
|
||||||
|
'{
|
||||||
|
type: $type,
|
||||||
|
to: $to,
|
||||||
|
subject: $subject,
|
||||||
|
message: $message,
|
||||||
|
payload: $payload
|
||||||
|
}'
|
||||||
|
)"
|
||||||
|
|
||||||
|
if curl -fsS -X POST "$email_webhook" -H "Content-Type: application/json" -d "$email_payload" >/dev/null; then
|
||||||
|
status_email="sent"
|
||||||
|
else
|
||||||
|
status_email="failed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Alert dispatch result: webhook=${status_webhook}, email=${status_email}"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
469
infra/deploy/rotate-integration-secrets.sh
Normal file
469
infra/deploy/rotate-integration-secrets.sh
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
APP_DIR="${APP_DIR:-/opt/proxpanel}"
|
||||||
|
ENV_FILE="${ENV_FILE:-$APP_DIR/.env.production}"
|
||||||
|
BACKUP_ROOT="${BACKUP_ROOT:-/opt/proxpanel-backups}"
|
||||||
|
BACKEND_URL="${BACKEND_URL:-}"
|
||||||
|
|
||||||
|
ADMIN_EMAIL_INPUT=""
|
||||||
|
ADMIN_PASSWORD_INPUT=""
|
||||||
|
|
||||||
|
NEW_PROXMOX_HOST=""
|
||||||
|
NEW_PROXMOX_PORT=""
|
||||||
|
NEW_PROXMOX_USERNAME=""
|
||||||
|
NEW_PROXMOX_TOKEN_ID=""
|
||||||
|
NEW_PROXMOX_TOKEN_SECRET=""
|
||||||
|
NEW_PROXMOX_VERIFY_SSL=""
|
||||||
|
|
||||||
|
NEW_PAYSTACK_SECRET=""
|
||||||
|
NEW_FLUTTERWAVE_SECRET=""
|
||||||
|
NEW_FLUTTERWAVE_WEBHOOK_HASH=""
|
||||||
|
FINALIZE_PAYMENT_GRACE="false"
|
||||||
|
|
||||||
|
NEW_MONITORING_WEBHOOK_URL=""
|
||||||
|
NEW_ALERT_WEBHOOK_URL=""
|
||||||
|
NEW_NOTIFICATION_EMAIL_WEBHOOK=""
|
||||||
|
NEW_OPS_EMAIL=""
|
||||||
|
|
||||||
|
API_TOKEN=""
|
||||||
|
TS=""
|
||||||
|
BACKUP_DIR=""
|
||||||
|
|
||||||
|
UPDATED_PROXMOX="false"
|
||||||
|
UPDATED_PAYMENT="false"
|
||||||
|
UPDATED_NOTIFICATIONS="false"
|
||||||
|
|
||||||
|
ORIGINAL_PROXMOX_JSON=""
|
||||||
|
ORIGINAL_PAYMENT_JSON=""
|
||||||
|
ORIGINAL_NOTIFICATIONS_JSON=""
|
||||||
|
ORIGINAL_NOTIFICATIONS_JSON_SANITIZED=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
sudo bash infra/deploy/rotate-integration-secrets.sh [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--backend-url <url> Backend URL (default: http://127.0.0.1:${BACKEND_PORT}/api)
|
||||||
|
--admin-email <email> Admin email (default from .env.production)
|
||||||
|
--admin-password <password> Admin password (default from .env.production)
|
||||||
|
|
||||||
|
# Proxmox token rotation (zero-downtime by preflight + post-sync validation):
|
||||||
|
--proxmox-token-secret <secret> New Proxmox token secret
|
||||||
|
--proxmox-token-id <token-id> Optional new token id
|
||||||
|
--proxmox-username <user@realm> Optional new token username
|
||||||
|
--proxmox-host <host> Optional host override
|
||||||
|
--proxmox-port <port> Optional port override
|
||||||
|
--proxmox-verify-ssl <true|false> Optional SSL verify override
|
||||||
|
|
||||||
|
# Payment/webhook secret rotation (supports grace window):
|
||||||
|
--paystack-secret <secret> New Paystack secret (stores old value as paystack_secret_previous)
|
||||||
|
--flutterwave-secret <secret> New Flutterwave secret (stores old value as flutterwave_secret_previous)
|
||||||
|
--flutterwave-webhook-hash <hash> New Flutterwave webhook hash (stores old value as flutterwave_webhook_hash_previous)
|
||||||
|
--finalize-payment-webhook-grace Clears *_previous fields after provider cutover is complete
|
||||||
|
|
||||||
|
# Alerting webhook endpoints (optional):
|
||||||
|
--monitoring-webhook-url <url>
|
||||||
|
--alert-webhook-url <url>
|
||||||
|
--notification-email-webhook <url>
|
||||||
|
--ops-email <email>
|
||||||
|
|
||||||
|
-h, --help Show help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
sudo bash infra/deploy/rotate-integration-secrets.sh \
|
||||||
|
--proxmox-token-secret 'NEW_SECRET' \
|
||||||
|
--paystack-secret 'sk_live_new' \
|
||||||
|
--flutterwave-webhook-hash 'new_hash'
|
||||||
|
|
||||||
|
sudo bash infra/deploy/rotate-integration-secrets.sh \
|
||||||
|
--finalize-payment-webhook-grace
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '\n[%s] %s\n' "$(date -u +'%Y-%m-%d %H:%M:%S UTC')" "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die() {
|
||||||
|
printf '\n[ERROR] %s\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_root() {
|
||||||
|
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
||||||
|
die "Run as root (or with sudo)."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require_command() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_file() {
|
||||||
|
[[ -f "$1" ]] || die "Missing required file: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--backend-url)
|
||||||
|
BACKEND_URL="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--admin-email)
|
||||||
|
ADMIN_EMAIL_INPUT="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--admin-password)
|
||||||
|
ADMIN_PASSWORD_INPUT="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--proxmox-host)
|
||||||
|
NEW_PROXMOX_HOST="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--proxmox-port)
|
||||||
|
NEW_PROXMOX_PORT="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--proxmox-username)
|
||||||
|
NEW_PROXMOX_USERNAME="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--proxmox-token-id)
|
||||||
|
NEW_PROXMOX_TOKEN_ID="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--proxmox-token-secret)
|
||||||
|
NEW_PROXMOX_TOKEN_SECRET="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--proxmox-verify-ssl)
|
||||||
|
NEW_PROXMOX_VERIFY_SSL="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--paystack-secret)
|
||||||
|
NEW_PAYSTACK_SECRET="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--flutterwave-secret)
|
||||||
|
NEW_FLUTTERWAVE_SECRET="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--flutterwave-webhook-hash)
|
||||||
|
NEW_FLUTTERWAVE_WEBHOOK_HASH="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--finalize-payment-webhook-grace)
|
||||||
|
FINALIZE_PAYMENT_GRACE="true"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--monitoring-webhook-url)
|
||||||
|
NEW_MONITORING_WEBHOOK_URL="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--alert-webhook-url)
|
||||||
|
NEW_ALERT_WEBHOOK_URL="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--notification-email-webhook)
|
||||||
|
NEW_NOTIFICATION_EMAIL_WEBHOOK="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--ops-email)
|
||||||
|
NEW_OPS_EMAIL="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "Unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
json_escape() {
|
||||||
|
printf '%s' "$1" | jq -Rs .
|
||||||
|
}
|
||||||
|
|
||||||
|
json_canonical() {
|
||||||
|
printf '%s' "$1" | jq -cS .
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitize_notifications_payload() {
|
||||||
|
printf '%s' "$1" | jq '
|
||||||
|
if (.monitoring_webhook_url // "") == "" then del(.monitoring_webhook_url) else . end
|
||||||
|
| if (.alert_webhook_url // "") == "" then del(.alert_webhook_url) else . end
|
||||||
|
| if (.email_gateway_url // "") == "" then del(.email_gateway_url) else . end
|
||||||
|
| if (.notification_email_webhook // "") == "" then del(.notification_email_webhook) else . end
|
||||||
|
| if (.ops_email // "") == "" then del(.ops_email) else . end
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
api_get() {
|
||||||
|
local path="$1"
|
||||||
|
curl -fsS \
|
||||||
|
-H "Authorization: Bearer ${API_TOKEN}" \
|
||||||
|
"${BACKEND_URL}${path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
api_put() {
|
||||||
|
local path="$1"
|
||||||
|
local payload="$2"
|
||||||
|
curl -fsS \
|
||||||
|
-X PUT \
|
||||||
|
-H "Authorization: Bearer ${API_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
"${BACKEND_URL}${path}" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
preflight_proxmox_candidate() {
|
||||||
|
local payload_json="$1"
|
||||||
|
local host port username token_id token_secret verify_ssl auth_header
|
||||||
|
|
||||||
|
host="$(printf '%s' "$payload_json" | jq -r '.host // empty')"
|
||||||
|
port="$(printf '%s' "$payload_json" | jq -r '.port // 8006')"
|
||||||
|
username="$(printf '%s' "$payload_json" | jq -r '.username // empty')"
|
||||||
|
token_id="$(printf '%s' "$payload_json" | jq -r '.token_id // empty')"
|
||||||
|
token_secret="$(printf '%s' "$payload_json" | jq -r '.token_secret // empty')"
|
||||||
|
verify_ssl="$(printf '%s' "$payload_json" | jq -r '.verify_ssl // true')"
|
||||||
|
|
||||||
|
[[ -n "$host" && -n "$username" && -n "$token_id" && -n "$token_secret" ]] || {
|
||||||
|
die "Candidate Proxmox settings are incomplete."
|
||||||
|
}
|
||||||
|
|
||||||
|
auth_header="PVEAPIToken=${username}!${token_id}=${token_secret}"
|
||||||
|
log "Preflight: validating candidate Proxmox token directly against ${host}:${port}"
|
||||||
|
if [[ "$verify_ssl" == "false" ]]; then
|
||||||
|
curl -ksfS -H "Authorization: ${auth_header}" "https://${host}:${port}/api2/json/version" >/dev/null
|
||||||
|
else
|
||||||
|
curl -fsS -H "Authorization: ${auth_header}" "https://${host}:${port}/api2/json/version" >/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
rollback_if_needed() {
|
||||||
|
if [[ -z "$API_TOKEN" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$UPDATED_NOTIFICATIONS" == "true" && -n "$ORIGINAL_NOTIFICATIONS_JSON" ]]; then
|
||||||
|
log "Rollback: restoring notifications settings"
|
||||||
|
api_put "/settings/notifications" "$ORIGINAL_NOTIFICATIONS_JSON_SANITIZED" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$UPDATED_PAYMENT" == "true" && -n "$ORIGINAL_PAYMENT_JSON" ]]; then
|
||||||
|
log "Rollback: restoring payment settings"
|
||||||
|
api_put "/settings/payment" "$ORIGINAL_PAYMENT_JSON" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$UPDATED_PROXMOX" == "true" && -n "$ORIGINAL_PROXMOX_JSON" ]]; then
|
||||||
|
log "Rollback: restoring proxmox settings"
|
||||||
|
api_put "/settings/proxmox" "$ORIGINAL_PROXMOX_JSON" || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
on_error() {
|
||||||
|
local line="$1"
|
||||||
|
printf '\n[ERROR] Rotation failed at line %s. Attempting rollback where possible.\n' "$line" >&2
|
||||||
|
rollback_if_needed
|
||||||
|
}
|
||||||
|
|
||||||
|
build_backend_url() {
|
||||||
|
if [[ -n "$BACKEND_URL" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local backend_port
|
||||||
|
backend_port="${BACKEND_PORT:-8080}"
|
||||||
|
BACKEND_URL="http://127.0.0.1:${backend_port}/api"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
parse_args "$@"
|
||||||
|
require_root
|
||||||
|
require_command curl
|
||||||
|
require_command jq
|
||||||
|
require_file "$ENV_FILE"
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$ENV_FILE"
|
||||||
|
build_backend_url
|
||||||
|
|
||||||
|
local admin_email admin_password
|
||||||
|
admin_email="${ADMIN_EMAIL_INPUT:-${ADMIN_EMAIL:-}}"
|
||||||
|
admin_password="${ADMIN_PASSWORD_INPUT:-${ADMIN_PASSWORD:-}}"
|
||||||
|
|
||||||
|
[[ -n "$admin_email" && -n "$admin_password" ]] || {
|
||||||
|
die "Admin credentials are required (provide --admin-email/--admin-password or set ADMIN_EMAIL/ADMIN_PASSWORD in env)."
|
||||||
|
}
|
||||||
|
|
||||||
|
local wants_rotation="false"
|
||||||
|
if [[ -n "$NEW_PROXMOX_TOKEN_SECRET" || -n "$NEW_PROXMOX_TOKEN_ID" || -n "$NEW_PROXMOX_USERNAME" || -n "$NEW_PROXMOX_HOST" || -n "$NEW_PROXMOX_PORT" || -n "$NEW_PROXMOX_VERIFY_SSL" || -n "$NEW_PAYSTACK_SECRET" || -n "$NEW_FLUTTERWAVE_SECRET" || -n "$NEW_FLUTTERWAVE_WEBHOOK_HASH" || -n "$NEW_MONITORING_WEBHOOK_URL" || -n "$NEW_ALERT_WEBHOOK_URL" || -n "$NEW_NOTIFICATION_EMAIL_WEBHOOK" || -n "$NEW_OPS_EMAIL" || "$FINALIZE_PAYMENT_GRACE" == "true" ]]; then
|
||||||
|
wants_rotation="true"
|
||||||
|
fi
|
||||||
|
[[ "$wants_rotation" == "true" ]] || die "No rotation inputs supplied. Run with --help for options."
|
||||||
|
|
||||||
|
trap 'on_error $LINENO' ERR
|
||||||
|
|
||||||
|
TS="$(date -u +%Y%m%d-%H%M%S)"
|
||||||
|
BACKUP_DIR="${BACKUP_ROOT}/${TS}-integration-secret-rotation"
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
chmod 700 "$BACKUP_DIR"
|
||||||
|
|
||||||
|
log "Authenticating against backend API"
|
||||||
|
local login_response
|
||||||
|
login_response="$(
|
||||||
|
curl -fsS -X POST "${BACKEND_URL}/auth/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"email\":$(json_escape "$admin_email"),\"password\":$(json_escape "$admin_password")}"
|
||||||
|
)"
|
||||||
|
API_TOKEN="$(printf '%s' "$login_response" | jq -r '.token // empty')"
|
||||||
|
[[ -n "$API_TOKEN" ]] || die "Failed to obtain auth token from backend login."
|
||||||
|
|
||||||
|
log "Fetching current integration settings"
|
||||||
|
ORIGINAL_PROXMOX_JSON="$(api_get "/settings/proxmox")"
|
||||||
|
ORIGINAL_PAYMENT_JSON="$(api_get "/settings/payment")"
|
||||||
|
ORIGINAL_NOTIFICATIONS_JSON="$(api_get "/settings/notifications")"
|
||||||
|
ORIGINAL_NOTIFICATIONS_JSON_SANITIZED="$(sanitize_notifications_payload "$ORIGINAL_NOTIFICATIONS_JSON")"
|
||||||
|
|
||||||
|
printf '%s\n' "$ORIGINAL_PROXMOX_JSON" >"${BACKUP_DIR}/proxmox.before.json"
|
||||||
|
printf '%s\n' "$ORIGINAL_PAYMENT_JSON" >"${BACKUP_DIR}/payment.before.json"
|
||||||
|
printf '%s\n' "$ORIGINAL_NOTIFICATIONS_JSON" >"${BACKUP_DIR}/notifications.before.json"
|
||||||
|
cp "$ENV_FILE" "${BACKUP_DIR}/.env.production.bak"
|
||||||
|
|
||||||
|
local proxmox_payload payment_payload notifications_payload
|
||||||
|
proxmox_payload="$ORIGINAL_PROXMOX_JSON"
|
||||||
|
payment_payload="$ORIGINAL_PAYMENT_JSON"
|
||||||
|
notifications_payload="$ORIGINAL_NOTIFICATIONS_JSON"
|
||||||
|
|
||||||
|
if [[ -n "$NEW_PROXMOX_HOST" ]]; then
|
||||||
|
proxmox_payload="$(printf '%s' "$proxmox_payload" | jq --arg v "$NEW_PROXMOX_HOST" '.host = $v')"
|
||||||
|
fi
|
||||||
|
if [[ -n "$NEW_PROXMOX_PORT" ]]; then
|
||||||
|
proxmox_payload="$(printf '%s' "$proxmox_payload" | jq --argjson v "$NEW_PROXMOX_PORT" '.port = $v')"
|
||||||
|
fi
|
||||||
|
if [[ -n "$NEW_PROXMOX_USERNAME" ]]; then
|
||||||
|
proxmox_payload="$(printf '%s' "$proxmox_payload" | jq --arg v "$NEW_PROXMOX_USERNAME" '.username = $v')"
|
||||||
|
fi
|
||||||
|
if [[ -n "$NEW_PROXMOX_TOKEN_ID" ]]; then
|
||||||
|
proxmox_payload="$(printf '%s' "$proxmox_payload" | jq --arg v "$NEW_PROXMOX_TOKEN_ID" '.token_id = $v')"
|
||||||
|
fi
|
||||||
|
if [[ -n "$NEW_PROXMOX_TOKEN_SECRET" ]]; then
|
||||||
|
proxmox_payload="$(printf '%s' "$proxmox_payload" | jq --arg v "$NEW_PROXMOX_TOKEN_SECRET" '.token_secret = $v')"
|
||||||
|
fi
|
||||||
|
if [[ -n "$NEW_PROXMOX_VERIFY_SSL" ]]; then
|
||||||
|
proxmox_payload="$(printf '%s' "$proxmox_payload" | jq --arg v "${NEW_PROXMOX_VERIFY_SSL,,}" '.verify_ssl = ($v == "true")')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$(json_canonical "$proxmox_payload")" != "$(json_canonical "$ORIGINAL_PROXMOX_JSON")" ]]; then
|
||||||
|
preflight_proxmox_candidate "$proxmox_payload"
|
||||||
|
log "Applying Proxmox settings rotation"
|
||||||
|
api_put "/settings/proxmox" "$proxmox_payload"
|
||||||
|
UPDATED_PROXMOX="true"
|
||||||
|
|
||||||
|
log "Post-rotation validation: Proxmox sync"
|
||||||
|
curl -fsS -X POST "${BACKEND_URL}/proxmox/sync" -H "Authorization: Bearer ${API_TOKEN}" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$NEW_PAYSTACK_SECRET" ]]; then
|
||||||
|
payment_payload="$(
|
||||||
|
printf '%s' "$payment_payload" | jq --arg v "$NEW_PAYSTACK_SECRET" '
|
||||||
|
if (.paystack_secret // "") == $v then . else .paystack_secret_previous = (.paystack_secret // "") | .paystack_secret = $v end
|
||||||
|
'
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$NEW_FLUTTERWAVE_SECRET" ]]; then
|
||||||
|
payment_payload="$(
|
||||||
|
printf '%s' "$payment_payload" | jq --arg v "$NEW_FLUTTERWAVE_SECRET" '
|
||||||
|
if (.flutterwave_secret // "") == $v then . else .flutterwave_secret_previous = (.flutterwave_secret // "") | .flutterwave_secret = $v end
|
||||||
|
'
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$NEW_FLUTTERWAVE_WEBHOOK_HASH" ]]; then
|
||||||
|
payment_payload="$(
|
||||||
|
printf '%s' "$payment_payload" | jq --arg v "$NEW_FLUTTERWAVE_WEBHOOK_HASH" '
|
||||||
|
if (.flutterwave_webhook_hash // "") == $v then .
|
||||||
|
else .flutterwave_webhook_hash_previous = (.flutterwave_webhook_hash // "") | .flutterwave_webhook_hash = $v end
|
||||||
|
'
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FINALIZE_PAYMENT_GRACE" == "true" ]]; then
|
||||||
|
payment_payload="$(
|
||||||
|
printf '%s' "$payment_payload" | jq '
|
||||||
|
.paystack_secret_previous = "" |
|
||||||
|
.flutterwave_secret_previous = "" |
|
||||||
|
.flutterwave_webhook_hash_previous = ""
|
||||||
|
'
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$(json_canonical "$payment_payload")" != "$(json_canonical "$ORIGINAL_PAYMENT_JSON")" ]]; then
|
||||||
|
log "Applying payment/webhook rotation payload"
|
||||||
|
api_put "/settings/payment" "$payment_payload"
|
||||||
|
UPDATED_PAYMENT="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$NEW_MONITORING_WEBHOOK_URL" ]]; then
|
||||||
|
notifications_payload="$(printf '%s' "$notifications_payload" | jq --arg v "$NEW_MONITORING_WEBHOOK_URL" '.monitoring_webhook_url = $v')"
|
||||||
|
fi
|
||||||
|
if [[ -n "$NEW_ALERT_WEBHOOK_URL" ]]; then
|
||||||
|
notifications_payload="$(printf '%s' "$notifications_payload" | jq --arg v "$NEW_ALERT_WEBHOOK_URL" '.alert_webhook_url = $v')"
|
||||||
|
fi
|
||||||
|
if [[ -n "$NEW_NOTIFICATION_EMAIL_WEBHOOK" ]]; then
|
||||||
|
notifications_payload="$(printf '%s' "$notifications_payload" | jq --arg v "$NEW_NOTIFICATION_EMAIL_WEBHOOK" '.notification_email_webhook = $v')"
|
||||||
|
fi
|
||||||
|
if [[ -n "$NEW_OPS_EMAIL" ]]; then
|
||||||
|
notifications_payload="$(printf '%s' "$notifications_payload" | jq --arg v "$NEW_OPS_EMAIL" '.ops_email = $v')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
notifications_payload="$(sanitize_notifications_payload "$notifications_payload")"
|
||||||
|
|
||||||
|
if [[ "$(json_canonical "$notifications_payload")" != "$(json_canonical "$ORIGINAL_NOTIFICATIONS_JSON_SANITIZED")" ]]; then
|
||||||
|
log "Applying alerting destination updates"
|
||||||
|
api_put "/settings/notifications" "$notifications_payload"
|
||||||
|
UPDATED_NOTIFICATIONS="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local summary_file
|
||||||
|
summary_file="/root/proxpanel-integration-rotation-${TS}.txt"
|
||||||
|
cat >"$summary_file" <<EOF
|
||||||
|
ProxPanel integration secret rotation completed at $(date -u +'%Y-%m-%d %H:%M:%S UTC')
|
||||||
|
|
||||||
|
Backend URL:
|
||||||
|
${BACKEND_URL}
|
||||||
|
|
||||||
|
Backup directory:
|
||||||
|
${BACKUP_DIR}
|
||||||
|
|
||||||
|
Changed blocks:
|
||||||
|
Proxmox settings: ${UPDATED_PROXMOX}
|
||||||
|
Payment/webhook settings: ${UPDATED_PAYMENT}
|
||||||
|
Notification destinations: ${UPDATED_NOTIFICATIONS}
|
||||||
|
|
||||||
|
Grace mode:
|
||||||
|
finalize_payment_webhook_grace=${FINALIZE_PAYMENT_GRACE}
|
||||||
|
|
||||||
|
Post checks:
|
||||||
|
GET ${BACKEND_URL}/health
|
||||||
|
POST ${BACKEND_URL}/proxmox/sync
|
||||||
|
|
||||||
|
Next:
|
||||||
|
If webhook grace fields were populated, finalize later with:
|
||||||
|
sudo bash ${APP_DIR}/infra/deploy/rotate-integration-secrets.sh --finalize-payment-webhook-grace
|
||||||
|
EOF
|
||||||
|
chmod 600 "$summary_file"
|
||||||
|
|
||||||
|
trap - ERR
|
||||||
|
log "Integration secret rotation completed successfully."
|
||||||
|
printf 'Summary: %s\n' "$summary_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user