Backing up linux, Mac, and Windows servers vi rsync

September 23rd, 2006

These scripts allows for remote incremental daily snapshot backups of linux, mac, and windows based servers.
The theory is that we want a full snapshot of any given day without taking up the full disk space required. The solution us to use rsync with hard links.
In addition I’ve made it backup to an encrypted disk image stored on a usb drive in OS X.
This makes it fairly portable and secure.
So far the only down-side is that the password for the encrypted drive must be stored in the script (or in another text file) as plain text. To avoid prying eyes the script has been chmod’ded 700, and is owned by root.

The setup:

  1. Create a public and private key pair and distribute the public to all linux servers to be backed up, and make sure sudo rsync can be run by the local backup user without requiring a password.
  2. Create an encrypted disk image using Disk Utility under Mac OS X. Ours is on a 250GB removable USB drive, but if you have access to a RAID drive with more space make it as large as you like.
  3. Our setup creates a daily snapshot and maintains the last 30 days, rotating out the oldest to save space, adjust to the availability of your space.

  4. Because rsync doesn’t run natively or easily on Windows I’m using XCOPY to backup windows servers to an intermediary Samba share on a linux box and then rsync’ing that. While not perfect, it does allow for hard-linking, and daily snapshots.

The script:
#!/bin/sh

# make sure we're running as root
if (( `id -u` != 0 )) ;
then { echo "Sorry, must be root. Exiting..."; exit; }
fi

# Tell the log file we're starting:
echo "==========================================="
echo "Backup Started: "
date

echo "Mounting the disk image"
echo -n 'DiskImagePassword' | hdiutil attach -stdinpass /Volumes/backup1/backupimagename.dmg

echo "Rotating the backups"
# Repeat for as many days as you would like to maintain your backup.
# If you choose to run your cron job only once a week, this would be
# the number of weekly backups
mv /Volumes/backupimagename/daily-30 /Volumes/backupimagename/daily-tmp
mv /Volumes/backupimagename/daily-29 /Volumes/backupimagename/daily-30
mv /Volumes/backupimagename/daily-28 /Volumes/backupimagename/daily-29
mv /Volumes/backupimagename/daily-27 /Volumes/backupimagename/daily-28
mv /Volumes/backupimagename/daily-26 /Volumes/backupimagename/daily-27
mv /Volumes/backupimagename/daily-25 /Volumes/backupimagename/daily-26
mv /Volumes/backupimagename/daily-24 /Volumes/backupimagename/daily-25
mv /Volumes/backupimagename/daily-23 /Volumes/backupimagename/daily-24
mv /Volumes/backupimagename/daily-22 /Volumes/backupimagename/daily-23
mv /Volumes/backupimagename/daily-21 /Volumes/backupimagename/daily-22
mv /Volumes/backupimagename/daily-20 /Volumes/backupimagename/daily-21
mv /Volumes/backupimagename/daily-19 /Volumes/backupimagename/daily-20
mv /Volumes/backupimagename/daily-18 /Volumes/backupimagename/daily-19
mv /Volumes/backupimagename/daily-17 /Volumes/backupimagename/daily-18
mv /Volumes/backupimagename/daily-16 /Volumes/backupimagename/daily-17
mv /Volumes/backupimagename/daily-15 /Volumes/backupimagename/daily-16
mv /Volumes/backupimagename/daily-14 /Volumes/backupimagename/daily-15
mv /Volumes/backupimagename/daily-13 /Volumes/backupimagename/daily-14
mv /Volumes/backupimagename/daily-12 /Volumes/backupimagename/daily-13
mv /Volumes/backupimagename/daily-11 /Volumes/backupimagename/daily-12
mv /Volumes/backupimagename/daily-10 /Volumes/backupimagename/daily-11
mv /Volumes/backupimagename/daily-9 /Volumes/backupimagename/daily-10
mv /Volumes/backupimagename/daily-8 /Volumes/backupimagename/daily-9
mv /Volumes/backupimagename/daily-7 /Volumes/backupimagename/daily-8
mv /Volumes/backupimagename/daily-6 /Volumes/backupimagename/daily-7
mv /Volumes/backupimagename/daily-5 /Volumes/backupimagename/daily-6
mv /Volumes/backupimagename/daily-4 /Volumes/backupimagename/daily-5
mv /Volumes/backupimagename/daily-3 /Volumes/backupimagename/daily-4
mv /Volumes/backupimagename/daily-2 /Volumes/backupimagename/daily-3
mv /Volumes/backupimagename/daily-1 /Volumes/backupimagename/daily-2
mv /Volumes/backupimagename/daily-0 /Volumes/backupimagename/daily-1
mkdir /Volumes/backupimagename/daily-0

