Two-factor authentication through Windows Server 2008 Net Policy Server

Nick Owen of WiKID Systems Inc. offers a step-by-step tutorial to help enterprises add strong authentication to the network.

<meta content="text/html; charset=utf-8" http-equiv="content-type"/><style type="text/css"> <!-- .command { padding: 1em; border: 1px dashed #2f6fab; color: black; background-color: #f9f9f9; line-height: 1.1em; font-family: Courier New, Courier, mono; font-size: 12px; font-style: italic; } .system { color: black; font-family: Courier New, Courier, mono; font-size: 12px; font-style: italic; } .highlight { color: #FF0000; font-family: Georgia, "Times New Roman", Times, serif; font-size: 12px; text-decoration: underline; } --> </style><p>Increasingly, whether due to regulatory requirements or a basic recognition that static passwords just don't provide adequate security, organizations are implementing some form of strong authentication. Like all new efforts, before you start you want to be reasonably assured that you will succeed. In this tutorial we will document how to add two-factor authentication to various Microsoft remote access solutions through the Windows Server 2008 <a href="" target="" rel="nofollow">Network Policy Server</a>. For two-factor authentication, we will be using the <a href="" target="" rel="nofollow">WiKID Strong Authentication Server - Enterprise Edition</a>. <a href="" target="" rel="nofollow">WiKID</a> is a dual-sourced, software-based two-factor authentication system. While the document is product specific, the process is typically the same no matter the products.</p><p>Assume that you have a mixed OS environment with some Windows, some Linux/Unix. You have a new requirement for two-factor authentication to meet PCI requirements. You intend to protect all key systems, which are mostly linux and you are going to lock down your remote desktop with two-factor authentication too (though we will only discuss the SSH here). The plan is to create an SSH gateway server that is locked down with two-factor authentication. Admins can then jump from the gateway box to other servers using public key authentication. </p><p>SSH offers a highly secure channel for remote administration of servers. However, since you face an audit for PCI, you have become aware of some potential authentication related short-comings that may cause headaches in an audit. For example: </p><aside id="fsb-2398" class="fakesidebar fakesidebar-auto">[ Keep up with <a href="">8 hot cyber security trends (and 4 going cold)</a>. Give your career a boost with <a href="">top security certifications: Who they're for, what they cost, and which you need</a>. | <a href="">Sign up for CSO newsletters</a>. ]</aside><ul> <li>There is no way to control which users have public key authorization</li> <li>There is no way to enforce passphrase complexity (or even be sure that one is being used)</li> <li>There is no way to expire a public key</li> </ul><p>Additionally, your intention is to add two-factor authentication to other services, such as RDP and a VPN. There is great benefit in having a single two-factor authentication service for all those services and SSH keys will not work for other services.</p><h2>An overview</h2><p>After everything is configured, the system will work like this: The user generates a one-time passcode from their WiKID software token. They enter it into the SSH password field. The credentials are passed from the SSH gateway to NPS via radius. NPS validates that the user is active in AD and in the proper group. If so, it sends the username and one-time password to the WiKID Strong Authentication Server still using Radius. If the OTP is valid, the WiKID server responds to the NPS, which in turn responds to the SSH gateway server and the user is granted access. Note that this process is only for authentication, session management is still handled by the SSH gateway or any other remote access service you are using. </p><h2>First we will enable Windows Server 2008 Network Policy Server (NPS)</h2><p>Add the "Network Policy and Access Services" role to your domain controller.</p><aside class="nativo-promo nativo-promo-1 smartphone" id=""> </aside><p>Enable these role services during installation:</p><aside id="fsb-2599" class="fakesidebar fakesidebar-auto fakesidebar-sponsored"><strong>[ <a href="" rel="nofollow">Prepare to become a Certified Information Security Systems Professional with this comprehensive online course from PluralSight. Now offering a 10-day free trial!</a> ] </strong></aside><ul><li>Network Policy Server</li> <li> Routing & Remote Access Services</li> <li>Remote Access Service</li> <li> Routing</li> </ul><h2>Next we add a new RADIUS Client - The SSH Gateway in this case.</h2><p>From Administrative Tools select Network Policy Server</p><aside class="nativo-promo nativo-promo-1 tablet desktop" id=""> </aside><p>Right click on Radius Clients and Select New</p><p> Add a name, the ip address of your remote access server (RAS, VPN, etc) and create a shared secret. You will enter the same shared secret on the WiKID server.</p><p>Click OK</p><h2>Add a new Radius Server - The WiKID Strong Authentication Server</h2><p>Right click on Remote RADIUS servers and name the group, something like "WiKID".</p><aside class="nativo-promo nativo-promo-2 tablet desktop smartphone" id=""> </aside><p>Click the Add button to add a new radius server in the group.</p><p>Enter the IP address of the WiKID server on the first tab. On the second tab, enter the shared secret. That should be all you need to change.</p><h2>Creating a Network Policy</h2><p>Now that we've created the radius client and radius server (WiKID), we need a new Network Policy that tells IAS which users to proxy to WiKID. </p><p>Enter a name and leave Type of network access server as Unspecified or choose your remote access system. </p><p>Click on the Conditions tab. I added a condition for all requests from my server's IP address.</p><p>Click on the Settings Page. Click on Authentication and Select the button for "Forward requests to the following remote RADIUS server group for authentication. Choose WiKID.</p><h2>Configuring the WiKID Strong Authentication Server.</h2><p>Now that we've configured the NPS to proxy authentications, we need to configure WiKID to accept them. See the <a href="" target="" rel="nofollow">WiKID installation manual</a> for the details on how to install and configure the WiKID server. Here we're just going to be adding a radius network client for the NPS:</p><p>Log into the WiKIDAdmin web interface.</p><p>Click on the Network Clients tab.</p><p>Click on "Create New Network Client". Give the Network Client a name, specify the IP address, select Radius as the protocol and choose which WiKID Domain to use. (WiKID domains hold the users and specify certain security parameters such as PIN length, the lifetime of the one-time passcodes, max bad PIN/passcode attempts, etc.) </p><p>Click Add</p><p>On the next page, enter the Shared Secret. This is the same secret you entered in NPS above in the second tab of the 'Add Radius Server' step on the NPS. <b>Be sure these match!</b> WiKID support adding radius return attributes at the Network Client level and on a per-user group level, however, that is beyond the scope of this document.</p><p>You will get a notice that the network client has been added. You will need to restart the WiKID server from the command line. This loads the network client into the radius interface and opens the radius ports on the built-in WiKID firewall.</p><pre class="prettyprint"># wikidctl restart</pre><h2>Configuring the SSH Gateway Server</h2><h2>Configure the SSH Gateway</h2><p>Now we will configure the central SSH gateway. This linux box is the gateway/proxy to all the production servers in the farm. It should be locked down tight with no extraneous software or services running on it. It should have an external interface for in-bound connections and an internal interface for internal connections. First, we will configure the gateway box to use WiKID for strong authentication of SSH users.</p><p>Start by installing PAM Radius. The PAM Radius home page is <a href="" rel="nofollow"> here</a>.</p><p>Download the tar file (as of this writing <a href="" rel="nofollow">1.3.17 was the latest)</a>.</p><p>Run:</p><pre class="prettyprint">$ make</pre><p> Copy the resulting shared library to /lib/security.</p><pre class="prettyprint">$ sudo cp /lib/security/ </pre><p> Edit /etc/pam.d/sshd to allow Radius authentication.</p><pre class="prettyprint">$ sudo vi /etc/pam.d/sshd</pre><p> <b>N.B.: Distributions of linux have different pam.d file formats.</b> Please check with your distribution for specific suggestions. These instructions work for Fedora/Redhat/Centos.</p><p>Go to the first line of the file, hit the Insert key or the i key and insert this line.</p><pre class="prettyprint">auth        sufficient     /lib/security/</pre><p> The 'sufficient' tag indicates that if the Radius authentication succeeds then no additional authentication will be required. However, if the Radius authentication fails, a username and password from the system will work. Use 'Required' to require strong authentication.</p><p>Write the file and quit. Hit the Esc key to exit insert mode and type ':wq'</p><p>Edit or create your /etc/raddb/server file.  There is a <a href="" rel="nofollow">sample here</a>.</p><pre class="prettyprint">vi /etc/raddb/server </pre><p>Below the line:</p><pre class="prettyprint">       secret      1</pre><p>Add this line, substituting your routableIPAddress:</p><pre class="prettyprint">routableIPaddress      shared_secret      1</pre><p>The routeableIPaddress is the IP address of your NPS server.</p><p> Edit your /etc/pam.d/sshd file thusly: </p><pre class="prettyprint">#%PAM-1.0 auth sufficient /lib/security/ auth include system-auth account required account include system-auth password include system-auth session include system-auth session required </pre><p>Add your WiKID server to the /etc/raddb/server file, using the internal IP Address of the WiKID server and the shared secret you entered in the Network Client creation page:</p><pre class="prettyprint"># server[:port] shared_secret timeout (s) secret 1 wikidserver_secret 3 </pre><p>Let's add some security to SSH configuration here too. Open your /etc/ssh/sshd_config (not the nearby ssh_config file). Add these configuration options: </p><pre class="prettyprint">#Protocol 2,1 #Check that only protocol 2 is allowed: Protocol 2 #Disallow root login: PermitRootLogin no #Disallow accounts without passwords: PermitEmptyPasswords no </pre><p>If you want to change the port you can. It won't stop an attacker, but it might cut down on log events caused by script kiddies. This gateway box is now set to use WiKID one-time passwords for SSH authentication. All users have to be registered with the WiKID server and no one can login as root. Before we leave this box, we'll do something that is a little bit different - we will have the users create their RSA private key on the gateway. Once each users is signed into the box with WiKID, have them create their keys:</p><p>class="command">ssh-keygen -t rsa</p><p>In my opinion, passphrases for these keys are redundant. They are here only to create a single sign on functionality into the server farm. Obviously, you must be careful to be sure that users do not have access to other keys. </p><h2>Configure the target servers</h2><p>Obviously, we configure these servers to only accept incoming SSH requests from the gateway. We do this by restricting access on port 22 to our internal addresses. Edit /etc/sysconfig/iptables and add or edit the line for SSH on port 22: </p><pre class="prettyprint">-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp -s --dport 22 -j ACCEPT </pre><h2>Running the WiKID Software token</h2><p>Start the WiKID token and select the Domain associated with the SSH Gateway. Then enter the PIN and you will get back the one-time passcode. The OTP is time-bounded, but the time can be set on the WiKID server to whatever you want. </p><figure class="medium"><img src=""/></figure><figure class="medium"><img src=""/></figure><figure class="medium"><img src=""/></figure><p>The user simply enters the one-time passcode when prompted by SSH.</p><p>The token can also be run from the command line, which is quote convenient for SSH:</p><pre class="prettyprint">java -cp jWiKID-3.1.3.jar:jwcl.jar com.wikidsystems.jw.JWcl domainid </pre><p>Were domainid is the 12 digit domain identifier. </p><h2>Conclusion</h2><p>Many organizations are facing increased compliance and regulation. Additionally, environments are becoming more and more heterogenous and the Internet is becoming more and more dangerous. At the same time, users need more access and want to telecommute more, which is good from a disaster recovery perspective. </p><p>While this tutorial has focused on adding two-factor authentication to SSH, an un-stated concept is that you have chosen Radius as a network authentication standard. And it's a good one. Most VPNs, remote desktop systems, web servers and other remote access services support Radius. Now when you want to add a new service with two-factor authentication, all you have to consider is "Does it support Radius?". If it does, then all you need to do is point it to your NPS server. The same holds true if you use Freeradius or some other Radius server.</p><p>For more information on WiKID's two-factor authentication system, <a href="" target="" rel="nofollow">please visit our website</a>.</p> <div class="end-note"> <!-- blx4 #2004 blox4.html --> <div id="" class="blx blxParticleendnote blxM2004 blox4_html blxC30910"><aside> <strong>Next read this</strong> <ul> <li><a href="">The new CISO's playbook: 5 rules to follow</a></li> <li><a href="">7 hot cybersecurity trends (and 4 going cold)</a></li> <li><a href="">Top cyber security certifications: Who they're for, what they cost, and which you need</a></li> <li><a href="">The best password advice right now (Hint: It's not the NIST guidelines)</a></li> <li><a href="">8 cheap or free cybersecurity training resources</a></li> <li><a href="">24 best free security tools</a></li> <li><a href="">8 cheap or free cybersecurity training resources</a></li> <li><a href="">Top cyber security certifications: Who they're for, what they cost, and which you need</a></li> <li><a href="">12 tips for effectively presenting cybersecurity to the board </a></li> </ul> </aside> </div> </div> </div> <script> $("a[data-product-id]").on('click',function(e){ var e = e || window.event; var dataLayer = window.dataLayer = window.dataLayer || []; var prodName = $(this).attr("data-product-name"); prodName = prodName.replace("\\'","'"); // to counter the effects of over-escaping if ( $(this).parents('.slideshow').length > 0 ) { var productLinkPosition = "Slideshow"; } else if ( $(this).parents('.emo-sb').length > 0 ) { var productLinkPosition = "Product Sidebar"; } else if ( $(this).parents('').length > 0 ) { var productLinkPosition = "AAG Top"; } else if ( $(this).parents('.at-a-glance').length > 0 ) { var productLinkPosition = "AAG Bottom"; } else if ( $(this).parents('.quick-hits.medium').length > 0 ) { var productLinkPosition = "Quick Hit Medium"; } else if ( $(this).parents('.quick-hits').length > 0 ) { var productLinkPosition = "Quick Hit Full"; } else if ( $(this).parents('.product-chart').length > 0 ) { var productLinkPosition = "Product Chart"; } else if ( $(this).parents('.emo-list').length > 0 ) { var productLinkPosition = "Product List"; } else { var productLinkPosition = "Body Text"; } dataLayer.push({ 'event': 'affiliateLink', 'eventCategory': 'Affiliate Link', // Hardcoded, not dynamic 'eventAction': 'Click', // Hardcoded, not dynamic 'eventLabel': '''', // The URL the affiliate link leads to 'productVendor': $(this).attr("data-bkvndr"), // The vendor of the product 'productManufacturer': $(this).attr("data-bkmfr"), // The manufacturer of the product 'productName': prodName, // The name of the product 'productId': $(this).attr("data-product-id"), // The ID of the product 'productLinkPosition': productLinkPosition // location of product link (Product Sidebar, Product Chart, etc.) }); }); </script> <script> $("#drr-container a:not('[data-product-id]')").on('click',function(e){ var dataLayer = window.dataLayer = window.dataLayer || []; var $clickEl = $(this); var clickUrl = $clickEl.attr("href"); var clickParentsUntilDrr = $clickEl.parentsUntil("#drr-container"); var clickText = $clickEl.text().trim(); var linkLocation; var linkType; var regex = new RegExp("|||||||||||||", "i"); if (clickParentsUntilDrr.hasClass("fakesidebar")) { linkLocation = "FSB"; } else if (clickParentsUntilDrr.hasClass("end-note")) { linkLocation = "End Note"; } else { linkLocation = "Body"; } if (regex.test(clickUrl)) { linkType = "Internal"; } else { linkType = "External"; } dataLayer.push({ 'event': 'editBodyLink', 'eventCategory': 'Editorial Body Links', 'eventAction': linkType, 'eventLabel': linkLocation+" | "+clickText+" | "+clickUrl }); }); </script> <script> $("#drr-top-ad .related-promo-wrapper .title > a").on('click',function(e){ var dataLayer = window.dataLayer = window.dataLayer || []; var $clickEl = $(this); var clickUrl = $clickEl.attr("href"); var clickText = $clickEl.text().trim(); var linkLocation = "Promo"; dataLayer.push({ 'event': 'promoModuleLink', 'eventCategory': 'Promotion module', 'eventAction': 'Click', 'eventLabel': linkLocation+" | "+clickText+" | "+clickUrl }); }); </script> <div class="apart-alt tags"> <span class="related">Related: </span> <ul> <li><a class="edition-link-url primary-cat-url2" href="/category/access-control"><span class="primary-cat-name2">Access Control</span></a></li> <li><a class="edition-link-url primary-cat-url2" href="/category/identity-management"><span class="primary-cat-name2">Identity Management</span></a></li> <li><a class="edition-link-url primary-cat-url2" href="/category/network-security"><span class="primary-cat-name2">Network Security</span></a></li> <li><a class="edition-link-url primary-cat-url2" href="/category/privacy"><span class="primary-cat-name2">Privacy</span></a></li> <li><a class="edition-link-url primary-cat-url2" href="/category/security"><span class="primary-cat-name2">Security</span></a></li> <li><a class="edition-link-url primary-cat-url2" href="/category/authentication"><span class="primary-cat-name2">Authentication</span></a></li> </ul> </div> <div class="brand-social"> <div class="follow-text">Follow everything from CSO Online</div> <ul class="social-links"> <li> <a href="" class="twitter-follow" target="_blank"> <i class="ss-icon ss-social ss-twitter"></i> </a> </li> <li> <a href="" class="facebook-follow" target="_blank"> <i class="ss-icon ss-social ss-facebook"></i> </a> </li> <li> <a href="" class="linkedin-follow" target="_blank"> <i class="ss-icon ss-social ss-linkedin"></i> </a> </li> <li> <a href="/index.rss" class="rss"> <i class="ss-icon ss-social ss-rss"></i> </a> </li> </ul> </div> <script> //set business unit / publisher: US / global is default var contentCopyright = "IDG Communications, Inc."; var contentEdition = ""; // if there's an article locale ID, use that to get the business unit / publisher for copyright (empty string is global) if ( contentEdition != "" || contentEdition != null ) { // get locale slug of article (contentEdition) contentEdition = ""; if (contentEdition === 'us') { contentCopyright = "IDG Communications, Inc."; } if (contentEdition === 'asean') { contentCopyright = "IDG Communications, Inc."; } if (contentEdition === 'mideast' || contentEdition === 'middle-east') { contentCopyright = ""; } if (contentEdition === 'uk') { contentCopyright = "IDG Communications Ltd."; } if (contentEdition === 'au') { contentCopyright = "IDG Communications. ABN 14 001 592 650"; } if (contentEdition === 'nz') { contentCopyright = "IDG Communications. ABN 14 001 592 650"; } if (contentEdition === 'in') { contentCopyright = "IDG Media Private Ltd"; } } $(document).ready(function() { if (contentCopyright != null && contentCopyright != "") { document.querySelector("span.ccbu").textContent = contentCopyright; } }); </script> <p class="content-copy">Copyright © 2010 <span class="ccbu">IDG Communications, Inc.</span></p> <!-- blx4 #1174 blox4.html --> <div class="article-intercept"> <i class="ss-icon ss-lightbulb"></i> <a href="">Get the best of CSO ... delivered. Sign up for our FREE email newsletters!</a> </div> <script src="/www/js/video/embedder-jwp.js?v=20190822115043"></script> </section><!-- /.bodee --> <script>var suppressEd = false;</script> <script> if (countryCode !== "" && (countryCode === 'in' || countryCode === "africa")) { suppressEd = true; $("div#content-recommender").remove(); } </script> <div id="content-recommender"> <div class="OUTBRAIN" data-widget-id="AR_1" data-ob-template="CSOOnline"></div>   </div> <script> var obEdition = edition || countryCode || ''; var widgetId=""; if (obEdition === "asean") { widgetId="AR_3"; if ($("body").hasClass("slideshow")) { widgetId="AR_4"; } } if (widgetId !== "") { $(".OUTBRAIN").attr("data-widget-id", widgetId); } // suppressed for certain editions if (!suppressEd) { $.getScript("//"); } </script> <div class="lazyload_ad"> <code type="text/javascript"> <!-- var slotName = 'bottomleaderboard'; var slotSize = []; if ($thm.deviceClass == 'mobile') { slotSize = [[300,50],[320,50],[300,250]]; } else if ($thm.deviceClass == 'tablet') { slotSize = [[728,90],[468,60]]; } else { slotSize = [[728,90],[970,90],[970,250]]; } IDG.GPT.addDisplayedAd(slotName, "true"); document.write('<div id="' + slotName + '" class="ad-container">');{ IDG.GPT.defineGoogleTagSlot(slotName, slotSize, false, true);}); document.write('</div>');{ $('#' + slotName).responsiveAd({screenSize:'971 1115', scriptTags: []}, true);}); //--> </code> </div> <link rel="stylesheet" href="/www.idge/css/sponsored-links.css?v=20190822115043" /> <link rel="stylesheet" href="/www.idge.cso/css/sponsored-links.css?v=20190822115043" /> <section id="resources-sponsored-links"> <section class="sponsored-links"> <div class="head">Sponsored Links</div> <ul class="first"> <li> <a rel="nofollow" target="_blank" href="" > dtSearch® instantly searches terabytes of files, emails, databases, web data. See site for hundreds of reviews; enterprise & developer evaluations </a> </li> </ul><ul> <li> <a rel="nofollow" target="_blank" href="" > It’s a Race to Connect the Globe. 451 Research and Iron Mountain explore this Hybrid IT trend. </a> </li> </ul> </section> </section> </article> </section><!-- /role=main --> </div><!-- /#page-wrapper --> <link rel="stylesheet" href="/www.idge/css/foot.css?v=20190822115043" /> <link rel="stylesheet" href="/www.idge.cso/css/foot.css?v=20190822115043" /> <footer> <section class="brand" itemscope itemtype=""> <link itemprop="url" href=""> <a href="/"><span class="logo">CSO Online</span></a> <span class="tagline"> CSO provides news, analysis and research on security and risk management</span> <span class="follow"> <label>Follow us</label> <ul> <li><a class="social-media-tw-foot" href="" itemprop="sameAs" rel="nofollow" target="_blank" onclick="brandFollowTrack('Twitter')"><i class="ss-icon ss-social-circle ss-twitter"></i></a></li> <li class="lnkdn"><a class="social-media-li-foot" href="" itemprop="sameAs" rel="nofollow" target="_blank" onclick="brandFollowTrack('LinkedIn')"><i class="ss-icon ss-social-circle brand ss-linkedin"></i></a></li> <li><a class="social-media-fb-foot" href="" itemprop="sameAs" rel="nofollow" target="_blank" onclick="brandFollowTrack('Facebook')"><i class="ss-icon ss-social-circle brand ss-facebook"></i></a></li> <li><a href="/index.rss" target="_blank"><i class="ss-icon ss-social-circle ss-rss"></i></a></li> <script> if (typeof facebookUrl !== "undefined" && facebookUrl !== null && facebookUrl !== "") { document.querySelector(".social-media-fb-foot").setAttribute("href", facebookUrl); } if (typeof twitterUrl !== "undefined" && twitterUrl !== null && twitterUrl != "") { document.querySelector(".social-media-tw-foot").setAttribute("href", twitterUrl); } if (typeof youTubeUrl !== "undefined" && youTubeUrl !== null && youTubeUrl !== "") { if (document.querySelector(".social-media-yt-foot") !== null) { document.querySelector(".social-media-yt-foot").setAttribute("href", youTubeUrl); } else { if (typeof countryCode !== 'undefined' && countryCode === "in") { youTubeUrl = ""; } $( "<li class='ytbe'><a class='social-media-yt-foot' href='"+youTubeUrl+"' rel='nofollow' target='_blank'><i class='ss-icon ss-social-circle brand ss-youtube'></i></a></li>" ).insertAfter("li.lnkdn"); } } if (document.querySelector(".social-media-li-foot") != null) { if (typeof linkedInUrl !== "undefined" && linkedInUrl !== null && linkedInUrl !== "") { document.querySelector(".social-media-li-foot").setAttribute("href", linkedInUrl); } else { $("li.lnkdn").hide(); } } </script> </ul> </span> </section> <section class="topics"> <nav id="ft1"> <ul> <li class="selected"><a class="edition-link-url" href="/category/access-control/" class="active">Access Control</a> </li> <li><a class="edition-link-url" href="/category/analytics/">Analytics</a> </li> <li><a class="edition-link-url" href="/category/business-continuity/">Business Continuity</a> </li> <li><a class="edition-link-url" href="/category/careers/">Careers</a> </li> <li><a class="edition-link-url" href="/category/compliance/">Compliance</a> </li> <li><a class="edition-link-url" href="/category/data-loss-prevention/">Data Loss Prevention</a> </li> <li><a class="edition-link-url" href="/category/hacking/">Hacking</a> </li> <li><a class="edition-link-url" href="/category/it-leadership/">IT Leadership</a> </li> <li><a class="edition-link-url" href="/category/malware/">Malware</a> </li> <li><a class="edition-link-url" href="/category/physical-security/">Physical Security</a> </li> <li><a class="edition-link-url" href="/category/security/">Security</a> </li> <li><a class="edition-link-url" href="/category/social-engineering/">Social Engineering</a> </li> </ul> </nav> <nav id="ft2"> <ul> <li><a class="edition-link-url" href="/howto">How-Tos</a></li> <li><a class="edition-link-url" href="/feature">Features</a></li> <li><a class="edition-link-url" href="/news">News</a></li> <li><a class="edition-link-url" href="/blogs">Blogs</a></li> <li><a class="edition-link-url" href="/resources">Resources</a></li> <li><a class="edition-link-url" href="/newsletters/signup.html">Newsletters</a></li> </ul> </nav> </section> <section class="about"> <div class="wrapper"> <nav class="tertiary" id="ft3"> <ul> <li><a class="edition-link-url" href="/about/about.html" >About Us</a> <li><a class="edition-link-url" href="/about/contactus.html" >Contact</a> <li><a class="edition-link-url" href="/about/privacy.html" >Privacy Policy</a> <li><a class="edition-link-url" href="/about/cookie-policy.html" >Cookie Policy</a> <li><a class="edition-link-url" href="/about/member-preferences.html" >Member Preferences</a> <li><a class="edition-link-url" href="" target="_blank" rel="nofollow" >Advertising</a> <li><a class="edition-link-url" href="" target="_blank" rel="nofollow" >IDG Careers</a> <li><a class="edition-link-url" href="/about/adchoices.html" >Ad Choices</a> <li><a class="edition-link-url" href="/about/affiliates.html" >E-commerce Links</a> </ul> </nav> </div> </section> <section class="copyright"> <div class="wrapper"> <p><a href="/about/tos.html">Copyright</a> © 2019 <span class="bu">IDG Communications, Inc.</span></p> <div class="network"> <div id="network-selector"> <div class="label">Explore the IDG Network <i class="ss-icon tick">descend</i></div> <ul> <li><a href="" target="_blank" rel="nofollow">CIO</a></li> <li><a href="" target="_blank" rel="nofollow">Computerworld</a></li> <li><a href="" target="_blank" rel="nofollow">CSO</a></li> <li><a href="" target="_blank" rel="nofollow">IDC</a></li> <li><a href="" target="_blank" rel="nofollow">IDG</a></li> <li><a href="" target="_blank" rel="nofollow">IDG Connect</a></li> <li><a href="" target="_blank" rel="nofollow">InfoWorld</a></li> <li><a href="" target="_blank" rel="nofollow">Insider Pro</a></li> <li><a href="" target="_blank" rel="nofollow">ITwhitepapers</a></li> <li><a href="" target="_blank" rel="nofollow">JavaWorld</a></li> <li><a href="" target="_blank" rel="nofollow">Network World</a></li> </ul> </div><!-- /#network-selector --> </div><!-- /.network --> </div><!-- /.wrapper --> </section> </footer> <script src="/www/js/jquery/jquery-ui.js"></script> <script src="/www/js/jquery/jquery.dfp.min.js"></script> <script src="/www.idge/js/mule/shortstack_nav.js"></script> <div id="mobilewelcomead" class=" ad-container test"> </div> <script type="text/javascript">{ IDG.GPT.addDisplayedAd("mobilewelcomead", "true"); $('#mobilewelcomead').responsiveAd({screenSize:'971 1115', scriptTags: []}); IDG.GPT.log("Creating ad: mobilewelcomead - [971 1115]"); }); </script> <script src="/www/js/analytics/tracking.js"></script> <script src="/www/js/autocomplete.js"></script> <script src="/www.idge/js/social_sidecar.js"></script> <script src="/www.idge/js/jquery/plugins/jquery.colorbox-min.js"></script> <script src="/www.idge/js/article.js?v=20190822115043"></script> <script src="/www/js/prettify.js"></script> <script src="/www.idge/js/jquery/responsive-tables.js"></script> <script src="/www.idge/js/jquery/jquery.tablesorter.min.js"></script> <script> $(document).ready(function() { $("table.tablesorter").tablesorter({ widgets: ['zebra'] }); $("table.tablesorter tbody tr").hover(function() { $(this).toggleClass("selected"); }); $("table.tablesorter thead tr th").each(function(){ if ($(this).find('.ss-icon').length < 1) { $(this).append('<i class="ss-icon"></i>'); } }); }); </script> <script src="/www/js/jquery/jquery.lazyload.min.js"></script> <script src="/www.idge/js/global.js?v=20190822115043"></script> <script src="/www/js/webfonts/ss-social.js"></script> <script src="/www/js/webfonts/ss-standard.js"></script> <script src="/www/js/analytics/brandAnalytics.js"></script> <script src="/www/js/locales-editions-slug.js?v=20190822115043"></script> <div id="catfish" class=" ad-container test"> </div> <script type="text/javascript">{ IDG.GPT.addDisplayedAd("catfish", "true"); $('#catfish').responsiveAd({screenSize:'971 1115', scriptTags: []}); IDG.GPT.log("Creating ad: catfish - [971 1115]"); }); </script> <!-- Begin gpt-skin --> <div id="gpt-skin" class=" ad-container"> </div> <script type="text/javascript">{ IDG.GPT.addDisplayedAd("gpt-skin", "true"); IDG.GPT.displayGoogleTagSlot('gpt-skin'); }); </script> <!-- End gpt-skin --> <script type="text/javascript" src="/www/js/ads/jquery.lazyload-ad.js"></script> <script type="text/javascript"> // -- Load Lazy Advertisement placement as deferred $("div.lazyload_ad").lazyLoadAd({ threshold : 0, // You can set threshold on how close to the edge ad should come before it is loaded. Default is 0 (when it is visible). forceLoad : false, // Ad is loaded even if not visible. Default is false. onLoad : false, // Callback function on call ad loading onComplete : false, // Callback function when load is loaded timeout : 1500, // Timeout ad load debug : false, // For debug use : draw colors border depends on load status xray : false // For debug use : display a complete page view with ad placements }) ; </script> <script type='text/javascript'> var ocHead = $("#oc-head").outerHeight(); var relatedContentHeight = $(".related-content-wrapper").outerHeight(); relatedTop = ocHead; $(document).ready(function() { function debounce(func, wait, immediate){ var timeout; return function(){ var context = this, args = arguments; var later = function(){ timeout = null; if (!immediate) func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } var lastScrollTop = 0; if (window.addEventListener) { window.addEventListener('scroll', debounce(function(e){ var containerOffset = $("#drr-container p:last").offset().top; var containerHeight = $("#drr-container p:last").outerHeight(); var windowHeight = $(window).height(); var windowScrolltop = $(this).scrollTop(); if (windowScrolltop > lastScrollTop) { // user is scrolling DOWN: hide ribbon $(".related-content-wrapper").css('top','-'+relatedTop+'px'); if (windowScrolltop > (containerOffset+containerHeight-windowHeight)) { // user is scrolling down and sees last paragraph: show ribbon $(".related-content-wrapper").css('top',ocHead+'px'); } } else { // user is scrolling up, show ribbon if #oc-head has class stick... and does not have no-stick-important (sticky leaderboard) //if ($('#oc-head').hasClass('stick')) { if (windowScrolltop > (ocHeadTop + 100) && !($('#oc-head').hasClass('no-stick-important'))) { $(".related-content-wrapper").css('top',ocHead+'px'); } else { $(".related-content-wrapper").css('top','-'+relatedTop+'px'); } } lastScrollTop = windowScrolltop; }, 5)); // run maximum of one time every 5ms } }); $('#resources-sponsored-links').addClass('show'); </script> <!-- Begin comScore Tag --> <script> var _comscore = _comscore || []; _comscore.push( { c1: "2", c2: "6035308", c3: "", c5: "Access Control", c6: "Article: Tip", c15: "2125087" }); (function() { var s = document.createElement("script"), el = document.getElementsByTagName("script")[0]; s.async = true; s.src = (document.location.protocol == "https:" ? "https://sb" : "http://b") + ""; el.parentNode.insertBefore(s, el); })(); </script> <noscript> <img src="" /> </noscript> <!-- End comScore Tag --> <!-- LiveRamp Sources --> <script src="/www/js/utils/liveRamp.js"></script> <!-- End of LiveRamp --> <script type="text/javascript"> consent.digitalelement.queue.push(function(){ $thm.logPlEvent({"b":"cso","e":"view","t":"article","id":"2125087"}); }); </script> <div id="loginModal"></div> <div id="logoffModal"></div> <script type="text/javascript"> a=top;f=self; var acceptedDomains = [""]; var cleanedHost = a.location.hostname.split(".").slice(-2).join("."); if(a!=f && acceptedDomains.indexOf(cleanedHost) < 0) { a.location=f.location; } </script> </body> </html>