What's Your Backup Plan?

What's Your Backup Plan?

in

Table of Contents

Druva

I got tasked with implementing a new backup system for all workstations within Hayward. The number of endpoints that would be backed up was about 199 Windows 10 workstations.
While there were some hiccups along the way, it was a great learning experience in terms of mass deployment of software at the computer and user level via group policy. Looking back, however, I would have gone the approach of using known folder redirection combined with Azure FileShares, but I was not in management at the time; my task was to implement the backup solution chosen.

Installation

Powershell Install Script

The PowerShell script is straightforward;
The script checks if the Druva backup client is installed on an endpoint; if not, the script will install it. Where it got interesting was the peculiarities of Druva. Software like Google Chrome, for example, is installed in the user profile (under “$env:LOCALAPPDATA\google” to be exact); thus, it does not need elevated privileges to be installed because it does not write to any protected directories.
Druva, on the other hand, does.
Therefore the PowerShell script below needs to be run as NT AUTHORITY\SYSTEM at least or from a High Integrity level Administrative account.
I opted to use a startup script.

param([string]$action)
# Because of the auto user registration we prob want that as a user logon script
$folder = 'C:\Program Files (x86)\Druva\inSync\7.1.0\inSyncAgent.exe'
function Install-Druva {
  # installing
  if (-not (Test-Path -Path $Folder)) {
      msiexec /qn /i "\\location\of\druva_client.msi" `
      TOKENV2='MASS DEPLOYMENT TOKEN' `
      SERVERLIST="cloud.druva.com:443" `
      LANGUAGE="en"
  }
}
function Uninstall-Druva {
    # uninstalling
    if (Test-Path -Path $Folder) {
        msiexec /x "\\bayonet\public\zinhart\gpo-packages\druva-client\inSync7.1.0r110275.msi"
    }
}
if ($action -eq 'INSTALL') {
    Install-Druva
}
elseif ($action -eq 'UNINSTALL') {
    Uninstall-Druva
}

Interacting with Druva API

Here is the code I used to interact with the Druva API. Most of it is similar if not exactly the same as: https://raw.githubusercontent.com/druvainc/inSync/master/inSync_APIs.py. The main difference is that I added the api_call_single and download_auth_key methods.

# https://raw.githubusercontent.com/druvainc/inSync/master/inSync_APIs.py
import requests
import time
import traceback
import sys 
import os

# Import Libs required for Bearer Token OAuth2.0
from oauthlib.oauth2 import BackendApplicationClient
from requests.auth import HTTPBasicAuth
from requests_oauthlib import OAuth2Session

# https://developer.druva.com/docs/get-bearer-token
client_id = 'Secret'
secret_key = 'ID'

# 
api_url = "https://apis.druva.com/"

# https://developer.druva.com/reference#reference-getting-started
def get_token(client_id, secret_key):
    #global auth_token
    auth = HTTPBasicAuth(client_id, secret_key)
    client = BackendApplicationClient(client_id=client_id)
    oauth = OAuth2Session(client=client)
    response = oauth.fetch_token(token_url='https://apis.druva.com/token', auth=auth)
    auth_token = response['access_token']
    expires_at = response['expires_at']
    return auth_token



def get_api_call(auth_token, api_url, api_path):
    nextpage = None
    while True:
        # print nextpage
        nextpage = _get_api_call(auth_token, api_url, api_path, nextpage)
        if not nextpage:
            break
#
def _get_api_call(auth_token, api_url, api_path, nextpage):
    headers = {'accept': 'application/json', 'Authorization': 'Bearer ' + auth_token}
    response = requests.get(api_url+api_path, headers=headers, params={'pageToken':nextpage})
    try:
        print ('Invoking API call')
        if response.status_code == 200:
            print (response.json())
            return response.json()['nextPageToken']
        elif response.status_code == 429:
            print ('Sleeping for 60 seconds')
            time.sleep(60)
            return _get_api_call(auth_token, api_url, api_path, nextpage)
        else:
            print ('Failure occured in API call.')
            print ('[ERROR CODE] : '+ str(response.status_code))
    except Exception as e:
        print (traceback.format_exc())

#
#get_token(client_id, secret_key)
#print ('Auth_token: ' + auth_token)

########################################################################
# inSync: API call to List all Users.
api_path = "insync/usermanagement/v1/users"

# inSync: API call to List all Profiles.
api_path = "insync/profilemanagement/v1/profiles"

# inSync: API call to List all Devices.
api_path = "insync/endpoints/v1/devices"

# inSync: API call to List all Devices Backups (Last Successful Bbackups).
api_path = "insync/endpoints/v1/backups"

# inSync: API call to List all Devices Restore activities.
api_path = "insync/endpoints/v1/restores"

# inSync: API call to List all Storages.
api_path = "insync/storagemanagement/v1/storages"

# inSync: API call to A user by email.
api_path = "insync/usermanagement/v1/[email protected]"

# insync: API call to Download auth key for a user
########################################################################
def api_single(auth_token, api_url, api_path, nextpage=None):
    headers = {'accept': 'application/json', 'Authorization': 'Bearer ' + auth_token}
    response = requests.get(api_url+api_path, headers=headers, params={'pageToken':nextpage})
    try:
        print ('Invoking API call')
        if response.status_code == 200:
            return response.json()
        else:
            print ('Failure occured in API call.')
            print ('[ERROR CODE] : '+ str(response.status_code))
    except Exception as e:
        print (traceback.format_exc())
def download_auth_key(auth_token, api_url, api_path, nextpage=None):
    headers = {'accept': 'application/json', 'Authorization': 'Bearer ' + auth_token}
    response = requests.get(api_url+api_path, headers=headers, params={'pageToken':nextpage})
    try:
        print ('Invoking API call')
        if response.status_code == 200:
            print(response.text)
            return response.text
            #return response.json()
        else:
            print ('Failure occured in API call.')
            print ('[ERROR CODE] : '+ str(response.status_code))
    except Exception as e:
        print (traceback.format_exc())

User Registration Script

The Druva Backup Client installs itself in C:\Program Files (x86), which is write-protected to High Integrity level Administrative process and, of course, the System account. Obviously, adding all domain user accounts to the Builtin\Administrators group would be trolling, and we didn’t have any just-in-time admin software.
In addition, some other security policies prevented me from installing the client from a domain user account, so I separated the install and sign-up steps. Druva does offer help in the Mass Deployment of their software. Specifically, you can set up a mass deployment token that auto-registers a user when the software is installed. The caveat is that in addition to the concerns I mentioned above, the Druva Backup Client must be installed while the user is logged in, and as I said, our users aren’t local administrators. Kind of a catch-22, right?
Below is the python install script, which I turned into a Windows executable with PyInstaller.

# Turn this script into an exe, python -m PyInstaller --onefile .\druva-user-registration.py
import sys
import os
import subprocess
from datetime import datetime

sys.path.append("..")
from lib.druva_api import client_id
from lib.druva_api import secret_key
from lib.druva_api import api_url
from lib.druva_api import get_token
from lib.druva_api import api_single
from lib.druva_api import download_auth_key


auth_token = get_token(client_id, secret_key)
#print(F"{client_id}")
#print(F"{secret_key}")
#print(F"{api_url}")
#print(F"{auth_token}")
USER_LOGON = os.getlogin()
MAX_DEVICES_PER_USER = 4 # LICENSE RESTRICTION
# SETUP LOGGING
REGISTRATION_LOG_FILE_NAME = F"C:\\Windows\\Temp\\druva-registration-{USER_LOGON}-{datetime.now().strftime('%d-%m-%Y-%H-%M-%S')}.txt"
REGISTRATION_LOG_FILE_HANDLE = open(REGISTRATION_LOG_FILE_NAME, 'a')
sys.stdout = REGISTRATION_LOG_FILE_HANDLE

print(F"user logon:{os.getlogin()}")
API_PATHS = {
    'ENDPOINTS':'insync/endpoints/v1/devices',
    'USER_MANAGEMENT': 'insync/usermanagement/v1/users',
    'AUTH_KEY': lambda druva_id: F"insync/usermanagement/v1/users/{druva_id}/download_user_auth_key"
}


# Get Druva unique ID for the current user
user_data = api_single(auth_token, api_url, F"{API_PATHS['USER_MANAGEMENT']}?emailID={USER_LOGON}@haywardlumber.com")
print('user data: ',user_data)
druva_user_id=user_data['users'][0]['userID']
print('druva user id: ', druva_user_id)

# Check if device is already registered for the current user.
# If it is we abort
current_users_devices = F"{API_PATHS['ENDPOINTS']}?userID={druva_user_id}"
user_devices = api_single(auth_token, api_url, current_users_devices)
for device in user_devices['devices']:
    print(device['deviceName'])
    hostname = os.getenv('COMPUTERNAME','defaultValue')
    if hostname == device['deviceName']:
        print(F"Device: [{hostname}] is already registered aborting")
        sys.exit(0)
    elif len(user_devices) > MAX_DEVICES_PER_USER:
        print(F"User has already registered {MAX_DEVICES_PER_USER} devices")

# Assuming we get here this a new device for the currently logged in user.
# So we proceed with the registration of the device for the currently logged in user.


# Download an auth key for the current user
auth_key = download_auth_key(auth_token, api_url, API_PATHS['AUTH_KEY'](druva_user_id))
print(F"auth_key: {auth_key}")

# Write auth key to C:\users\CURRENT_USER\downloads
downloads_folder = F"C:\\users\\{USER_LOGON}\\downloads"
registration_filname = F"{USER_LOGON}.idk"
absolute_path_to_auth_key = F"{downloads_folder}\\{registration_filname}"
file_handle = open(absolute_path_to_auth_key,'w')
file_handle.write(auth_key)
file_handle.close()

# Register the device
insyc_agent_location = "C:\\Program Files (x86)\\Druva\\InSync"
# unfortunately for every update, druva creates a new directory with a version number,
# which contains the inSyncAgent
# so we have to search for the newest directory, get the absolute path ... bleh
targets = []
for directories in os.listdir(insyc_agent_location):
    targets.append(directories)
    print(directories)
targets.sort(reverse=True)
insync_agent_exe_location = F"{insyc_agent_location}\\{targets[0]}\\inSyncAgent.exe"
print(F"Insync agent abs path: {insync_agent_exe_location}")
process = subprocess.run([F"{insync_agent_exe_location}",F"{absolute_path_to_auth_key}"])

# Delete auth key
os.remove(absolute_path_to_auth_key)

# Close logging file
REGISTRATION_LOG_FILE_HANDLE.close()

Deployment via GPO

The two scripts above are deployed by the following GPO:

  • The installation script is run as a startup script.
  • The signup script is run under each users account.

Bitlocker Recovery Tab

Logging

Generating Statistics with Python (i.e providing reports for management)

The last piece of the puzzle is providing statistics to management. Who doesn’t love pivot tables?
The script below generates a CSV file, consumable by Microsoft Excel, with a bunch of different data points, such as when the endpoint was signed up, the total amount of data backed up, etc.

import sys
import os
import subprocess
from datetime import datetime
import csv
sys.path.append("..")
from lib.druva_api import client_id
from lib.druva_api import secret_key
from lib.druva_api import api_url
from lib.druva_api import get_token
from lib.druva_api import api_single

auth_token = get_token(client_id, secret_key)
API_PATHS = {
    'ENDPOINTS':'insync/endpoints/v1/devices',
    'USER_MANAGEMENT': 'insync/usermanagement/v1/users',
    'AUTH_KEY': lambda druva_id: F"insync/usermanagement/v1/users/{druva_id}/download_user_auth_key"
}

all_devices_response = api_single(auth_token, api_url, API_PATHS['ENDPOINTS'])
all_users_response =api_single(auth_token, api_url, API_PATHS['USER_MANAGEMENT'])


all_devices = all_devices_response['devices']
all_users = all_users_response['users']

print(all_devices[0])
print(all_users[0])
data = []
for device in all_devices:
    for user in all_users:
        if device['userID'] == user['userID']:
            row = (
                user['emailID'],
                user['ldapGUID'],
                device['userID'],
                device['profileID'],
                user['status'],
                device['deviceName'],
                user['quota'],
                device['totalBackupData'],
                device['totalBackupDataInBytes'],
                user['privacySettingsEnabled'],
                user['addedOn'].split('T')[0],
                device['lastConnected'].split('T')[0],
                device['clientVersion'],
                device['upgradeState'],
                device['deviceOS'],
                device['deviceOSVersion'],
                device['platformOS'],
                device['uuid'],
                device['deviceStatus']
            )
            data.append(row)

row_headers = [
    'emailID',
    'ldapGUID',
    'druvaUserID',
    'druvaUserProfileID',
    'userStatus',
    'deviceID',
    'userQuota',
    'totalBackupData',
    'totalBackupDataInBytes',
    'privacySettingsEnabled',
    'addedOn',
    'lastConnected',
    'clientVersion',
    'clientUpgradeState',
    'deviceOS',
    'deviceOSVersion',
    'platformOS',
    'deviceUUID',
    'deviceStatus'

]
with open(F"druva-report-{datetime.now().strftime('%m-%d-%Y')}.csv",'w', newline='') as out:
    csv_out=csv.writer(out)
    csv_out.writerow(row_headers)
    for row in data:
        csv_out.writerow(row)