echo "Backing up a Server"
# Repeat as necessary
# rsync uses RAM exponentially with the number of files you are backing up
# You may want to break this command up into multiple if you have a server with many files such as a mail server
rsync -az --delete --link-dest=/Volumes/backupimagename/daily-1/server.domain.com -e "ssh -i /path/to/private/key" backupuser@serverIP:/folder/to/backup /Volumes/backupimagename/daily-0/server.domain.com/

echo "Remote backup completed"
date
touch /Volumes/backupimagename/daily-0

echo "Deleting temp files"
rm -rf /Volumes/backupimagename/daily-tmp

# See how full the drive is
df -h

date

echo "Unmounting the disk image:"
hdiutil detach /Volumes/backupimagename

# Tell the log file we're done:
echo "Backup Finished: "
echo "==========================================="
echo " "
date

Backing up the Windows to a Samba share is relatively straight forward. For security purposes I created Samba shares that are only accessible to one user, the account that runs the script on the Windows servers.

Samba Share:
[global]
workgroup = yourdomain
security = user
wins support = yes
[WinBackups]
path = /var/winbackups
comment = Windows Backups
public = no
write list = administrator

The Window script:
echo off
:: variables
set drive="\\linux-box\sambashare\backupfolder"
set backup1="E:\FirstLocation"
set backup2="E:\SecondLocation"
set backupcmd=xcopy /s /c /d /e /h /i /r /k /y

echo ### Backing up First Location...
%backupcmd% %backup1% %drive%

echo ### Backing up Second Location...
%backupcmd% %backup2% %drive%

I’ve scheduled the xcopy script to run nightly well before the rsync script, with large sets of data you might have to run them on different days.

I’ve also scheduled the rsync backup script to run nightly and send me an email detailing what it wrote to the log.
MAILTO=" email@yourdomain.com"
30 22 * * * /private/var/root/backup-scripts/backup-daily.sh 2>&1 | tee -a /private/var/root/backup-scripts/logs/backup-daily.log | mail -s "Daily Backup" email@yourdomain.com

If you wish to make a backup of your backup, simply copy the entire disk image to another drive. Make sure the image isn’t mounted when you copy it.

Creating ssh keys:

This is based on this article.

create a key pair on your backup server machine.

This section should only be done once.

ssh-keygen -t rsa -f ~/rsync-key

This creates two files public and private keys.

place the private key into /etc/ssl/private/rsync.key and secure it.
mv rsync-key /etc/ssl/private/rsync.key
chown root:root /etc/ssl/private/rsync.key
chmod 600 /etc/ssl/private/rsync.key

The public key is the file that you place in the /home/(rsync_user)/.ssh/authorized_keys file on all your clients so that they know to allow you to use rsync as that user without a password. (Note that (rsync_user) should be replaced with the user you are using to connect with so a command that looks like this: rsync -avz -e “ssh -i /etc/ssl/private/rsync.key” user@server:/files/to/backup /backup/location, you would replace it with eanders.

Before you place it on your client machines you’ll want to tell it that it should only be valid if used from your backup server and that it is only valid for the rsync command. This is security for your client not your server. Having this public key means that anyone on a machine with the private key can connect and execute stuff on your machine without a password.

You should place the following at the beginning of the rsync-key.pub file:
from="ip.of.backup.server",command="/home/(rsync_user)/.rsync/validate-rsync.sh"
The file would then look something like this:
from="10.0.100.5",command="/home/(rsync_user)/.rsync/validate-rsync.sh" ssh-rsa AAAAB3NzaC1yc2...

Repeat the following section as needed:

In most circumstances you will only be backing up information that the user you connect as has permission to. In these cases you should copy the edited rsync-key.pub file to the users home directory and place it in the .ssh/authorized_keys file. If your user was eanders you would move the file to eanders’ home directory and do something like this:

mkdir /home/(rsync-user)/.ssh
cat rsync-key.pub >> .ssh/authorized_keys

This will place the key at the end of the authorized_keys file. It is now safe to delete the key.
Finally you’ll need to place the validate-rsync script in the /home/(rsync-user)/.rsync/ directory on your client computer so that you can allow rsync to be run without a password, but nothing else will work.
You can just create the .rsync directory and then paste the following into validate-rsync.sh within that file.
mkdir /home/(rsync-user)/.rsync
vi /home/(rsync-user)/.rsync/validate-rsync.sh

#!/bin/sh
case "$SSH_ORIGINAL_COMMAND" in
*\&*)
echo "Rejected"
;;
*\;*)
echo "Rejected"
;;
rsync*)
$SSH_ORIGINAL_COMMAND
;;
*)
echo "Rejected"
;;
esac

Then we tell it that the validate-rsync.sh file should be executable
chmod 755 /home/(rsync-user)/.rsync/validate-rsync.sh

At this point you should be able to run an rsync command from your backup server as root to backup anything that (rsync-user) has permission to backup. Something like:
rsync -avz -e "ssh -i /etc/ssl/private/rsync.key" (rsync-user)@client_ip_address:/files/to/backup /backup/location

If you want to backup a machine which contains files that are only readable by root you need to do the following

This will be the case where you have a file server that hosts files which are only readable by inidividual users (root always has access) but your administrator user can’t access their files without the use of sudo.

First you will create an unprivileged user on the client machine, we are using the username rsync:
adduser rsync
Give this user whatever password you want.

Then follow the instructions above replacing (rsync-user) with the user rsync.

Next you will need to login and gain root access to the server and we’ll move the rsync application and replace it with a script.
mv /usr/bin/rsync /usr/bin/rsync_real
vi /usr/bin/rsync

Fill the file with this script:

#!/bin/sh
/usr/bin/sudo /usr/bin/rsync_real "$@"

Then make it executable:

chmod 755 /usr/bin/rsync

This tells the computer that when you ask it to run rsync it should actually run it as root, so issuing:
rsync
will result in this command actually running:
sudo rsync_real

Finally you need to tell your client computer that the (rsync-user) should be able to run rsync_real as sudo without a password since we’ll be running it without an interactive shell. To do this run, as root:
visudo
and add the following line:
(rsync-user) ALL= NOPASSWD: /usr/bin/rsync_real

For our purposes we are making a single script that will run various rsync commands to backup servers across the wan, this script will reside in /var/usb1/backup-district.sh
In addition we will rsync /var/usb1 to /var/usb2 each night with an rsync command.

If you are having problems where it is continually asking for your password (even though you’ve done all the key stuff) make sure that the line you added to visudo is at the bottom. That file appears to be processed from the top down instead of going from broad to granular.
This is especially a problem when your user is also an administrator and already has an entry in visudo.

Allow users to reset their Active Directory passwords via a web form.

September 23rd, 2006

This is the functioning post that allowed us to be able to reset user’s Active Directory passwords via a php page hosted on a linux box. Combine this with the password resetting delegation of a previous post for a bit more security. We’re also going to set this up to figure out what group someone is in and let teachers reset thier students passwords.

Before you do this you need to generate an ssl certificate on the windows box and import it into the linux box, see a previous post.

This information was mostly gleaned from here:
———————————————————
The post: http://forums.devshed.com/ldap-programming-76/modifying-active-directory-passwords-through-php-and-iis-74683-7.html
———————————————————

Here is the final script I am using for a user to change his or her password.
The form to submit to the script:
<form method="post" action="change_password.php">
username: <input type="text" name="uid" />
<br />
password: <input type="password" name="password" />
<br />
new password: <input type="password" name="newpass1" />
<br />
confirm new password: <input type="password" name="newpass2" />
<br />
<input type="submit" name="submit" value="Change Password" />
</form>

Config File:
< ?PHP
/*** Variable Settings ***/
// administrative bind user
// Admin account with permission to reset passwords
$adminUID = 'adminusername';
$adminPass = 'AdministrativePassword';
// ldap server info, moved to config file
$ldapserver = 'ldapserver.mydomain.com';
$baseDN = 'DC=mydomain,DC=com';
?>

Script:
< ?PHP
require_once('/var/www_config_files/secure/change_password.inc.php');
/*** Variable Settings ***/
$uid = $_POST['uid']; // Should be something like jsmith
$userbindDN = $uid . '@yourdomain.com'; // jsmith@yourdomain.com
//existing password
$userbindPass = $_POST['password'];
// new password
$passwd1 = $_POST['newpass1'];
$passwd2 = $_POST['newpass2'];
// administrative bind user
// Admin account with permission to reset passwords
$authbindDN = $adminUID . '@yourdomain.com';
$authbindPass = $adminPass;

// ldap server info, moved to config file
//$ldapserver = 'ldapserver.yourdomain.com';
//$baseDN = 'DC=mydomain,DC=com';
/**************************/

/************* Main Script Code ***************/
/** Connect SSL to Ldap Server **/

$ldap = ldap_connect('ldaps://'.$ldapserver,686);
//$ldap = ldap_connect($ldapserver);
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);

echo "Verifying old password ...<br>";

ldap_bind($ldap, $userbindDN, $userbindPass);

if (ldap_errno($ldap) !== 0)
{
exit('ERROR: Username or Password Invalid - Please Try again');
// exit('ERROR: User ID/Password Invalid - '.ldap_error($ldap));
}
/** We got this far, let's bind with an admin user **/
echo "Authenticated, changing password ...<br />";

ldap_bind($ldap, $authbindDN, $authbindPass);
if (ldap_errno($ldap) !== 0)
{
exit('ERROR: Unable to bind with admin user info - '.ldap_error
($ldap));
}

// Searching for the user
$filter = "(|(samaccountname=$uid))";
$justthese = array("cn");

$searchResults = ldap_search($ldap, $baseDN, $filter, $justthese);
// no matching records
$info = ldap_get_entries($ldap, $searchResults);
if ($searchResults === false)
{
exit('User ($uid) not found.');
}
if (!is_resource($searchResults))
{
exit('Error in search results.');
}

//echo "<pre>";
//print_r($info);
//echo "</pre>";

$entry = ldap_first_entry($ldap, $searchResults);
if (!is_resource($entry))
{
exit('Couldn\'t get entry');
}
$userDn = ldap_get_dn($ldap, $entry);

if ($passwd1 == $passwd2){
// prepare data
$newPassword = $passwd1;
$newPassword = "\"" . $newPassword . "\"";
$len = strlen($newPassword);
for($i = 0; $i < $len; $i++)
{
$newPassw .= "{$newPassword{$i}}\000";
}
$newPassword = $newPassw;
$userdata['unicodePwd'] = $newPassword;

// echo "Changing Password<br /><br />";
echo "Username = ".$uid."<br />";
// echo "User login ID = ".$userbindDN."<br />";

$result = ldap_mod_replace($ldap, $userDn , $userdata);
if($result)
{
// echo "User modified!<br />" ;
}else{
echo "There was a problem!<br />";
echo ldap_error($ldap)."<br />";
}
/** Now try to bind with the username and new password to
ensure change**/
echo "Now testing new password to insure change<br />";
ldap_bind($ldap, $userbindDN, $passwd1);
if (ldap_errno($ldap) !== 0)
{
exit('ERROR: User ID/Password Invalid - '.ldap_error($ldap));
}else{
echo "Password Verified.<br />Password change complete.<br />";
echo "<p>You may now close this window, your password for
your computer, email, and web access has been updated.</p>";
}
}
?>

In addition I have modified this script to allow teachers to reset student passwords:
The page that requests information:
<html>
<head>
<title>Reset Student Password</title>
</head>
<body>
<h2>Reset a Student Password</h2>
<p>Teachers may use this form to reset the password of a student in their class. Simply fill out all of the fields and click the "Reset Student Password" button.</p>
<form method="post" action="change_password.php">
Teacher Username: <input type="text" name="uid" />
<br />
Teacher Password: <input type="password" name="password" />
<br />
Student Username: <input type="text" name="student_uid" />
<br />
New Student Password: <input type="password" name="newpass1" />
<br />
Confirm Student Password: <input type="password" name="newpass2" />
<br />
<input type="submit" name="submit" value="Reset Student Password" />
</form>
</body>
</html>

Uses the same config file from above.

The script:
< ?PHP
require_once('/var/www_config_files/secure/change_password.inc.php');
/*** Variable Settings ***/
$uid = $_POST['uid']; // Should be something like jsmith
$student_uid = $_POST['student_uid'];
$userbindDN = $uid . '@yourdomain.com'; // jsmith@yourdomain.com
//existing password
$userbindPass = $_POST['password'];
// new password
$passwd1 = $_POST['newpass1'];
$passwd2 = $_POST['newpass2'];
// administrative bind user
// Admin account with permission to reset passwords
$authbindDN = $adminUID . '@yourdomain.com';
$authbindPass = $adminPass;
// ldap server info, moved to config file
//$ldapserver = 'ldapserver.yourdomain.com';
//$baseDN = 'DC=yourdomain,DC=com';
/**************************/
/*
The theory: Bind as the teacher to verify password and get group membership
Bind as the admin
Grab student dn and group membership
if the teacher is a member of teachers and the student is a member of students
check to see that they are both a member of another group (other than Domain Users)
If they are: reset the student password

*/
/************* Main Script Code ***************/
/** Connect SSL to LDAP Server **/
?>
<html>
<head>
<title>Reset Student Password Results</title>
</head>
<body>
< ?
//echo "Connecting SSL to server<br>";
$ldap = ldap_connect('ldaps://'.$ldapserver,686);
//$ldap = ldap_connect($ldapserver);
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
/** Now try to bind with the username and password **/
echo "Verifying teacher credentials ...<br />";
ldap_bind($ldap, $userbindDN, $userbindPass);
if (ldap_errno($ldap) !== 0)
{
exit('ERROR: User Teacher username or password invalid');
//exit('ERROR: User ID/Password Invalid - '.ldap_error($ldap));
}
echo "Teacher credentials verified. <br />";

/** We got this far, let's bind with an admin user **/
ldap_bind($ldap, $authbindDN, $authbindPass);
if (ldap_errno($ldap) !== 0)
{
exit('ERROR: Unable to bind with admin user info - '.ldap_error
($ldap));
}

// Figure out who the teacher is so we can get their membership info
$filter = "(&(samaccountname=".$uid."))";
$justthese = array("dn");

$searchResults = ldap_search($ldap, $baseDN, $filter, $justthese);
// no matching records
$info = ldap_get_entries($ldap, $searchResults);
if ($searchResults === false)
{
exit('User ($uid) not found in AD');
}
if (!is_resource($searchResults))
{
exit('Error in search results.');
}

//echo "<pre>";
//print_r($info);
for ($i=0; $i < $info["count"]; $i++) {
//print "LDAP DN: " . $info[$i]['dn'];
//echo "<br /><br />";
$userDN = $info[$i]['dn'];
}
//echo "</pre>";

$filter = "(&(samaccountname=".$student_uid."))";
$justthese = array("dn");

$searchResults = ldap_search($ldap, $baseDN, $filter, $justthese);
// no matching records
$info = ldap_get_entries($ldap, $searchResults);
if ($searchResults === false)
{
exit('User ($uid) not found in AD');
}
if (!is_resource($searchResults))
{
exit('Error in search results.');
}

for ($i=0; $i < $info["count"]; $i++) {
//print "LDAP DN: " . $info[$i]['dn'];
//echo "<br /><br />";
$student_userDN = $info[$i]['dn'];
}

// Searching for the teacher
$filter = "(&(objectCategory=group)(member=".$userDN."))";
$justthese = array("dn");

$searchResults = ldap_search($ldap, $baseDN, $filter, $justthese);
// no matching records
$info = ldap_get_entries($ldap, $searchResults);
if ($searchResults === false)
{
exit('User ($uid) not found in AD');
}
if (!is_resource($searchResults))
{
exit('Error in search results.');
}

//echo "<pre>";
for ($i=0; $i < $info["count"]; $i++) {
$teacher_groups[] = $info[$i]['dn'];
}
//print_r($teacher_groups);
//echo "</pre>";

// Searching for the student
$filter = "(&(objectCategory=group)(member=".$student_userDN."))";
$justthese = array("dn");

$searchResults = ldap_search($ldap, $baseDN, $filter, $justthese);
// no matching records
$info = ldap_get_entries($ldap, $searchResults);
if ($searchResults === false)
{
exit('User ($student_uid) not found in AD');
}
if (!is_resource($searchResults))
{
exit('Error in search results.');
}

//echo "</pre><pre>";
for ($i=0; $i < $info["count"]; $i++) {
$student_groups[] = $info[$i]['dn'];
}
//print_r($student_groups);
//echo "</pre>";

foreach ($teacher_groups as $teacher_group) {
$is_teacher = stristr($teacher_group, 'Teacher');
}
//echo "$is_teacher <br />";
if($is_teacher !== 'FALSE') {
echo "Verifying group membership...";
// In this case the teacher must be a member of the same group as the student
// This was added to allow fifth grade teachers to reset fifth grades student
// passwords, but not sixth grade student passwords.
foreach ($student_groups as $student_group) {
$same_group = array_search($student_group, $teacher_groups);
//echo $same_group;

}
if($same_group !== 'FALSE') {
echo "Group membership verified";
echo "<br />";
echo "Resetting student password...";

//---------------------------------------------//
// Check Passwords to make sure they match.
if ($passwd1 == $passwd2){
// prepare data
$newPassword = $passwd1;
$newPassword = "\"" . $newPassword . "\"";
$len = strlen($newPassword);
for($i = 0; $i < $len; $i++) {
$newPassw .= "{$newPassword{$i}}\000";
}
$newPassword = $newPassw;
$userdata['unicodePwd'] = $newPassword;

echo "for username = ".$student_uid."<br />";

$result = ldap_mod_replace($ldap, $student_userDN, $userdata);
if($result)
{
echo "User modified!<br />" ;
}else{
echo "There was a problem!<br />";
echo ldap_error($ldap)."<br />";
}
/** Now try to bind with the username and new password to
insure change**/
echo "Now testing new password to ensure change<br />";
ldap_bind($ldap, $student_userDN, $passwd1);
if (ldap_errno($ldap) !== 0)
{
exit('ERROR: User ID/Password Invalid - '.ldap_error($ldap));
}else{
echo "Password Verified.<br />Password change complete.<br />";
echo "<p>You may now close this window, the student password has been modified.</p><p>";
}
}

//--------------------------------------------------//

}
}
else {
echo "Sorry we could not verity that you are a teacher.";
}
?>
</p></pre></body>
</html>

Delegate Password Resetting Control in Active Directory

September 23rd, 2006

This allows you to have a user who can reset passwords for other users, but not necessarily administer the domain. We are using it so people can reset their passwords from a PHP page and so teachers will be able to reset their students’ passwords.
See:http://support.microsoft.com/default.aspx?scid=KB;en-us;296999

  1. Create a group that you want to be able to reset passwords
  2. Add the users you want to give the password resetting ability to to that group.
  3. Right Click on an OU that you want the resetters to be able to reset and choose “Delegate Control”
  4. Click Next and then add the Group you just created to the empty box.
  5. Click Next and then Check the Reset Password Box.
  6. Click Finish

Network Printers Assigned by Computer via a script run by a GPO

September 23rd, 2006

To assign a network printer via GPO you can either assign them to the user or to a computer. Scripting Printer installation for a user is fairly simple and straight forward.

  1. Create a script to map the appropriate printer:
    Option Explicit
    Dim objNetwork

    Set ObjNetwork=CreateObject("Wscript.Network")
    objNetwork.addWindowsPrinterConnection"\\serveripaddress\printername"
    Wscript.Quit

  2. In windows explorer copy the file. Open GPMC.
  3. Create a GPO for the appropriate user OU. Edit the settings and navigate to:
    User Configuration->Windows Settings->Scripts->Logon
  4. Add a script, just type the name of the file, not the path.
  5. Click the Show Files button and Paste your script into the Folder that opens.

If you login as a user within this OU you should now see the printer is available to you.

To assign a printer by computer, such as in a lab situation where anyone who logs in should have access to the printer follow the previous steps, but make the GPO on the OU that contains the computers. Note that you are making a GPO that targets users, but applying it to computers. To make this work you need to add one more setting to your GPO.

  1. Navigate to:
    Computer Configuration->Administrative Templates->System->Group Policy
  2. Set the value of “User Group Policy loopback processing mode” to Enabled and use the “Merge” option.

If you now login to a computer in the OU that you just applied the policy to you will have access to that printer, but if you move to a computer outside the OU it won’t be available to you.

The Group Policy Loopback processing mode of merge essentially means that policies should be applied to the computer at startup, the user at login, and then the user section of the computer policy afterward (still at login.)

Here is a relatively concise set of information on GPO loopback.