diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/404.html b/404.html new file mode 100644 index 00000000..a87f141f --- /dev/null +++ b/404.html @@ -0,0 +1,212 @@ + + + + + + + + + + + Linux Guide and Hints + + + + + + + + + + + + + + + +
+
+ +
+
+

404

+

Page not found

+
+
+ + +
+
+ + + + + + + + + + + + diff --git a/CNAME b/CNAME new file mode 100644 index 00000000..8d0ac555 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +linuxguideandhints.com diff --git a/archive/index.html b/archive/index.html new file mode 100644 index 00000000..1a9dfa19 --- /dev/null +++ b/archive/index.html @@ -0,0 +1,232 @@ + + + + + + + + + + + Archives - Linux Guide and Hints + + + + + + + + + + + + + + + +
+
+
+
+ +

This section is for archives. Most of these pages were created by one of our late +contributors and have not been updated since his passing. As such, we have moved +most of them here as clean up.

+
+
+ + + + + + + + + + + + diff --git a/assets/grub_ex.png b/assets/grub_ex.png new file mode 100644 index 00000000..c83381e9 Binary files /dev/null and b/assets/grub_ex.png differ diff --git a/assets/grub_ex_fedora.png b/assets/grub_ex_fedora.png new file mode 100644 index 00000000..db286797 Binary files /dev/null and b/assets/grub_ex_fedora.png differ diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 00000000..f48d5323 Binary files /dev/null and b/assets/logo.png differ diff --git a/assets/proton.png b/assets/proton.png new file mode 100644 index 00000000..9a2252b2 Binary files /dev/null and b/assets/proton.png differ diff --git a/assets/silent_hill.png b/assets/silent_hill.png new file mode 100644 index 00000000..b55ccf09 Binary files /dev/null and b/assets/silent_hill.png differ diff --git a/assets/silent_hill_16.png b/assets/silent_hill_16.png new file mode 100644 index 00000000..5bef3e45 Binary files /dev/null and b/assets/silent_hill_16.png differ diff --git a/assets/silent_hill_32.png b/assets/silent_hill_32.png new file mode 100644 index 00000000..5aa5abfd Binary files /dev/null and b/assets/silent_hill_32.png differ diff --git a/assets/statusline.png b/assets/statusline.png new file mode 100644 index 00000000..cf07125e Binary files /dev/null and b/assets/statusline.png differ diff --git a/css/base.css b/css/base.css new file mode 100644 index 00000000..5ea2fd06 --- /dev/null +++ b/css/base.css @@ -0,0 +1,270 @@ +html { + /* The nav header is 4.40625rem high, plus 20px for the margin-top of the + main container. */ + scroll-padding-top: calc(4.40625rem + 20px); +} + +body > .container { + margin-top: 20px; +} + +.navbar.fixed-top { + position: -webkit-sticky; + position: sticky; +} + +.source-links { + float: right; +} + +.col-md-9 img { + max-width: 100%; +} + +pre, code { + background: #444; + color: #e0e2e4; + border: 1px solid rgba(0,0,0,0.125); + border-radius: 0.25rem; +} + +pre { + padding: 0.5em; +} + +code { + padding: 1px 3px; +} + +pre code { + border: none; + /* Override styles from hljs theme */ + background: transparent !important; + padding: 0 !important; +} + +a code { + color: #00bc8c; +} + +a:hover code { + color: #007053; +} + +kbd { + padding: 2px 4px; + font-size: 90%; + color: #fff; + background-color: #333; + border-radius: 3px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); + box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); +} + +footer { + margin-top: 30px; + margin-bottom: 10px; + text-align: center; + font-weight: 200; +} + +.modal-dialog { + margin-top: 60px; +} + +/* Style the admonitions. */ + +.admonition { + margin-bottom: 1rem; + border: 1px solid rgba(0, 0, 0, 0.125); + border-radius: 0.25rem; + color: #fff; +} + +.admonition > .admonition-title { + margin: 0; + padding: 0.75rem 1rem; + background: #444; + border-bottom: 1px solid rgba(0, 0, 0, 0.125); + border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0; + font-size: 1rem; +} + +.admonition > * { + margin-left: 1rem; + margin-right: 1rem; +} + +.admonition > *:not(.admonition-title):first-of-type, +.admonition > .admonition-title + * { + margin-top: 1rem; +} + +.admonition > *:last-child { + margin-bottom: 1rem; +} + +/* Style each kind of admonition. */ + +.admonition.note, .admonition.note > .admonition-title { + border-color: #304f6f; +} + +.admonition.note > .admonition-title { + background: #375a7f; +} + +.admonition.warning, .admonition.warning > .admonition-title { + border-color: #d58910; +} + +.admonition.warning > .admonition-title { + background: #f39c12; +} + +.admonition.danger, .admonition.danger > .admonition-title { + border-color: #ca4335; +} + +.admonition.danger > .admonition-title { + background: #e74c3c; +} + +/* + * Side navigation + * + * Scrollspy and affixed enhanced navigation to highlight sections and secondary + * sections of docs content. + */ + +.bs-sidebar.affix { + position: -webkit-sticky; + position: sticky; + /* The nav header is 4.40625rem high, plus 20px for the margin-top of the + main container. */ + top: calc(4.40625rem + 20px); +} + +.bs-sidebar.card { + padding: 0; +} + +/* Toggle (vertically flip) sidebar collapse icon */ +.bs-sidebar .navbar-toggler span { + -moz-transform: scale(1, -1); + -webkit-transform: scale(1, -1); + -o-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} + +.bs-sidebar .navbar-toggler.collapsed span { + -moz-transform: scale(1, 1); + -webkit-transform: scale(1, 1); + -o-transform: scale(1, 1); + -ms-transform: scale(1, 1); + transform: scale(1, 1); +} + +/* First level of nav */ +.bs-sidebar > .navbar-collapse > .nav { + padding-top: 10px; + padding-bottom: 10px; + border-radius: 5px; + width: 100%; +} + +/* All levels of nav */ +.bs-sidebar .nav > li > a { + display: block; + padding: 5px 20px; + z-index: 1; +} +.bs-sidebar .nav > li > a:hover, +.bs-sidebar .nav > li > a:focus { + text-decoration: none; + border-right: 1px solid; +} +.bs-sidebar .nav > li > a.active, +.bs-sidebar .nav > li > a.active:hover, +.bs-sidebar .nav > li > a.active:focus { + font-weight: bold; + background-color: transparent; + border-right: 1px solid; +} + +.bs-sidebar .nav .nav .nav { + margin-left: 1em; +} + +.bs-sidebar .nav > li > a { + font-weight: bold; +} + +.bs-sidebar .nav .nav > li > a { + font-weight: normal; +} + +.headerlink { + display: none; + padding-left: .5em; +} + +h1:hover .headerlink, h2:hover .headerlink, h3:hover .headerlink, h4:hover .headerlink, h5:hover .headerlink, h6:hover .headerlink{ + display:inline-block; +} + +@media (max-width: 991.98px) { + .navbar-collapse.show { + overflow-y: auto; + max-height: calc(100vh - 4.40625rem); + } +} + +.dropdown-item.open { + color: #fff; + background-color: #375a7f; +} + +.dropdown-submenu > .dropdown-menu { + margin: 0 0 0 1.5rem; + padding: 0; + border-width: 0; +} + +.dropdown-submenu > a::after { + display: block; + content: " "; + float: right; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + border-width: 5px 0 5px 5px; + border-left-color: #ccc; + margin-top: 5px; + margin-right: -10px; +} + +.dropdown-submenu:hover > a::after { + border-left-color: #404040; +} + +@media (min-width: 992px) { + .dropdown-menu { + overflow-y: auto; + max-height: calc(100vh - 4.40625rem); + } + + .dropdown-submenu { + position: relative; + } + + .dropdown-submenu > .dropdown-menu { + position: fixed !important; + margin-top: -9px; + margin-left: -2px; + border-width: 1px; + padding: 0.5rem 0; + } +} diff --git a/css/bootstrap.min.css b/css/bootstrap.min.css new file mode 100644 index 00000000..287e4b7e --- /dev/null +++ b/css/bootstrap.min.css @@ -0,0 +1,12 @@ +/*! + * Bootswatch v4.4.1 + * Homepage: https://bootswatch.com + * Copyright 2012-2019 Thomas Park + * Licensed under MIT + * Based on Bootstrap +*//*! + * Bootstrap v4.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */@import url("https://fonts.googleapis.com/css?family=Lato:400,700,400italic");:root{--blue: #375a7f;--indigo: #6610f2;--purple: #6f42c1;--pink: #e83e8c;--red: #E74C3C;--orange: #fd7e14;--yellow: #F39C12;--green: #00bc8c;--teal: #20c997;--cyan: #3498DB;--white: #fff;--gray: #999;--gray-dark: #303030;--primary: #375a7f;--secondary: #444;--success: #00bc8c;--info: #3498DB;--warning: #F39C12;--danger: #E74C3C;--light: #999;--dark: #303030;--breakpoint-xs: 0;--breakpoint-sm: 576px;--breakpoint-md: 768px;--breakpoint-lg: 992px;--breakpoint-xl: 1200px;--font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";--font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}*,*::before,*::after{-webkit-box-sizing:border-box;box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:"Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";font-size:0.9375rem;font-weight:400;line-height:1.5;color:#fff;text-align:left;background-color:#222}[tabindex="-1"]:focus:not(:focus-visible){outline:0 !important}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:0.5rem}p{margin-top:0;margin-bottom:1rem}abbr[title],abbr[data-original-title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#00bc8c;text-decoration:none;background-color:transparent}a:hover{color:#007053;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}pre,code,kbd,samp{font-family:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:0.75rem;padding-bottom:0.75rem;color:#999;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:0.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button:not(:disabled),[type="button"]:not(:disabled),[type="reset"]:not(:disabled),[type="submit"]:not(:disabled){cursor:pointer}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{padding:0;border-style:none}input[type="radio"],input[type="checkbox"]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="date"],input[type="time"],input[type="datetime-local"],input[type="month"]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{outline-offset:-2px;-webkit-appearance:none}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none !important}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{margin-bottom:0.5rem;font-weight:500;line-height:1.2}h1,.h1{font-size:3rem}h2,.h2{font-size:2.5rem}h3,.h3{font-size:2rem}h4,.h4{font-size:1.40625rem}h5,.h5{font-size:1.171875rem}h6,.h6{font-size:0.9375rem}.lead{font-size:1.171875rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,0.1)}small,.small{font-size:80%;font-weight:400}mark,.mark{padding:0.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:0.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.171875rem}.blockquote-footer{display:block;font-size:80%;color:#999}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:0.25rem;background-color:#222;border:1px solid #dee2e6;border-radius:0.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:0.5rem;line-height:1}.figure-caption{font-size:90%;color:#999}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:0.2rem 0.4rem;font-size:87.5%;color:#fff;background-color:#222;border-radius:0.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:inherit}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width: 576px){.container{max-width:540px}}@media (min-width: 768px){.container{max-width:720px}}@media (min-width: 992px){.container{max-width:960px}}@media (min-width: 1200px){.container{max-width:1140px}}.container-fluid,.container-sm,.container-md,.container-lg,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width: 576px){.container,.container-sm{max-width:540px}}@media (min-width: 768px){.container,.container-sm,.container-md{max-width:720px}}@media (min-width: 992px){.container,.container-sm,.container-md,.container-lg{max-width:960px}}@media (min-width: 1200px){.container,.container-sm,.container-md,.container-lg,.container-xl{max-width:1140px}}.row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*="col-"]{padding-right:0;padding-left:0}.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11,.col-12,.col,.col-auto,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm,.col-sm-auto,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-md,.col-md-auto,.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg,.col-lg-auto,.col-xl-1,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-1{margin-left:8.3333333333%}.offset-2{margin-left:16.6666666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.3333333333%}.offset-5{margin-left:41.6666666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.3333333333%}.offset-8{margin-left:66.6666666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.3333333333%}.offset-11{margin-left:91.6666666667%}@media (min-width: 576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-sm-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-sm-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-sm-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-sm-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-sm-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-sm-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-sm-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-sm-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-sm-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-sm-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-sm-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-sm-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-sm-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-sm-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-sm-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-sm-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-sm-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-sm-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-sm-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-sm-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-sm-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-sm-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-sm-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-sm-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-sm-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.3333333333%}.offset-sm-2{margin-left:16.6666666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.3333333333%}.offset-sm-5{margin-left:41.6666666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.3333333333%}.offset-sm-8{margin-left:66.6666666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.3333333333%}.offset-sm-11{margin-left:91.6666666667%}}@media (min-width: 768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-md-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-md-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-md-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-md-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-md-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-md-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-md-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-md-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-md-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-md-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-md-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-md-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-md-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-md-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-md-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-md-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-md-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-md-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-md-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-md-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-md-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-md-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-md-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-md-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-md-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.3333333333%}.offset-md-2{margin-left:16.6666666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.3333333333%}.offset-md-5{margin-left:41.6666666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.3333333333%}.offset-md-8{margin-left:66.6666666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.3333333333%}.offset-md-11{margin-left:91.6666666667%}}@media (min-width: 992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-lg-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-lg-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-lg-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-lg-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-lg-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-lg-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-lg-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-lg-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-lg-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-lg-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-lg-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-lg-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-lg-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-lg-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-lg-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-lg-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-lg-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-lg-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-lg-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-lg-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-lg-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-lg-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-lg-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-lg-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-lg-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.3333333333%}.offset-lg-2{margin-left:16.6666666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.3333333333%}.offset-lg-5{margin-left:41.6666666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.3333333333%}.offset-lg-8{margin-left:66.6666666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.3333333333%}.offset-lg-11{margin-left:91.6666666667%}}@media (min-width: 1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-xl-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-xl-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-xl-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-xl-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-xl-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-xl-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-xl-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-xl-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-xl-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-xl-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-xl-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-xl-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-xl-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-xl-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-xl-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-xl-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-xl-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-xl-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-xl-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-xl-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-xl-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-xl-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-xl-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-xl-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-xl-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.3333333333%}.offset-xl-2{margin-left:16.6666666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.3333333333%}.offset-xl-5{margin-left:41.6666666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.3333333333%}.offset-xl-8{margin-left:66.6666666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.3333333333%}.offset-xl-11{margin-left:91.6666666667%}}.table{width:100%;margin-bottom:1rem;color:#fff}.table th,.table td{padding:0.75rem;vertical-align:top;border-top:1px solid #444}.table thead th{vertical-align:bottom;border-bottom:2px solid #444}.table tbody+tbody{border-top:2px solid #444}.table-sm th,.table-sm td{padding:0.3rem}.table-bordered{border:1px solid #444}.table-bordered th,.table-bordered td{border:1px solid #444}.table-bordered thead th,.table-bordered thead td{border-bottom-width:2px}.table-borderless th,.table-borderless td,.table-borderless thead th,.table-borderless tbody+tbody{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:#303030}.table-hover tbody tr:hover{color:#fff;background-color:rgba(0,0,0,0.075)}.table-primary,.table-primary>th,.table-primary>td{background-color:#c7d1db}.table-primary th,.table-primary td,.table-primary thead th,.table-primary tbody+tbody{border-color:#97a9bc}.table-hover .table-primary:hover{background-color:#b7c4d1}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#b7c4d1}.table-secondary,.table-secondary>th,.table-secondary>td{background-color:#cbcbcb}.table-secondary th,.table-secondary td,.table-secondary thead th,.table-secondary tbody+tbody{border-color:#9e9e9e}.table-hover .table-secondary:hover{background-color:#bebebe}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#bebebe}.table-success,.table-success>th,.table-success>td{background-color:#b8ecdf}.table-success th,.table-success td,.table-success thead th,.table-success tbody+tbody{border-color:#7adcc3}.table-hover .table-success:hover{background-color:#a4e7d6}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#a4e7d6}.table-info,.table-info>th,.table-info>td{background-color:#c6e2f5}.table-info th,.table-info td,.table-info thead th,.table-info tbody+tbody{border-color:#95c9ec}.table-hover .table-info:hover{background-color:#b0d7f1}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#b0d7f1}.table-warning,.table-warning>th,.table-warning>td{background-color:#fce3bd}.table-warning th,.table-warning td,.table-warning thead th,.table-warning tbody+tbody{border-color:#f9cc84}.table-hover .table-warning:hover{background-color:#fbd9a5}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#fbd9a5}.table-danger,.table-danger>th,.table-danger>td{background-color:#f8cdc8}.table-danger th,.table-danger td,.table-danger thead th,.table-danger tbody+tbody{border-color:#f3a29a}.table-hover .table-danger:hover{background-color:#f5b8b1}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f5b8b1}.table-light,.table-light>th,.table-light>td{background-color:#e2e2e2}.table-light th,.table-light td,.table-light thead th,.table-light tbody+tbody{border-color:#cacaca}.table-hover .table-light:hover{background-color:#d5d5d5}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#d5d5d5}.table-dark,.table-dark>th,.table-dark>td{background-color:#c5c5c5}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody+tbody{border-color:#939393}.table-hover .table-dark:hover{background-color:#b8b8b8}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b8b8b8}.table-active,.table-active>th,.table-active>td{background-color:rgba(0,0,0,0.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,0.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,0.075)}.table .thead-dark th{color:#fff;background-color:#303030;border-color:#434343}.table .thead-light th{color:#444;background-color:#ebebeb;border-color:#444}.table-dark{color:#fff;background-color:#303030}.table-dark th,.table-dark td,.table-dark thead th{border-color:#434343}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,0.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,0.075)}@media (max-width: 575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width: 767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width: 991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width: 1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + 0.75rem + 2px);padding:0.375rem 0.75rem;font-size:0.9375rem;font-weight:400;line-height:1.5;color:#444;background-color:#fff;background-clip:padding-box;border:1px solid #222;border-radius:0.25rem;-webkit-transition:border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control{-webkit-transition:none;transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #444}.form-control:focus{color:#444;background-color:#fff;border-color:#739ac2;outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.form-control::-webkit-input-placeholder{color:#999;opacity:1}.form-control::-ms-input-placeholder{color:#999;opacity:1}.form-control::placeholder{color:#999;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#ebebeb;opacity:1}select.form-control:focus::-ms-value{color:#444;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(0.375rem + 1px);padding-bottom:calc(0.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(0.5rem + 1px);padding-bottom:calc(0.5rem + 1px);font-size:1.171875rem;line-height:1.5}.col-form-label-sm{padding-top:calc(0.25rem + 1px);padding-bottom:calc(0.25rem + 1px);font-size:0.8203125rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:0.375rem 0;margin-bottom:0;font-size:0.9375rem;line-height:1.5;color:#fff;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + 0.5rem + 2px);padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5;border-radius:0.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:0.5rem 1rem;font-size:1.171875rem;line-height:1.5;border-radius:0.3rem}select.form-control[size],select.form-control[multiple]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:0.25rem}.form-row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*="col-"]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:0.3rem;margin-left:-1.25rem}.form-check-input[disabled] ~ .form-check-label,.form-check-input:disabled ~ .form-check-label{color:#999}.form-check-label{margin-bottom:0}.form-check-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:0.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:0.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:0.25rem;font-size:80%;color:#00bc8c}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:0.25rem 0.5rem;margin-top:.1rem;font-size:0.8203125rem;line-height:1.5;color:#fff;background-color:rgba(0,188,140,0.9);border-radius:0.25rem}.was-validated :valid ~ .valid-feedback,.was-validated :valid ~ .valid-tooltip,.is-valid ~ .valid-feedback,.is-valid ~ .valid-tooltip{display:block}.was-validated .form-control:valid,.form-control.is-valid{border-color:#00bc8c;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2300bc8c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:valid:focus,.form-control.is-valid:focus{border-color:#00bc8c;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .custom-select:valid,.custom-select.is-valid{border-color:#00bc8c;padding-right:calc(0.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23303030' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2300bc8c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .custom-select:valid:focus,.custom-select.is-valid:focus{border-color:#00bc8c;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.was-validated .form-check-input:valid ~ .form-check-label,.form-check-input.is-valid ~ .form-check-label{color:#00bc8c}.was-validated .form-check-input:valid ~ .valid-feedback,.was-validated .form-check-input:valid ~ .valid-tooltip,.form-check-input.is-valid ~ .valid-feedback,.form-check-input.is-valid ~ .valid-tooltip{display:block}.was-validated .custom-control-input:valid ~ .custom-control-label,.custom-control-input.is-valid ~ .custom-control-label{color:#00bc8c}.was-validated .custom-control-input:valid ~ .custom-control-label::before,.custom-control-input.is-valid ~ .custom-control-label::before{border-color:#00bc8c}.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before,.custom-control-input.is-valid:checked ~ .custom-control-label::before{border-color:#00efb2;background-color:#00efb2}.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before,.custom-control-input.is-valid:focus ~ .custom-control-label::before{-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before,.custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before{border-color:#00bc8c}.was-validated .custom-file-input:valid ~ .custom-file-label,.custom-file-input.is-valid ~ .custom-file-label{border-color:#00bc8c}.was-validated .custom-file-input:valid:focus ~ .custom-file-label,.custom-file-input.is-valid:focus ~ .custom-file-label{border-color:#00bc8c;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.invalid-feedback{display:none;width:100%;margin-top:0.25rem;font-size:80%;color:#E74C3C}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:0.25rem 0.5rem;margin-top:.1rem;font-size:0.8203125rem;line-height:1.5;color:#fff;background-color:rgba(231,76,60,0.9);border-radius:0.25rem}.was-validated :invalid ~ .invalid-feedback,.was-validated :invalid ~ .invalid-tooltip,.is-invalid ~ .invalid-feedback,.is-invalid ~ .invalid-tooltip{display:block}.was-validated .form-control:invalid,.form-control.is-invalid{border-color:#E74C3C;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23E74C3C' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23E74C3C' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus{border-color:#E74C3C;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .custom-select:invalid,.custom-select.is-invalid{border-color:#E74C3C;padding-right:calc(0.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23303030' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23E74C3C' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23E74C3C' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .custom-select:invalid:focus,.custom-select.is-invalid:focus{border-color:#E74C3C;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.was-validated .form-check-input:invalid ~ .form-check-label,.form-check-input.is-invalid ~ .form-check-label{color:#E74C3C}.was-validated .form-check-input:invalid ~ .invalid-feedback,.was-validated .form-check-input:invalid ~ .invalid-tooltip,.form-check-input.is-invalid ~ .invalid-feedback,.form-check-input.is-invalid ~ .invalid-tooltip{display:block}.was-validated .custom-control-input:invalid ~ .custom-control-label,.custom-control-input.is-invalid ~ .custom-control-label{color:#E74C3C}.was-validated .custom-control-input:invalid ~ .custom-control-label::before,.custom-control-input.is-invalid ~ .custom-control-label::before{border-color:#E74C3C}.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before,.custom-control-input.is-invalid:checked ~ .custom-control-label::before{border-color:#ed7669;background-color:#ed7669}.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before,.custom-control-input.is-invalid:focus ~ .custom-control-label::before{-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before,.custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before{border-color:#E74C3C}.was-validated .custom-file-input:invalid ~ .custom-file-label,.custom-file-input.is-invalid ~ .custom-file-label{border-color:#E74C3C}.was-validated .custom-file-input:invalid:focus ~ .custom-file-label,.custom-file-input.is-invalid:focus ~ .custom-file-label{border-color:#E74C3C;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.form-inline{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width: 576px){.form-inline label{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .input-group,.form-inline .custom-select{width:auto}.form-inline .form-check{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:0.25rem;margin-left:0}.form-inline .custom-control{-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#fff;text-align:center;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:0.375rem 0.75rem;font-size:0.9375rem;line-height:1.5;border-radius:0.25rem;-webkit-transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.btn{-webkit-transition:none;transition:none}}.btn:hover{color:#fff;text-decoration:none}.btn:focus,.btn.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.btn.disabled,.btn:disabled{opacity:0.65}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-primary:hover{color:#fff;background-color:#2b4764;border-color:#28415b}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#2b4764;border-color:#28415b;-webkit-box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5);box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-primary:not(:disabled):not(.disabled):active,.btn-primary:not(:disabled):not(.disabled).active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#28415b;border-color:#243a53}.btn-primary:not(:disabled):not(.disabled):active:focus,.btn-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-primary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5);box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5)}.btn-secondary{color:#fff;background-color:#444;border-color:#444}.btn-secondary:hover{color:#fff;background-color:#313131;border-color:#2b2a2a}.btn-secondary:focus,.btn-secondary.focus{color:#fff;background-color:#313131;border-color:#2b2a2a;-webkit-box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5);box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#444;border-color:#444}.btn-secondary:not(:disabled):not(.disabled):active,.btn-secondary:not(:disabled):not(.disabled).active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#2b2a2a;border-color:#242424}.btn-secondary:not(:disabled):not(.disabled):active:focus,.btn-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-secondary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5);box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5)}.btn-success{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-success:hover{color:#fff;background-color:#009670;border-color:#008966}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#009670;border-color:#008966;-webkit-box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5);box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-success:not(:disabled):not(.disabled):active,.btn-success:not(:disabled):not(.disabled).active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#008966;border-color:#007c5d}.btn-success:not(:disabled):not(.disabled):active:focus,.btn-success:not(:disabled):not(.disabled).active:focus,.show>.btn-success.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5);box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5)}.btn-info{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-info:hover{color:#fff;background-color:#2384c6;border-color:#217dbb}.btn-info:focus,.btn-info.focus{color:#fff;background-color:#2384c6;border-color:#217dbb;-webkit-box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5);box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-info:not(:disabled):not(.disabled):active,.btn-info:not(:disabled):not(.disabled).active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#217dbb;border-color:#1f76b0}.btn-info:not(:disabled):not(.disabled):active:focus,.btn-info:not(:disabled):not(.disabled).active:focus,.show>.btn-info.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5);box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5)}.btn-warning{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-warning:hover{color:#fff;background-color:#d4860b;border-color:#c87f0a}.btn-warning:focus,.btn-warning.focus{color:#fff;background-color:#d4860b;border-color:#c87f0a;-webkit-box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5);box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5)}.btn-warning.disabled,.btn-warning:disabled{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-warning:not(:disabled):not(.disabled):active,.btn-warning:not(:disabled):not(.disabled).active,.show>.btn-warning.dropdown-toggle{color:#fff;background-color:#c87f0a;border-color:#bc770a}.btn-warning:not(:disabled):not(.disabled):active:focus,.btn-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-warning.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5);box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5)}.btn-danger{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-danger:hover{color:#fff;background-color:#e12e1c;border-color:#d62c1a}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#e12e1c;border-color:#d62c1a;-webkit-box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5);box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-danger:not(:disabled):not(.disabled):active,.btn-danger:not(:disabled):not(.disabled).active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#d62c1a;border-color:#ca2a19}.btn-danger:not(:disabled):not(.disabled):active:focus,.btn-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-danger.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5);box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5)}.btn-light{color:#fff;background-color:#999;border-color:#999}.btn-light:hover{color:#fff;background-color:#868686;border-color:#807f7f}.btn-light:focus,.btn-light.focus{color:#fff;background-color:#868686;border-color:#807f7f;-webkit-box-shadow:0 0 0 0.2rem rgba(168,168,168,0.5);box-shadow:0 0 0 0.2rem rgba(168,168,168,0.5)}.btn-light.disabled,.btn-light:disabled{color:#fff;background-color:#999;border-color:#999}.btn-light:not(:disabled):not(.disabled):active,.btn-light:not(:disabled):not(.disabled).active,.show>.btn-light.dropdown-toggle{color:#fff;background-color:#807f7f;border-color:#797979}.btn-light:not(:disabled):not(.disabled):active:focus,.btn-light:not(:disabled):not(.disabled).active:focus,.show>.btn-light.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(168,168,168,0.5);box-shadow:0 0 0 0.2rem rgba(168,168,168,0.5)}.btn-dark{color:#fff;background-color:#303030;border-color:#303030}.btn-dark:hover{color:#fff;background-color:#1d1d1d;border-color:#171616}.btn-dark:focus,.btn-dark.focus{color:#fff;background-color:#1d1d1d;border-color:#171616;-webkit-box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5);box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#303030;border-color:#303030}.btn-dark:not(:disabled):not(.disabled):active,.btn-dark:not(:disabled):not(.disabled).active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#171616;border-color:#101010}.btn-dark:not(:disabled):not(.disabled):active:focus,.btn-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-dark.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5);box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5)}.btn-outline-primary{color:#375a7f;border-color:#375a7f}.btn-outline-primary:hover{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-outline-primary:focus,.btn-outline-primary.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#375a7f;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled):active,.btn-outline-primary:not(:disabled):not(.disabled).active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5)}.btn-outline-secondary{color:#444;border-color:#444}.btn-outline-secondary:hover{color:#fff;background-color:#444;border-color:#444}.btn-outline-secondary:focus,.btn-outline-secondary.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5);box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#444;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled):active,.btn-outline-secondary:not(:disabled):not(.disabled).active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#444;border-color:#444}.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5);box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5)}.btn-outline-success{color:#00bc8c;border-color:#00bc8c}.btn-outline-success:hover{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-outline-success:focus,.btn-outline-success.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#00bc8c;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled):active,.btn-outline-success:not(:disabled):not(.disabled).active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-outline-success:not(:disabled):not(.disabled):active:focus,.btn-outline-success:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-success.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5)}.btn-outline-info{color:#3498DB;border-color:#3498DB}.btn-outline-info:hover{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-outline-info:focus,.btn-outline-info.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5);box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#3498DB;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled):active,.btn-outline-info:not(:disabled):not(.disabled).active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-outline-info:not(:disabled):not(.disabled):active:focus,.btn-outline-info:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-info.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5);box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5)}.btn-outline-warning{color:#F39C12;border-color:#F39C12}.btn-outline-warning:hover{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-outline-warning:focus,.btn-outline-warning.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5);box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#F39C12;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled):active,.btn-outline-warning:not(:disabled):not(.disabled).active,.show>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5);box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5)}.btn-outline-danger{color:#E74C3C;border-color:#E74C3C}.btn-outline-danger:hover{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-outline-danger:focus,.btn-outline-danger.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#E74C3C;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled):active,.btn-outline-danger:not(:disabled):not(.disabled).active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5)}.btn-outline-light{color:#999;border-color:#999}.btn-outline-light:hover{color:#fff;background-color:#999;border-color:#999}.btn-outline-light:focus,.btn-outline-light.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(153,153,153,0.5);box-shadow:0 0 0 0.2rem rgba(153,153,153,0.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#999;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled):active,.btn-outline-light:not(:disabled):not(.disabled).active,.show>.btn-outline-light.dropdown-toggle{color:#fff;background-color:#999;border-color:#999}.btn-outline-light:not(:disabled):not(.disabled):active:focus,.btn-outline-light:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-light.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(153,153,153,0.5);box-shadow:0 0 0 0.2rem rgba(153,153,153,0.5)}.btn-outline-dark{color:#303030;border-color:#303030}.btn-outline-dark:hover{color:#fff;background-color:#303030;border-color:#303030}.btn-outline-dark:focus,.btn-outline-dark.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5);box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#303030;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled):active,.btn-outline-dark:not(:disabled):not(.disabled).active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#303030;border-color:#303030}.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5);box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5)}.btn-link{font-weight:400;color:#00bc8c;text-decoration:none}.btn-link:hover{color:#007053;text-decoration:underline}.btn-link:focus,.btn-link.focus{text-decoration:underline;-webkit-box-shadow:none;box-shadow:none}.btn-link:disabled,.btn-link.disabled{color:#999;pointer-events:none}.btn-lg,.btn-group-lg>.btn{padding:0.5rem 1rem;font-size:1.171875rem;line-height:1.5;border-radius:0.3rem}.btn-sm,.btn-group-sm>.btn{padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5;border-radius:0.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:0.5rem}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{-webkit-transition:opacity 0.15s linear;transition:opacity 0.15s linear}@media (prefers-reduced-motion: reduce){.fade{-webkit-transition:none;transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height 0.35s ease;transition:height 0.35s ease}@media (prefers-reduced-motion: reduce){.collapsing{-webkit-transition:none;transition:none}}.dropup,.dropright,.dropdown,.dropleft{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid;border-right:0.3em solid transparent;border-bottom:0;border-left:0.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:0.5rem 0;margin:0.125rem 0 0;font-size:0.9375rem;color:#fff;text-align:left;list-style:none;background-color:#222;background-clip:padding-box;border:1px solid #444;border-radius:0.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width: 576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width: 768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width: 992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width: 1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:0.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0;border-right:0.3em solid transparent;border-bottom:0.3em solid;border-left:0.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:0.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid transparent;border-right:0;border-bottom:0.3em solid transparent;border-left:0.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:0.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid transparent;border-right:0.3em solid;border-bottom:0.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^="top"],.dropdown-menu[x-placement^="right"],.dropdown-menu[x-placement^="bottom"],.dropdown-menu[x-placement^="left"]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:0.5rem 0;overflow:hidden;border-top:1px solid #444}.dropdown-item{display:block;width:100%;padding:0.25rem 1.5rem;clear:both;font-weight:400;color:#fff;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:hover,.dropdown-item:focus{color:#fff;text-decoration:none;background-color:#375a7f}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#375a7f}.dropdown-item.disabled,.dropdown-item:disabled{color:#999;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:0.5rem 1.5rem;margin-bottom:0;font-size:0.8203125rem;color:#999;white-space:nowrap}.dropdown-item-text{display:block;padding:0.25rem 1.5rem;color:#fff}.btn-group,.btn-group-vertical{position:relative;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover{z-index:1}.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child){margin-left:-1px}.btn-group>.btn:not(:last-child):not(.dropdown-toggle),.btn-group>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:0.5625rem;padding-left:0.5625rem}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:0.375rem;padding-left:0.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:0.75rem;padding-left:0.75rem}.btn-group-vertical{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle),.btn-group-vertical>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type="radio"],.btn-group-toggle>.btn input[type="checkbox"],.btn-group-toggle>.btn-group>.btn input[type="radio"],.btn-group-toggle>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-control-plaintext,.input-group>.custom-select,.input-group>.custom-file{position:relative;-webkit-box-flex:1;-ms-flex:1 1 0%;flex:1 1 0%;min-width:0;margin-bottom:0}.input-group>.form-control+.form-control,.input-group>.form-control+.custom-select,.input-group>.form-control+.custom-file,.input-group>.form-control-plaintext+.form-control,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.custom-file,.input-group>.custom-select+.form-control,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.custom-file,.input-group>.custom-file+.form-control,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.custom-file{margin-left:-1px}.input-group>.form-control:focus,.input-group>.custom-select:focus,.input-group>.custom-file .custom-file-input:focus ~ .custom-file-label{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.form-control:not(:last-child),.input-group>.custom-select:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.form-control:not(:first-child),.input-group>.custom-select:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-prepend,.input-group-append{display:-webkit-box;display:-ms-flexbox;display:flex}.input-group-prepend .btn,.input-group-append .btn{position:relative;z-index:2}.input-group-prepend .btn:focus,.input-group-append .btn:focus{z-index:3}.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.input-group-text,.input-group-append .input-group-text+.btn{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:0.375rem 0.75rem;margin-bottom:0;font-size:0.9375rem;font-weight:400;line-height:1.5;color:#adb5bd;text-align:center;white-space:nowrap;background-color:#444;border:1px solid #222;border-radius:0.25rem}.input-group-text input[type="radio"],.input-group-text input[type="checkbox"]{margin-top:0}.input-group-lg>.form-control:not(textarea),.input-group-lg>.custom-select{height:calc(1.5em + 1rem + 2px)}.input-group-lg>.form-control,.input-group-lg>.custom-select,.input-group-lg>.input-group-prepend>.input-group-text,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-append>.btn{padding:0.5rem 1rem;font-size:1.171875rem;line-height:1.5;border-radius:0.3rem}.input-group-sm>.form-control:not(textarea),.input-group-sm>.custom-select{height:calc(1.5em + 0.5rem + 2px)}.input-group-sm>.form-control,.input-group-sm>.custom-select,.input-group-sm>.input-group-prepend>.input-group-text,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-append>.btn{padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5;border-radius:0.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text,.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.40625rem;padding-left:1.5rem}.custom-control-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.203125rem;opacity:0}.custom-control-input:checked ~ .custom-control-label::before{color:#fff;border-color:#375a7f;background-color:#375a7f}.custom-control-input:focus ~ .custom-control-label::before{-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-control-input:focus:not(:checked) ~ .custom-control-label::before{border-color:#739ac2}.custom-control-input:not(:disabled):active ~ .custom-control-label::before{color:#fff;background-color:#97b3d2;border-color:#97b3d2}.custom-control-input[disabled] ~ .custom-control-label,.custom-control-input:disabled ~ .custom-control-label{color:#999}.custom-control-input[disabled] ~ .custom-control-label::before,.custom-control-input:disabled ~ .custom-control-label::before{background-color:#ebebeb}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:0.203125rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:0.203125rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50% / 50% 50%}.custom-checkbox .custom-control-label::before{border-radius:0.25rem}.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before{border-color:#375a7f;background-color:#375a7f}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:0.5rem}.custom-switch .custom-control-label::after{top:calc(0.203125rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:0.5rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-switch .custom-control-label::after{-webkit-transition:none;transition:none}}.custom-switch .custom-control-input:checked ~ .custom-control-label::after{background-color:#fff;-webkit-transform:translateX(0.75rem);transform:translateX(0.75rem)}.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + 0.75rem + 2px);padding:0.375rem 1.75rem 0.375rem 0.75rem;font-size:0.9375rem;font-weight:400;line-height:1.5;color:#444;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23303030' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px;border:1px solid #222;border-radius:0.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#739ac2;outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-select:focus::-ms-value{color:#444;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:0.75rem;background-image:none}.custom-select:disabled{color:#999;background-color:#ebebeb}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #444}.custom-select-sm{height:calc(1.5em + 0.5rem + 2px);padding-top:0.25rem;padding-bottom:0.25rem;padding-left:0.5rem;font-size:0.8203125rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:0.5rem;padding-bottom:0.5rem;padding-left:1rem;font-size:1.171875rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + 0.75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + 0.75rem + 2px);margin:0;opacity:0}.custom-file-input:focus ~ .custom-file-label{border-color:#739ac2;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-file-input[disabled] ~ .custom-file-label,.custom-file-input:disabled ~ .custom-file-label{background-color:#ebebeb}.custom-file-input:lang(en) ~ .custom-file-label::after{content:"Browse"}.custom-file-input ~ .custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + 0.75rem + 2px);padding:0.375rem 0.75rem;font-weight:400;line-height:1.5;color:#adb5bd;background-color:#fff;border:1px solid #222;border-radius:0.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + 0.75rem);padding:0.375rem 0.75rem;line-height:1.5;color:#adb5bd;content:"Browse";background-color:#444;border-left:inherit;border-radius:0 0.25rem 0.25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:none}.custom-range:focus::-webkit-slider-thumb{-webkit-box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-0.25rem;background-color:#375a7f;border:0;border-radius:1rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#97b3d2}.custom-range::-webkit-slider-runnable-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#375a7f;border:0;border-radius:1rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-moz-range-thumb{-webkit-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#97b3d2}.custom-range::-moz-range-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:0.2rem;margin-left:0.2rem;background-color:#375a7f;border:0;border-radius:1rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-ms-thumb{-webkit-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#97b3d2}.custom-range::-ms-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:0.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-control-label::before,.custom-file-label,.custom-select{-webkit-transition:none;transition:none}}.nav{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:0.5rem 2rem}.nav-link:hover,.nav-link:focus{text-decoration:none}.nav-link.disabled{color:#adb5bd;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #444}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:0.25rem;border-top-right-radius:0.25rem}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{border-color:#444 #444 transparent}.nav-tabs .nav-link.disabled{color:#adb5bd;background-color:transparent;border-color:transparent}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:#fff;background-color:#222;border-color:#444 #444 transparent}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:0.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#375a7f}.nav-fill .nav-item{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-sm,.navbar .container-md,.navbar .container-lg,.navbar .container-xl{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:0.32421875rem;padding-bottom:0.32421875rem;margin-right:1rem;font-size:1.171875rem;line-height:inherit;white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-nav{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:0.5rem;padding-bottom:0.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:0.25rem 0.75rem;font-size:1.171875rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:0.25rem}.navbar-toggler:hover,.navbar-toggler:focus{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width: 575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 576px){.navbar-expand-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width: 767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-md,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 768px){.navbar-expand-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-md,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width: 991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 992px){.navbar-expand-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width: 1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 1200px){.navbar-expand-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-sm,.navbar-expand>.container-md,.navbar-expand>.container-lg,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-sm,.navbar-expand>.container-md,.navbar-expand>.container-lg,.navbar-expand>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:#fff}.navbar-light .navbar-brand:hover,.navbar-light .navbar-brand:focus{color:#fff}.navbar-light .navbar-nav .nav-link{color:#fff}.navbar-light .navbar-nav .nav-link:hover,.navbar-light .navbar-nav .nav-link:focus{color:#00bc8c}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(255,255,255,0.3)}.navbar-light .navbar-nav .show>.nav-link,.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .nav-link.active{color:#fff}.navbar-light .navbar-toggler{color:#fff;border-color:rgba(255,255,255,0.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='%23fff' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:#fff}.navbar-light .navbar-text a{color:#fff}.navbar-light .navbar-text a:hover,.navbar-light .navbar-text a:focus{color:#fff}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:hover,.navbar-dark .navbar-brand:focus{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,0.6)}.navbar-dark .navbar-nav .nav-link:hover,.navbar-dark .navbar-nav .nav-link:focus{color:#fff}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,0.25)}.navbar-dark .navbar-nav .show>.nav-link,.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .nav-link.active{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,0.6);border-color:rgba(255,255,255,0.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(255, 255, 255, 0.6)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,0.6)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:hover,.navbar-dark .navbar-text a:focus{color:#fff}.card{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#303030;background-clip:border-box;border:1px solid rgba(0,0,0,0.125);border-radius:0.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:0.25rem;border-top-right-radius:0.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:0.25rem;border-bottom-left-radius:0.25rem}.card-body{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:0.75rem}.card-subtitle{margin-top:-0.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:0.75rem 1.25rem;margin-bottom:0;background-color:#444;border-bottom:1px solid rgba(0,0,0,0.125)}.card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:0.75rem 1.25rem;background-color:#444;border-top:1px solid rgba(0,0,0,0.125)}.card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.card-header-tabs{margin-right:-0.625rem;margin-bottom:-0.75rem;margin-left:-0.625rem;border-bottom:0}.card-header-pills{margin-right:-0.625rem;margin-left:-0.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img,.card-img-top,.card-img-bottom{-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(0.25rem - 1px);border-bottom-left-radius:calc(0.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width: 576px){.card-deck{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{-webkit-box-flex:1;-ms-flex:1 0 0%;flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width: 576px){.card-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-webkit-box-flex:1;-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-img-top,.card-group>.card:not(:last-child) .card-header{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-img-bottom,.card-group>.card:not(:last-child) .card-footer{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-img-top,.card-group>.card:not(:first-child) .card-header{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-img-bottom,.card-group>.card:not(:first-child) .card-footer{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:0.75rem}@media (min-width: 576px){.card-columns{-webkit-column-count:3;column-count:3;-webkit-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:0.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#444;border-radius:0.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:0.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:0.5rem;color:#999;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#999}.pagination{display:-webkit-box;display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:0.25rem}.page-link{position:relative;display:block;padding:0.5rem 0.75rem;margin-left:0;line-height:1.25;color:#fff;background-color:#00bc8c;border:0 solid transparent}.page-link:hover{z-index:2;color:#fff;text-decoration:none;background-color:#00efb2;border-color:transparent}.page-link:focus{z-index:3;outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:0.25rem;border-bottom-left-radius:0.25rem}.page-item:last-child .page-link{border-top-right-radius:0.25rem;border-bottom-right-radius:0.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#00efb2;border-color:transparent}.page-item.disabled .page-link{color:#fff;pointer-events:none;cursor:auto;background-color:#007053;border-color:transparent}.pagination-lg .page-link{padding:0.75rem 1.5rem;font-size:1.171875rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:0.3rem;border-bottom-left-radius:0.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:0.3rem;border-bottom-right-radius:0.3rem}.pagination-sm .page-link{padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:0.2rem;border-bottom-left-radius:0.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:0.2rem;border-bottom-right-radius:0.2rem}.badge{display:inline-block;padding:0.25em 0.4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:0.25rem;-webkit-transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.badge{-webkit-transition:none;transition:none}}a.badge:hover,a.badge:focus{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:0.6em;padding-left:0.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#375a7f}a.badge-primary:hover,a.badge-primary:focus{color:#fff;background-color:#28415b}a.badge-primary:focus,a.badge-primary.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5)}.badge-secondary{color:#fff;background-color:#444}a.badge-secondary:hover,a.badge-secondary:focus{color:#fff;background-color:#2b2a2a}a.badge-secondary:focus,a.badge-secondary.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5);box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5)}.badge-success{color:#fff;background-color:#00bc8c}a.badge-success:hover,a.badge-success:focus{color:#fff;background-color:#008966}a.badge-success:focus,a.badge-success.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5)}.badge-info{color:#fff;background-color:#3498DB}a.badge-info:hover,a.badge-info:focus{color:#fff;background-color:#217dbb}a.badge-info:focus,a.badge-info.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5);box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5)}.badge-warning{color:#fff;background-color:#F39C12}a.badge-warning:hover,a.badge-warning:focus{color:#fff;background-color:#c87f0a}a.badge-warning:focus,a.badge-warning.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5);box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5)}.badge-danger{color:#fff;background-color:#E74C3C}a.badge-danger:hover,a.badge-danger:focus{color:#fff;background-color:#d62c1a}a.badge-danger:focus,a.badge-danger.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5)}.badge-light{color:#fff;background-color:#999}a.badge-light:hover,a.badge-light:focus{color:#fff;background-color:#807f7f}a.badge-light:focus,a.badge-light.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(153,153,153,0.5);box-shadow:0 0 0 0.2rem rgba(153,153,153,0.5)}.badge-dark{color:#fff;background-color:#303030}a.badge-dark:hover,a.badge-dark:focus{color:#fff;background-color:#171616}a.badge-dark:focus,a.badge-dark.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5);box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#303030;border-radius:0.3rem}@media (min-width: 576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:0.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:0.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3.90625rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:0.75rem 1.25rem;color:inherit}.alert-primary{color:#1d2f42;background-color:#d7dee5;border-color:#c7d1db}.alert-primary hr{border-top-color:#b7c4d1}.alert-primary .alert-link{color:#0d161f}.alert-secondary{color:#232323;background-color:#dadada;border-color:#cbcbcb}.alert-secondary hr{border-top-color:#bebebe}.alert-secondary .alert-link{color:#0a0909}.alert-success{color:#006249;background-color:#ccf2e8;border-color:#b8ecdf}.alert-success hr{border-top-color:#a4e7d6}.alert-success .alert-link{color:#002f23}.alert-info{color:#1b4f72;background-color:#d6eaf8;border-color:#c6e2f5}.alert-info hr{border-top-color:#b0d7f1}.alert-info .alert-link{color:#113249}.alert-warning{color:#7e5109;background-color:#fdebd0;border-color:#fce3bd}.alert-warning hr{border-top-color:#fbd9a5}.alert-warning .alert-link{color:#4e3206}.alert-danger{color:#78281f;background-color:#fadbd8;border-color:#f8cdc8}.alert-danger hr{border-top-color:#f5b8b1}.alert-danger .alert-link{color:#4f1a15}.alert-light{color:#505050;background-color:#ebebeb;border-color:#e2e2e2}.alert-light hr{border-top-color:#d5d5d5}.alert-light .alert-link{color:#373636}.alert-dark{color:#191919;background-color:#d6d6d6;border-color:#c5c5c5}.alert-dark hr{border-top-color:#b8b8b8}.alert-dark .alert-link{color:black}@-webkit-keyframes progress-bar-stripes{from{background-position:0.625rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:0.625rem 0}to{background-position:0 0}}.progress{display:-webkit-box;display:-ms-flexbox;display:flex;height:0.625rem;overflow:hidden;font-size:0.625rem;background-color:#444;border-radius:0.25rem}.progress-bar{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#375a7f;-webkit-transition:width 0.6s ease;transition:width 0.6s ease}@media (prefers-reduced-motion: reduce){.progress-bar{-webkit-transition:none;transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-size:0.625rem 0.625rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion: reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.media-body{-webkit-box-flex:1;-ms-flex:1;flex:1}.list-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#444;text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{z-index:1;color:#444;text-decoration:none;background-color:#444}.list-group-item-action:active{color:#fff;background-color:#ebebeb}.list-group-item{position:relative;display:block;padding:0.75rem 1.25rem;background-color:#303030;border:1px solid #444}.list-group-item:first-child{border-top-left-radius:0.25rem;border-top-right-radius:0.25rem}.list-group-item:last-child{border-bottom-right-radius:0.25rem;border-bottom-left-radius:0.25rem}.list-group-item.disabled,.list-group-item:disabled{color:#999;pointer-events:none;background-color:#303030}.list-group-item.active{z-index:2;color:#fff;background-color:#375a7f;border-color:#375a7f}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal .list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal .list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal .list-group-item.active{margin-top:0}.list-group-horizontal .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width: 576px){.list-group-horizontal-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm .list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-sm .list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm .list-group-item.active{margin-top:0}.list-group-horizontal-sm .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 768px){.list-group-horizontal-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md .list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-md .list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-md .list-group-item.active{margin-top:0}.list-group-horizontal-md .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 992px){.list-group-horizontal-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg .list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-lg .list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg .list-group-item.active{margin-top:0}.list-group-horizontal-lg .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 1200px){.list-group-horizontal-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl .list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-xl .list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl .list-group-item.active{margin-top:0}.list-group-horizontal-xl .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush .list-group-item{border-right-width:0;border-left-width:0;border-radius:0}.list-group-flush .list-group-item:first-child{border-top-width:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#1d2f42;background-color:#c7d1db}.list-group-item-primary.list-group-item-action:hover,.list-group-item-primary.list-group-item-action:focus{color:#1d2f42;background-color:#b7c4d1}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#1d2f42;border-color:#1d2f42}.list-group-item-secondary{color:#232323;background-color:#cbcbcb}.list-group-item-secondary.list-group-item-action:hover,.list-group-item-secondary.list-group-item-action:focus{color:#232323;background-color:#bebebe}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#232323;border-color:#232323}.list-group-item-success{color:#006249;background-color:#b8ecdf}.list-group-item-success.list-group-item-action:hover,.list-group-item-success.list-group-item-action:focus{color:#006249;background-color:#a4e7d6}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#006249;border-color:#006249}.list-group-item-info{color:#1b4f72;background-color:#c6e2f5}.list-group-item-info.list-group-item-action:hover,.list-group-item-info.list-group-item-action:focus{color:#1b4f72;background-color:#b0d7f1}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#1b4f72;border-color:#1b4f72}.list-group-item-warning{color:#7e5109;background-color:#fce3bd}.list-group-item-warning.list-group-item-action:hover,.list-group-item-warning.list-group-item-action:focus{color:#7e5109;background-color:#fbd9a5}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#7e5109;border-color:#7e5109}.list-group-item-danger{color:#78281f;background-color:#f8cdc8}.list-group-item-danger.list-group-item-action:hover,.list-group-item-danger.list-group-item-action:focus{color:#78281f;background-color:#f5b8b1}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#78281f;border-color:#78281f}.list-group-item-light{color:#505050;background-color:#e2e2e2}.list-group-item-light.list-group-item-action:hover,.list-group-item-light.list-group-item-action:focus{color:#505050;background-color:#d5d5d5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#505050;border-color:#505050}.list-group-item-dark{color:#191919;background-color:#c5c5c5}.list-group-item-dark.list-group-item-action:hover,.list-group-item-dark.list-group-item-action:focus{color:#191919;background-color:#b8b8b8}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#191919;border-color:#191919}.close{float:right;font-size:1.40625rem;font-weight:700;line-height:1;color:#fff;text-shadow:none;opacity:.5}.close:hover{color:#fff;text-decoration:none}.close:not(:disabled):not(.disabled):hover,.close:not(:disabled):not(.disabled):focus{opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:0.875rem;background-color:rgba(255,255,255,0.85);background-clip:padding-box;border:1px solid rgba(0,0,0,0.1);-webkit-box-shadow:0 0.25rem 0.75rem rgba(0,0,0,0.1);box-shadow:0 0.25rem 0.75rem rgba(0,0,0,0.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:0.25rem}.toast:not(:last-child){margin-bottom:0.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:0.25rem 0.75rem;color:#999;background-color:rgba(255,255,255,0.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,0.05)}.toast-body{padding:0.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:0.5rem;pointer-events:none}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform 0.3s ease-out;transition:-webkit-transform 0.3s ease-out;transition:transform 0.3s ease-out;transition:transform 0.3s ease-out, -webkit-transform 0.3s ease-out;-webkit-transform:translate(0, -50px);transform:translate(0, -50px)}@media (prefers-reduced-motion: reduce){.modal.fade .modal-dialog{-webkit-transition:none;transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{display:-webkit-box;display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-header,.modal-dialog-scrollable .modal-footer{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);content:""}.modal-dialog-centered.modal-dialog-scrollable{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#303030;background-clip:padding-box;border:1px solid #444;border-radius:0.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:0.5}.modal-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #444;border-top-left-radius:calc(0.3rem - 1px);border-top-right-radius:calc(0.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;padding:0.75rem;border-top:1px solid #444;border-bottom-right-radius:calc(0.3rem - 1px);border-bottom-left-radius:calc(0.3rem - 1px)}.modal-footer>*{margin:0.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width: 576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width: 992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width: 1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:"Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:0.8203125rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:0.9}.tooltip .arrow{position:absolute;display:block;width:0.8rem;height:0.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-top,.bs-tooltip-auto[x-placement^="top"]{padding:0.4rem 0}.bs-tooltip-top .arrow,.bs-tooltip-auto[x-placement^="top"] .arrow{bottom:0}.bs-tooltip-top .arrow::before,.bs-tooltip-auto[x-placement^="top"] .arrow::before{top:0;border-width:0.4rem 0.4rem 0;border-top-color:#000}.bs-tooltip-right,.bs-tooltip-auto[x-placement^="right"]{padding:0 0.4rem}.bs-tooltip-right .arrow,.bs-tooltip-auto[x-placement^="right"] .arrow{left:0;width:0.4rem;height:0.8rem}.bs-tooltip-right .arrow::before,.bs-tooltip-auto[x-placement^="right"] .arrow::before{right:0;border-width:0.4rem 0.4rem 0.4rem 0;border-right-color:#000}.bs-tooltip-bottom,.bs-tooltip-auto[x-placement^="bottom"]{padding:0.4rem 0}.bs-tooltip-bottom .arrow,.bs-tooltip-auto[x-placement^="bottom"] .arrow{top:0}.bs-tooltip-bottom .arrow::before,.bs-tooltip-auto[x-placement^="bottom"] .arrow::before{bottom:0;border-width:0 0.4rem 0.4rem;border-bottom-color:#000}.bs-tooltip-left,.bs-tooltip-auto[x-placement^="left"]{padding:0 0.4rem}.bs-tooltip-left .arrow,.bs-tooltip-auto[x-placement^="left"] .arrow{right:0;width:0.4rem;height:0.8rem}.bs-tooltip-left .arrow::before,.bs-tooltip-auto[x-placement^="left"] .arrow::before{left:0;border-width:0.4rem 0 0.4rem 0.4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:0.25rem 0.5rem;color:#fff;text-align:center;background-color:#000;border-radius:0.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:"Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:0.8203125rem;word-wrap:break-word;background-color:#303030;background-clip:padding-box;border:1px solid rgba(0,0,0,0.2);border-radius:0.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:0.5rem;margin:0 0.3rem}.popover .arrow::before,.popover .arrow::after{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-top,.bs-popover-auto[x-placement^="top"]{margin-bottom:0.5rem}.bs-popover-top>.arrow,.bs-popover-auto[x-placement^="top"]>.arrow{bottom:calc(-0.5rem - 1px)}.bs-popover-top>.arrow::before,.bs-popover-auto[x-placement^="top"]>.arrow::before{bottom:0;border-width:0.5rem 0.5rem 0;border-top-color:rgba(0,0,0,0.25)}.bs-popover-top>.arrow::after,.bs-popover-auto[x-placement^="top"]>.arrow::after{bottom:1px;border-width:0.5rem 0.5rem 0;border-top-color:#303030}.bs-popover-right,.bs-popover-auto[x-placement^="right"]{margin-left:0.5rem}.bs-popover-right>.arrow,.bs-popover-auto[x-placement^="right"]>.arrow{left:calc(-0.5rem - 1px);width:0.5rem;height:1rem;margin:0.3rem 0}.bs-popover-right>.arrow::before,.bs-popover-auto[x-placement^="right"]>.arrow::before{left:0;border-width:0.5rem 0.5rem 0.5rem 0;border-right-color:rgba(0,0,0,0.25)}.bs-popover-right>.arrow::after,.bs-popover-auto[x-placement^="right"]>.arrow::after{left:1px;border-width:0.5rem 0.5rem 0.5rem 0;border-right-color:#303030}.bs-popover-bottom,.bs-popover-auto[x-placement^="bottom"]{margin-top:0.5rem}.bs-popover-bottom>.arrow,.bs-popover-auto[x-placement^="bottom"]>.arrow{top:calc(-0.5rem - 1px)}.bs-popover-bottom>.arrow::before,.bs-popover-auto[x-placement^="bottom"]>.arrow::before{top:0;border-width:0 0.5rem 0.5rem 0.5rem;border-bottom-color:rgba(0,0,0,0.25)}.bs-popover-bottom>.arrow::after,.bs-popover-auto[x-placement^="bottom"]>.arrow::after{top:1px;border-width:0 0.5rem 0.5rem 0.5rem;border-bottom-color:#303030}.bs-popover-bottom .popover-header::before,.bs-popover-auto[x-placement^="bottom"] .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-0.5rem;content:"";border-bottom:1px solid #444}.bs-popover-left,.bs-popover-auto[x-placement^="left"]{margin-right:0.5rem}.bs-popover-left>.arrow,.bs-popover-auto[x-placement^="left"]>.arrow{right:calc(-0.5rem - 1px);width:0.5rem;height:1rem;margin:0.3rem 0}.bs-popover-left>.arrow::before,.bs-popover-auto[x-placement^="left"]>.arrow::before{right:0;border-width:0.5rem 0 0.5rem 0.5rem;border-left-color:rgba(0,0,0,0.25)}.bs-popover-left>.arrow::after,.bs-popover-auto[x-placement^="left"]>.arrow::after{right:1px;border-width:0.5rem 0 0.5rem 0.5rem;border-left-color:#303030}.popover-header{padding:0.5rem 0.75rem;margin-bottom:0;font-size:0.9375rem;background-color:#444;border-bottom:1px solid #373737;border-top-left-radius:calc(0.3rem - 1px);border-top-right-radius:calc(0.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:0.5rem 0.75rem;color:#fff}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transition:-webkit-transform 0.6s ease-in-out;transition:-webkit-transform 0.6s ease-in-out;transition:transform 0.6s ease-in-out;transition:transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out}@media (prefers-reduced-motion: reduce){.carousel-item{-webkit-transition:none;transition:none}}.carousel-item.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next:not(.carousel-item-left),.active.carousel-item-right{-webkit-transform:translateX(100%);transform:translateX(100%)}.carousel-item-prev:not(.carousel-item-right),.active.carousel-item-left{-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;-webkit-transition-property:opacity;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;-webkit-transition:opacity 0s 0.6s;transition:opacity 0s 0.6s}@media (prefers-reduced-motion: reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{-webkit-transition:none;transition:none}}.carousel-control-prev,.carousel-control-next{position:absolute;top:0;bottom:0;z-index:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:0.5;-webkit-transition:opacity 0.15s ease;transition:opacity 0.15s ease}@media (prefers-reduced-motion: reduce){.carousel-control-prev,.carousel-control-next{-webkit-transition:none;transition:none}}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:0.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50% / 100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{-webkit-box-sizing:content-box;box-sizing:content-box;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;-webkit-transition:opacity 0.6s ease;transition:opacity 0.6s ease}@media (prefers-reduced-motion: reduce){.carousel-indicators li{-webkit-transition:none;transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:0.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:0.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline !important}.align-top{vertical-align:top !important}.align-middle{vertical-align:middle !important}.align-bottom{vertical-align:bottom !important}.align-text-bottom{vertical-align:text-bottom !important}.align-text-top{vertical-align:text-top !important}.bg-primary{background-color:#375a7f !important}a.bg-primary:hover,a.bg-primary:focus,button.bg-primary:hover,button.bg-primary:focus{background-color:#28415b !important}.bg-secondary{background-color:#444 !important}a.bg-secondary:hover,a.bg-secondary:focus,button.bg-secondary:hover,button.bg-secondary:focus{background-color:#2b2a2a !important}.bg-success{background-color:#00bc8c !important}a.bg-success:hover,a.bg-success:focus,button.bg-success:hover,button.bg-success:focus{background-color:#008966 !important}.bg-info{background-color:#3498DB !important}a.bg-info:hover,a.bg-info:focus,button.bg-info:hover,button.bg-info:focus{background-color:#217dbb !important}.bg-warning{background-color:#F39C12 !important}a.bg-warning:hover,a.bg-warning:focus,button.bg-warning:hover,button.bg-warning:focus{background-color:#c87f0a !important}.bg-danger{background-color:#E74C3C !important}a.bg-danger:hover,a.bg-danger:focus,button.bg-danger:hover,button.bg-danger:focus{background-color:#d62c1a !important}.bg-light{background-color:#999 !important}a.bg-light:hover,a.bg-light:focus,button.bg-light:hover,button.bg-light:focus{background-color:#807f7f !important}.bg-dark{background-color:#303030 !important}a.bg-dark:hover,a.bg-dark:focus,button.bg-dark:hover,button.bg-dark:focus{background-color:#171616 !important}.bg-white{background-color:#fff !important}.bg-transparent{background-color:transparent !important}.border{border:1px solid #dee2e6 !important}.border-top{border-top:1px solid #dee2e6 !important}.border-right{border-right:1px solid #dee2e6 !important}.border-bottom{border-bottom:1px solid #dee2e6 !important}.border-left{border-left:1px solid #dee2e6 !important}.border-0{border:0 !important}.border-top-0{border-top:0 !important}.border-right-0{border-right:0 !important}.border-bottom-0{border-bottom:0 !important}.border-left-0{border-left:0 !important}.border-primary{border-color:#375a7f !important}.border-secondary{border-color:#444 !important}.border-success{border-color:#00bc8c !important}.border-info{border-color:#3498DB !important}.border-warning{border-color:#F39C12 !important}.border-danger{border-color:#E74C3C !important}.border-light{border-color:#999 !important}.border-dark{border-color:#303030 !important}.border-white{border-color:#fff !important}.rounded-sm{border-radius:0.2rem !important}.rounded{border-radius:0.25rem !important}.rounded-top{border-top-left-radius:0.25rem !important;border-top-right-radius:0.25rem !important}.rounded-right{border-top-right-radius:0.25rem !important;border-bottom-right-radius:0.25rem !important}.rounded-bottom{border-bottom-right-radius:0.25rem !important;border-bottom-left-radius:0.25rem !important}.rounded-left{border-top-left-radius:0.25rem !important;border-bottom-left-radius:0.25rem !important}.rounded-lg{border-radius:0.3rem !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:50rem !important}.rounded-0{border-radius:0 !important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-block{display:block !important}.d-table{display:table !important}.d-table-row{display:table-row !important}.d-table-cell{display:table-cell !important}.d-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}@media (min-width: 576px){.d-sm-none{display:none !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-block{display:block !important}.d-sm-table{display:table !important}.d-sm-table-row{display:table-row !important}.d-sm-table-cell{display:table-cell !important}.d-sm-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-sm-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 768px){.d-md-none{display:none !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-block{display:block !important}.d-md-table{display:table !important}.d-md-table-row{display:table-row !important}.d-md-table-cell{display:table-cell !important}.d-md-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-md-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 992px){.d-lg-none{display:none !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-block{display:block !important}.d-lg-table{display:table !important}.d-lg-table-row{display:table-row !important}.d-lg-table-cell{display:table-cell !important}.d-lg-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-lg-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 1200px){.d-xl-none{display:none !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-block{display:block !important}.d-xl-table{display:table !important}.d-xl-table-row{display:table-row !important}.d-xl-table-cell{display:table-cell !important}.d-xl-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-xl-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media print{.d-print-none{display:none !important}.d-print-inline{display:inline !important}.d-print-inline-block{display:inline-block !important}.d-print-block{display:block !important}.d-print-table{display:table !important}.d-print-table-row{display:table-row !important}.d-print-table-cell{display:table-cell !important}.d-print-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-print-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.8571428571%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}@media (min-width: 576px){.flex-sm-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-sm-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-sm-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-sm-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-sm-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-sm-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-sm-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-sm-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-sm-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-sm-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-sm-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-sm-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-sm-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-sm-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-sm-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-sm-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-sm-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-sm-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-sm-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-sm-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-sm-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-sm-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-sm-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-sm-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-sm-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-sm-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-sm-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-sm-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-sm-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-sm-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-sm-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-sm-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-sm-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 768px){.flex-md-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-md-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-md-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-md-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-md-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-md-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-md-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-md-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-md-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-md-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-md-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-md-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-md-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-md-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-md-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-md-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-md-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-md-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-md-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-md-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-md-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-md-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-md-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-md-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-md-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-md-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-md-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-md-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-md-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-md-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-md-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-md-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-md-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 992px){.flex-lg-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-lg-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-lg-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-lg-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-lg-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-lg-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-lg-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-lg-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-lg-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-lg-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-lg-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-lg-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-lg-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-lg-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-lg-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-lg-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-lg-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-lg-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-lg-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-lg-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-lg-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-lg-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-lg-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-lg-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-lg-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-lg-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-lg-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-lg-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-lg-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-lg-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-lg-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-lg-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-lg-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 1200px){.flex-xl-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-xl-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-xl-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-xl-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-xl-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-xl-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-xl-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-xl-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-xl-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-xl-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-xl-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-xl-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-xl-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-xl-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-xl-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-xl-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-xl-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-xl-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-xl-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-xl-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-xl-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-xl-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-xl-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-xl-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-xl-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-xl-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-xl-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-xl-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-xl-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-xl-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-xl-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-xl-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-xl-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}.float-left{float:left !important}.float-right{float:right !important}.float-none{float:none !important}@media (min-width: 576px){.float-sm-left{float:left !important}.float-sm-right{float:right !important}.float-sm-none{float:none !important}}@media (min-width: 768px){.float-md-left{float:left !important}.float-md-right{float:right !important}.float-md-none{float:none !important}}@media (min-width: 992px){.float-lg-left{float:left !important}.float-lg-right{float:right !important}.float-lg-none{float:none !important}}@media (min-width: 1200px){.float-xl-left{float:left !important}.float-xl-right{float:right !important}.float-xl-none{float:none !important}}.overflow-auto{overflow:auto !important}.overflow-hidden{overflow:hidden !important}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:-webkit-sticky !important;position:sticky !important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports (position: -webkit-sticky) or (position: sticky){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{-webkit-box-shadow:0 0.125rem 0.25rem rgba(0,0,0,0.075) !important;box-shadow:0 0.125rem 0.25rem rgba(0,0,0,0.075) !important}.shadow{-webkit-box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15) !important;box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15) !important}.shadow-lg{-webkit-box-shadow:0 1rem 3rem rgba(0,0,0,0.175) !important;box-shadow:0 1rem 3rem rgba(0,0,0,0.175) !important}.shadow-none{-webkit-box-shadow:none !important;box-shadow:none !important}.w-25{width:25% !important}.w-50{width:50% !important}.w-75{width:75% !important}.w-100{width:100% !important}.w-auto{width:auto !important}.h-25{height:25% !important}.h-50{height:50% !important}.h-75{height:75% !important}.h-100{height:100% !important}.h-auto{height:auto !important}.mw-100{max-width:100% !important}.mh-100{max-height:100% !important}.min-vw-100{min-width:100vw !important}.min-vh-100{min-height:100vh !important}.vw-100{width:100vw !important}.vh-100{height:100vh !important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.m-0{margin:0 !important}.mt-0,.my-0{margin-top:0 !important}.mr-0,.mx-0{margin-right:0 !important}.mb-0,.my-0{margin-bottom:0 !important}.ml-0,.mx-0{margin-left:0 !important}.m-1{margin:0.25rem !important}.mt-1,.my-1{margin-top:0.25rem !important}.mr-1,.mx-1{margin-right:0.25rem !important}.mb-1,.my-1{margin-bottom:0.25rem !important}.ml-1,.mx-1{margin-left:0.25rem !important}.m-2{margin:0.5rem !important}.mt-2,.my-2{margin-top:0.5rem !important}.mr-2,.mx-2{margin-right:0.5rem !important}.mb-2,.my-2{margin-bottom:0.5rem !important}.ml-2,.mx-2{margin-left:0.5rem !important}.m-3{margin:1rem !important}.mt-3,.my-3{margin-top:1rem !important}.mr-3,.mx-3{margin-right:1rem !important}.mb-3,.my-3{margin-bottom:1rem !important}.ml-3,.mx-3{margin-left:1rem !important}.m-4{margin:1.5rem !important}.mt-4,.my-4{margin-top:1.5rem !important}.mr-4,.mx-4{margin-right:1.5rem !important}.mb-4,.my-4{margin-bottom:1.5rem !important}.ml-4,.mx-4{margin-left:1.5rem !important}.m-5{margin:3rem !important}.mt-5,.my-5{margin-top:3rem !important}.mr-5,.mx-5{margin-right:3rem !important}.mb-5,.my-5{margin-bottom:3rem !important}.ml-5,.mx-5{margin-left:3rem !important}.p-0{padding:0 !important}.pt-0,.py-0{padding-top:0 !important}.pr-0,.px-0{padding-right:0 !important}.pb-0,.py-0{padding-bottom:0 !important}.pl-0,.px-0{padding-left:0 !important}.p-1{padding:0.25rem !important}.pt-1,.py-1{padding-top:0.25rem !important}.pr-1,.px-1{padding-right:0.25rem !important}.pb-1,.py-1{padding-bottom:0.25rem !important}.pl-1,.px-1{padding-left:0.25rem !important}.p-2{padding:0.5rem !important}.pt-2,.py-2{padding-top:0.5rem !important}.pr-2,.px-2{padding-right:0.5rem !important}.pb-2,.py-2{padding-bottom:0.5rem !important}.pl-2,.px-2{padding-left:0.5rem !important}.p-3{padding:1rem !important}.pt-3,.py-3{padding-top:1rem !important}.pr-3,.px-3{padding-right:1rem !important}.pb-3,.py-3{padding-bottom:1rem !important}.pl-3,.px-3{padding-left:1rem !important}.p-4{padding:1.5rem !important}.pt-4,.py-4{padding-top:1.5rem !important}.pr-4,.px-4{padding-right:1.5rem !important}.pb-4,.py-4{padding-bottom:1.5rem !important}.pl-4,.px-4{padding-left:1.5rem !important}.p-5{padding:3rem !important}.pt-5,.py-5{padding-top:3rem !important}.pr-5,.px-5{padding-right:3rem !important}.pb-5,.py-5{padding-bottom:3rem !important}.pl-5,.px-5{padding-left:3rem !important}.m-n1{margin:-0.25rem !important}.mt-n1,.my-n1{margin-top:-0.25rem !important}.mr-n1,.mx-n1{margin-right:-0.25rem !important}.mb-n1,.my-n1{margin-bottom:-0.25rem !important}.ml-n1,.mx-n1{margin-left:-0.25rem !important}.m-n2{margin:-0.5rem !important}.mt-n2,.my-n2{margin-top:-0.5rem !important}.mr-n2,.mx-n2{margin-right:-0.5rem !important}.mb-n2,.my-n2{margin-bottom:-0.5rem !important}.ml-n2,.mx-n2{margin-left:-0.5rem !important}.m-n3{margin:-1rem !important}.mt-n3,.my-n3{margin-top:-1rem !important}.mr-n3,.mx-n3{margin-right:-1rem !important}.mb-n3,.my-n3{margin-bottom:-1rem !important}.ml-n3,.mx-n3{margin-left:-1rem !important}.m-n4{margin:-1.5rem !important}.mt-n4,.my-n4{margin-top:-1.5rem !important}.mr-n4,.mx-n4{margin-right:-1.5rem !important}.mb-n4,.my-n4{margin-bottom:-1.5rem !important}.ml-n4,.mx-n4{margin-left:-1.5rem !important}.m-n5{margin:-3rem !important}.mt-n5,.my-n5{margin-top:-3rem !important}.mr-n5,.mx-n5{margin-right:-3rem !important}.mb-n5,.my-n5{margin-bottom:-3rem !important}.ml-n5,.mx-n5{margin-left:-3rem !important}.m-auto{margin:auto !important}.mt-auto,.my-auto{margin-top:auto !important}.mr-auto,.mx-auto{margin-right:auto !important}.mb-auto,.my-auto{margin-bottom:auto !important}.ml-auto,.mx-auto{margin-left:auto !important}@media (min-width: 576px){.m-sm-0{margin:0 !important}.mt-sm-0,.my-sm-0{margin-top:0 !important}.mr-sm-0,.mx-sm-0{margin-right:0 !important}.mb-sm-0,.my-sm-0{margin-bottom:0 !important}.ml-sm-0,.mx-sm-0{margin-left:0 !important}.m-sm-1{margin:0.25rem !important}.mt-sm-1,.my-sm-1{margin-top:0.25rem !important}.mr-sm-1,.mx-sm-1{margin-right:0.25rem !important}.mb-sm-1,.my-sm-1{margin-bottom:0.25rem !important}.ml-sm-1,.mx-sm-1{margin-left:0.25rem !important}.m-sm-2{margin:0.5rem !important}.mt-sm-2,.my-sm-2{margin-top:0.5rem !important}.mr-sm-2,.mx-sm-2{margin-right:0.5rem !important}.mb-sm-2,.my-sm-2{margin-bottom:0.5rem !important}.ml-sm-2,.mx-sm-2{margin-left:0.5rem !important}.m-sm-3{margin:1rem !important}.mt-sm-3,.my-sm-3{margin-top:1rem !important}.mr-sm-3,.mx-sm-3{margin-right:1rem !important}.mb-sm-3,.my-sm-3{margin-bottom:1rem !important}.ml-sm-3,.mx-sm-3{margin-left:1rem !important}.m-sm-4{margin:1.5rem !important}.mt-sm-4,.my-sm-4{margin-top:1.5rem !important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem !important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem !important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem !important}.m-sm-5{margin:3rem !important}.mt-sm-5,.my-sm-5{margin-top:3rem !important}.mr-sm-5,.mx-sm-5{margin-right:3rem !important}.mb-sm-5,.my-sm-5{margin-bottom:3rem !important}.ml-sm-5,.mx-sm-5{margin-left:3rem !important}.p-sm-0{padding:0 !important}.pt-sm-0,.py-sm-0{padding-top:0 !important}.pr-sm-0,.px-sm-0{padding-right:0 !important}.pb-sm-0,.py-sm-0{padding-bottom:0 !important}.pl-sm-0,.px-sm-0{padding-left:0 !important}.p-sm-1{padding:0.25rem !important}.pt-sm-1,.py-sm-1{padding-top:0.25rem !important}.pr-sm-1,.px-sm-1{padding-right:0.25rem !important}.pb-sm-1,.py-sm-1{padding-bottom:0.25rem !important}.pl-sm-1,.px-sm-1{padding-left:0.25rem !important}.p-sm-2{padding:0.5rem !important}.pt-sm-2,.py-sm-2{padding-top:0.5rem !important}.pr-sm-2,.px-sm-2{padding-right:0.5rem !important}.pb-sm-2,.py-sm-2{padding-bottom:0.5rem !important}.pl-sm-2,.px-sm-2{padding-left:0.5rem !important}.p-sm-3{padding:1rem !important}.pt-sm-3,.py-sm-3{padding-top:1rem !important}.pr-sm-3,.px-sm-3{padding-right:1rem !important}.pb-sm-3,.py-sm-3{padding-bottom:1rem !important}.pl-sm-3,.px-sm-3{padding-left:1rem !important}.p-sm-4{padding:1.5rem !important}.pt-sm-4,.py-sm-4{padding-top:1.5rem !important}.pr-sm-4,.px-sm-4{padding-right:1.5rem !important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem !important}.pl-sm-4,.px-sm-4{padding-left:1.5rem !important}.p-sm-5{padding:3rem !important}.pt-sm-5,.py-sm-5{padding-top:3rem !important}.pr-sm-5,.px-sm-5{padding-right:3rem !important}.pb-sm-5,.py-sm-5{padding-bottom:3rem !important}.pl-sm-5,.px-sm-5{padding-left:3rem !important}.m-sm-n1{margin:-0.25rem !important}.mt-sm-n1,.my-sm-n1{margin-top:-0.25rem !important}.mr-sm-n1,.mx-sm-n1{margin-right:-0.25rem !important}.mb-sm-n1,.my-sm-n1{margin-bottom:-0.25rem !important}.ml-sm-n1,.mx-sm-n1{margin-left:-0.25rem !important}.m-sm-n2{margin:-0.5rem !important}.mt-sm-n2,.my-sm-n2{margin-top:-0.5rem !important}.mr-sm-n2,.mx-sm-n2{margin-right:-0.5rem !important}.mb-sm-n2,.my-sm-n2{margin-bottom:-0.5rem !important}.ml-sm-n2,.mx-sm-n2{margin-left:-0.5rem !important}.m-sm-n3{margin:-1rem !important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem !important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem !important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem !important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem !important}.m-sm-n4{margin:-1.5rem !important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem !important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem !important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem !important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem !important}.m-sm-n5{margin:-3rem !important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem !important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem !important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem !important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem !important}.m-sm-auto{margin:auto !important}.mt-sm-auto,.my-sm-auto{margin-top:auto !important}.mr-sm-auto,.mx-sm-auto{margin-right:auto !important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto !important}.ml-sm-auto,.mx-sm-auto{margin-left:auto !important}}@media (min-width: 768px){.m-md-0{margin:0 !important}.mt-md-0,.my-md-0{margin-top:0 !important}.mr-md-0,.mx-md-0{margin-right:0 !important}.mb-md-0,.my-md-0{margin-bottom:0 !important}.ml-md-0,.mx-md-0{margin-left:0 !important}.m-md-1{margin:0.25rem !important}.mt-md-1,.my-md-1{margin-top:0.25rem !important}.mr-md-1,.mx-md-1{margin-right:0.25rem !important}.mb-md-1,.my-md-1{margin-bottom:0.25rem !important}.ml-md-1,.mx-md-1{margin-left:0.25rem !important}.m-md-2{margin:0.5rem !important}.mt-md-2,.my-md-2{margin-top:0.5rem !important}.mr-md-2,.mx-md-2{margin-right:0.5rem !important}.mb-md-2,.my-md-2{margin-bottom:0.5rem !important}.ml-md-2,.mx-md-2{margin-left:0.5rem !important}.m-md-3{margin:1rem !important}.mt-md-3,.my-md-3{margin-top:1rem !important}.mr-md-3,.mx-md-3{margin-right:1rem !important}.mb-md-3,.my-md-3{margin-bottom:1rem !important}.ml-md-3,.mx-md-3{margin-left:1rem !important}.m-md-4{margin:1.5rem !important}.mt-md-4,.my-md-4{margin-top:1.5rem !important}.mr-md-4,.mx-md-4{margin-right:1.5rem !important}.mb-md-4,.my-md-4{margin-bottom:1.5rem !important}.ml-md-4,.mx-md-4{margin-left:1.5rem !important}.m-md-5{margin:3rem !important}.mt-md-5,.my-md-5{margin-top:3rem !important}.mr-md-5,.mx-md-5{margin-right:3rem !important}.mb-md-5,.my-md-5{margin-bottom:3rem !important}.ml-md-5,.mx-md-5{margin-left:3rem !important}.p-md-0{padding:0 !important}.pt-md-0,.py-md-0{padding-top:0 !important}.pr-md-0,.px-md-0{padding-right:0 !important}.pb-md-0,.py-md-0{padding-bottom:0 !important}.pl-md-0,.px-md-0{padding-left:0 !important}.p-md-1{padding:0.25rem !important}.pt-md-1,.py-md-1{padding-top:0.25rem !important}.pr-md-1,.px-md-1{padding-right:0.25rem !important}.pb-md-1,.py-md-1{padding-bottom:0.25rem !important}.pl-md-1,.px-md-1{padding-left:0.25rem !important}.p-md-2{padding:0.5rem !important}.pt-md-2,.py-md-2{padding-top:0.5rem !important}.pr-md-2,.px-md-2{padding-right:0.5rem !important}.pb-md-2,.py-md-2{padding-bottom:0.5rem !important}.pl-md-2,.px-md-2{padding-left:0.5rem !important}.p-md-3{padding:1rem !important}.pt-md-3,.py-md-3{padding-top:1rem !important}.pr-md-3,.px-md-3{padding-right:1rem !important}.pb-md-3,.py-md-3{padding-bottom:1rem !important}.pl-md-3,.px-md-3{padding-left:1rem !important}.p-md-4{padding:1.5rem !important}.pt-md-4,.py-md-4{padding-top:1.5rem !important}.pr-md-4,.px-md-4{padding-right:1.5rem !important}.pb-md-4,.py-md-4{padding-bottom:1.5rem !important}.pl-md-4,.px-md-4{padding-left:1.5rem !important}.p-md-5{padding:3rem !important}.pt-md-5,.py-md-5{padding-top:3rem !important}.pr-md-5,.px-md-5{padding-right:3rem !important}.pb-md-5,.py-md-5{padding-bottom:3rem !important}.pl-md-5,.px-md-5{padding-left:3rem !important}.m-md-n1{margin:-0.25rem !important}.mt-md-n1,.my-md-n1{margin-top:-0.25rem !important}.mr-md-n1,.mx-md-n1{margin-right:-0.25rem !important}.mb-md-n1,.my-md-n1{margin-bottom:-0.25rem !important}.ml-md-n1,.mx-md-n1{margin-left:-0.25rem !important}.m-md-n2{margin:-0.5rem !important}.mt-md-n2,.my-md-n2{margin-top:-0.5rem !important}.mr-md-n2,.mx-md-n2{margin-right:-0.5rem !important}.mb-md-n2,.my-md-n2{margin-bottom:-0.5rem !important}.ml-md-n2,.mx-md-n2{margin-left:-0.5rem !important}.m-md-n3{margin:-1rem !important}.mt-md-n3,.my-md-n3{margin-top:-1rem !important}.mr-md-n3,.mx-md-n3{margin-right:-1rem !important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem !important}.ml-md-n3,.mx-md-n3{margin-left:-1rem !important}.m-md-n4{margin:-1.5rem !important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem !important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem !important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem !important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem !important}.m-md-n5{margin:-3rem !important}.mt-md-n5,.my-md-n5{margin-top:-3rem !important}.mr-md-n5,.mx-md-n5{margin-right:-3rem !important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem !important}.ml-md-n5,.mx-md-n5{margin-left:-3rem !important}.m-md-auto{margin:auto !important}.mt-md-auto,.my-md-auto{margin-top:auto !important}.mr-md-auto,.mx-md-auto{margin-right:auto !important}.mb-md-auto,.my-md-auto{margin-bottom:auto !important}.ml-md-auto,.mx-md-auto{margin-left:auto !important}}@media (min-width: 992px){.m-lg-0{margin:0 !important}.mt-lg-0,.my-lg-0{margin-top:0 !important}.mr-lg-0,.mx-lg-0{margin-right:0 !important}.mb-lg-0,.my-lg-0{margin-bottom:0 !important}.ml-lg-0,.mx-lg-0{margin-left:0 !important}.m-lg-1{margin:0.25rem !important}.mt-lg-1,.my-lg-1{margin-top:0.25rem !important}.mr-lg-1,.mx-lg-1{margin-right:0.25rem !important}.mb-lg-1,.my-lg-1{margin-bottom:0.25rem !important}.ml-lg-1,.mx-lg-1{margin-left:0.25rem !important}.m-lg-2{margin:0.5rem !important}.mt-lg-2,.my-lg-2{margin-top:0.5rem !important}.mr-lg-2,.mx-lg-2{margin-right:0.5rem !important}.mb-lg-2,.my-lg-2{margin-bottom:0.5rem !important}.ml-lg-2,.mx-lg-2{margin-left:0.5rem !important}.m-lg-3{margin:1rem !important}.mt-lg-3,.my-lg-3{margin-top:1rem !important}.mr-lg-3,.mx-lg-3{margin-right:1rem !important}.mb-lg-3,.my-lg-3{margin-bottom:1rem !important}.ml-lg-3,.mx-lg-3{margin-left:1rem !important}.m-lg-4{margin:1.5rem !important}.mt-lg-4,.my-lg-4{margin-top:1.5rem !important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem !important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem !important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem !important}.m-lg-5{margin:3rem !important}.mt-lg-5,.my-lg-5{margin-top:3rem !important}.mr-lg-5,.mx-lg-5{margin-right:3rem !important}.mb-lg-5,.my-lg-5{margin-bottom:3rem !important}.ml-lg-5,.mx-lg-5{margin-left:3rem !important}.p-lg-0{padding:0 !important}.pt-lg-0,.py-lg-0{padding-top:0 !important}.pr-lg-0,.px-lg-0{padding-right:0 !important}.pb-lg-0,.py-lg-0{padding-bottom:0 !important}.pl-lg-0,.px-lg-0{padding-left:0 !important}.p-lg-1{padding:0.25rem !important}.pt-lg-1,.py-lg-1{padding-top:0.25rem !important}.pr-lg-1,.px-lg-1{padding-right:0.25rem !important}.pb-lg-1,.py-lg-1{padding-bottom:0.25rem !important}.pl-lg-1,.px-lg-1{padding-left:0.25rem !important}.p-lg-2{padding:0.5rem !important}.pt-lg-2,.py-lg-2{padding-top:0.5rem !important}.pr-lg-2,.px-lg-2{padding-right:0.5rem !important}.pb-lg-2,.py-lg-2{padding-bottom:0.5rem !important}.pl-lg-2,.px-lg-2{padding-left:0.5rem !important}.p-lg-3{padding:1rem !important}.pt-lg-3,.py-lg-3{padding-top:1rem !important}.pr-lg-3,.px-lg-3{padding-right:1rem !important}.pb-lg-3,.py-lg-3{padding-bottom:1rem !important}.pl-lg-3,.px-lg-3{padding-left:1rem !important}.p-lg-4{padding:1.5rem !important}.pt-lg-4,.py-lg-4{padding-top:1.5rem !important}.pr-lg-4,.px-lg-4{padding-right:1.5rem !important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem !important}.pl-lg-4,.px-lg-4{padding-left:1.5rem !important}.p-lg-5{padding:3rem !important}.pt-lg-5,.py-lg-5{padding-top:3rem !important}.pr-lg-5,.px-lg-5{padding-right:3rem !important}.pb-lg-5,.py-lg-5{padding-bottom:3rem !important}.pl-lg-5,.px-lg-5{padding-left:3rem !important}.m-lg-n1{margin:-0.25rem !important}.mt-lg-n1,.my-lg-n1{margin-top:-0.25rem !important}.mr-lg-n1,.mx-lg-n1{margin-right:-0.25rem !important}.mb-lg-n1,.my-lg-n1{margin-bottom:-0.25rem !important}.ml-lg-n1,.mx-lg-n1{margin-left:-0.25rem !important}.m-lg-n2{margin:-0.5rem !important}.mt-lg-n2,.my-lg-n2{margin-top:-0.5rem !important}.mr-lg-n2,.mx-lg-n2{margin-right:-0.5rem !important}.mb-lg-n2,.my-lg-n2{margin-bottom:-0.5rem !important}.ml-lg-n2,.mx-lg-n2{margin-left:-0.5rem !important}.m-lg-n3{margin:-1rem !important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem !important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem !important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem !important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem !important}.m-lg-n4{margin:-1.5rem !important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem !important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem !important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem !important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem !important}.m-lg-n5{margin:-3rem !important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem !important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem !important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem !important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem !important}.m-lg-auto{margin:auto !important}.mt-lg-auto,.my-lg-auto{margin-top:auto !important}.mr-lg-auto,.mx-lg-auto{margin-right:auto !important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto !important}.ml-lg-auto,.mx-lg-auto{margin-left:auto !important}}@media (min-width: 1200px){.m-xl-0{margin:0 !important}.mt-xl-0,.my-xl-0{margin-top:0 !important}.mr-xl-0,.mx-xl-0{margin-right:0 !important}.mb-xl-0,.my-xl-0{margin-bottom:0 !important}.ml-xl-0,.mx-xl-0{margin-left:0 !important}.m-xl-1{margin:0.25rem !important}.mt-xl-1,.my-xl-1{margin-top:0.25rem !important}.mr-xl-1,.mx-xl-1{margin-right:0.25rem !important}.mb-xl-1,.my-xl-1{margin-bottom:0.25rem !important}.ml-xl-1,.mx-xl-1{margin-left:0.25rem !important}.m-xl-2{margin:0.5rem !important}.mt-xl-2,.my-xl-2{margin-top:0.5rem !important}.mr-xl-2,.mx-xl-2{margin-right:0.5rem !important}.mb-xl-2,.my-xl-2{margin-bottom:0.5rem !important}.ml-xl-2,.mx-xl-2{margin-left:0.5rem !important}.m-xl-3{margin:1rem !important}.mt-xl-3,.my-xl-3{margin-top:1rem !important}.mr-xl-3,.mx-xl-3{margin-right:1rem !important}.mb-xl-3,.my-xl-3{margin-bottom:1rem !important}.ml-xl-3,.mx-xl-3{margin-left:1rem !important}.m-xl-4{margin:1.5rem !important}.mt-xl-4,.my-xl-4{margin-top:1.5rem !important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem !important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem !important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem !important}.m-xl-5{margin:3rem !important}.mt-xl-5,.my-xl-5{margin-top:3rem !important}.mr-xl-5,.mx-xl-5{margin-right:3rem !important}.mb-xl-5,.my-xl-5{margin-bottom:3rem !important}.ml-xl-5,.mx-xl-5{margin-left:3rem !important}.p-xl-0{padding:0 !important}.pt-xl-0,.py-xl-0{padding-top:0 !important}.pr-xl-0,.px-xl-0{padding-right:0 !important}.pb-xl-0,.py-xl-0{padding-bottom:0 !important}.pl-xl-0,.px-xl-0{padding-left:0 !important}.p-xl-1{padding:0.25rem !important}.pt-xl-1,.py-xl-1{padding-top:0.25rem !important}.pr-xl-1,.px-xl-1{padding-right:0.25rem !important}.pb-xl-1,.py-xl-1{padding-bottom:0.25rem !important}.pl-xl-1,.px-xl-1{padding-left:0.25rem !important}.p-xl-2{padding:0.5rem !important}.pt-xl-2,.py-xl-2{padding-top:0.5rem !important}.pr-xl-2,.px-xl-2{padding-right:0.5rem !important}.pb-xl-2,.py-xl-2{padding-bottom:0.5rem !important}.pl-xl-2,.px-xl-2{padding-left:0.5rem !important}.p-xl-3{padding:1rem !important}.pt-xl-3,.py-xl-3{padding-top:1rem !important}.pr-xl-3,.px-xl-3{padding-right:1rem !important}.pb-xl-3,.py-xl-3{padding-bottom:1rem !important}.pl-xl-3,.px-xl-3{padding-left:1rem !important}.p-xl-4{padding:1.5rem !important}.pt-xl-4,.py-xl-4{padding-top:1.5rem !important}.pr-xl-4,.px-xl-4{padding-right:1.5rem !important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem !important}.pl-xl-4,.px-xl-4{padding-left:1.5rem !important}.p-xl-5{padding:3rem !important}.pt-xl-5,.py-xl-5{padding-top:3rem !important}.pr-xl-5,.px-xl-5{padding-right:3rem !important}.pb-xl-5,.py-xl-5{padding-bottom:3rem !important}.pl-xl-5,.px-xl-5{padding-left:3rem !important}.m-xl-n1{margin:-0.25rem !important}.mt-xl-n1,.my-xl-n1{margin-top:-0.25rem !important}.mr-xl-n1,.mx-xl-n1{margin-right:-0.25rem !important}.mb-xl-n1,.my-xl-n1{margin-bottom:-0.25rem !important}.ml-xl-n1,.mx-xl-n1{margin-left:-0.25rem !important}.m-xl-n2{margin:-0.5rem !important}.mt-xl-n2,.my-xl-n2{margin-top:-0.5rem !important}.mr-xl-n2,.mx-xl-n2{margin-right:-0.5rem !important}.mb-xl-n2,.my-xl-n2{margin-bottom:-0.5rem !important}.ml-xl-n2,.mx-xl-n2{margin-left:-0.5rem !important}.m-xl-n3{margin:-1rem !important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem !important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem !important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem !important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem !important}.m-xl-n4{margin:-1.5rem !important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem !important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem !important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem !important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem !important}.m-xl-n5{margin:-3rem !important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem !important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem !important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem !important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem !important}.m-xl-auto{margin:auto !important}.mt-xl-auto,.my-xl-auto{margin-top:auto !important}.mr-xl-auto,.mx-xl-auto{margin-right:auto !important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto !important}.ml-xl-auto,.mx-xl-auto{margin-left:auto !important}}.text-monospace{font-family:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important}.text-justify{text-align:justify !important}.text-wrap{white-space:normal !important}.text-nowrap{white-space:nowrap !important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left !important}.text-right{text-align:right !important}.text-center{text-align:center !important}@media (min-width: 576px){.text-sm-left{text-align:left !important}.text-sm-right{text-align:right !important}.text-sm-center{text-align:center !important}}@media (min-width: 768px){.text-md-left{text-align:left !important}.text-md-right{text-align:right !important}.text-md-center{text-align:center !important}}@media (min-width: 992px){.text-lg-left{text-align:left !important}.text-lg-right{text-align:right !important}.text-lg-center{text-align:center !important}}@media (min-width: 1200px){.text-xl-left{text-align:left !important}.text-xl-right{text-align:right !important}.text-xl-center{text-align:center !important}}.text-lowercase{text-transform:lowercase !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.font-weight-light{font-weight:300 !important}.font-weight-lighter{font-weight:lighter !important}.font-weight-normal{font-weight:400 !important}.font-weight-bold{font-weight:700 !important}.font-weight-bolder{font-weight:bolder !important}.font-italic{font-style:italic !important}.text-white{color:#fff !important}.text-primary{color:#375a7f !important}a.text-primary:hover,a.text-primary:focus{color:#20344a !important}.text-secondary{color:#444 !important}a.text-secondary:hover,a.text-secondary:focus{color:#1e1e1e !important}.text-success{color:#00bc8c !important}a.text-success:hover,a.text-success:focus{color:#007053 !important}.text-info{color:#3498DB !important}a.text-info:hover,a.text-info:focus{color:#1d6fa5 !important}.text-warning{color:#F39C12 !important}a.text-warning:hover,a.text-warning:focus{color:#b06f09 !important}.text-danger{color:#E74C3C !important}a.text-danger:hover,a.text-danger:focus{color:#bf2718 !important}.text-light{color:#999 !important}a.text-light:hover,a.text-light:focus{color:#737373 !important}.text-dark{color:#303030 !important}a.text-dark:hover,a.text-dark:focus{color:#0a0a0a !important}.text-body{color:#fff !important}.text-muted{color:#999 !important}.text-black-50{color:rgba(0,0,0,0.5) !important}.text-white-50{color:rgba(255,255,255,0.5) !important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none !important}.text-break{word-break:break-word !important;overflow-wrap:break-word !important}.text-reset{color:inherit !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}@media print{*,*::before,*::after{text-shadow:none !important;-webkit-box-shadow:none !important;box-shadow:none !important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap !important}pre,blockquote{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px !important}.container{min-width:992px !important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #dee2e6 !important}.table-dark{color:inherit}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody+tbody{border-color:#444}.table .thead-dark th{color:inherit;border-color:#444}}.bg-primary .navbar-nav .active>.nav-link{color:#00bc8c !important}.bg-light.navbar{background-color:#00bc8c !important}.bg-light.navbar-light .navbar-nav .nav-link:focus,.bg-light.navbar-light .navbar-nav .nav-link:hover,.bg-light.navbar-light .navbar-nav .active>.nav-link{color:#375a7f !important}.blockquote-footer{color:#999}.table-primary,.table-primary>th,.table-primary>td{background-color:#375a7f}.table-secondary,.table-secondary>th,.table-secondary>td{background-color:#444}.table-light,.table-light>th,.table-light>td{background-color:#999}.table-dark,.table-dark>th,.table-dark>td{background-color:#303030}.table-success,.table-success>th,.table-success>td{background-color:#00bc8c}.table-info,.table-info>th,.table-info>td{background-color:#3498DB}.table-danger,.table-danger>th,.table-danger>td{background-color:#E74C3C}.table-warning,.table-warning>th,.table-warning>td{background-color:#F39C12}.table-active,.table-active>th,.table-active>td{background-color:rgba(0,0,0,0.075)}.table-hover .table-primary:hover,.table-hover .table-primary:hover>th,.table-hover .table-primary:hover>td{background-color:#2f4d6d}.table-hover .table-secondary:hover,.table-hover .table-secondary:hover>th,.table-hover .table-secondary:hover>td{background-color:#373737}.table-hover .table-light:hover,.table-hover .table-light:hover>th,.table-hover .table-light:hover>td{background-color:#8c8c8c}.table-hover .table-dark:hover,.table-hover .table-dark:hover>th,.table-hover .table-dark:hover>td{background-color:#232323}.table-hover .table-success:hover,.table-hover .table-success:hover>th,.table-hover .table-success:hover>td{background-color:#00a379}.table-hover .table-info:hover,.table-hover .table-info:hover>th,.table-hover .table-info:hover>td{background-color:#258cd1}.table-hover .table-danger:hover,.table-hover .table-danger:hover>th,.table-hover .table-danger:hover>td{background-color:#e43725}.table-hover .table-warning:hover,.table-hover .table-warning:hover>th,.table-hover .table-warning:hover>td{background-color:#e08e0b}.table-hover .table-active:hover,.table-hover .table-active:hover>th,.table-hover .table-active:hover>td{background-color:rgba(0,0,0,0.075)}.input-group-addon{color:#fff}.nav-tabs .nav-link,.nav-tabs .nav-link.active,.nav-tabs .nav-link.active:focus,.nav-tabs .nav-link.active:hover,.nav-tabs .nav-item.open .nav-link,.nav-tabs .nav-item.open .nav-link:focus,.nav-tabs .nav-item.open .nav-link:hover,.nav-pills .nav-link,.nav-pills .nav-link.active,.nav-pills .nav-link.active:focus,.nav-pills .nav-link.active:hover,.nav-pills .nav-item.open .nav-link,.nav-pills .nav-item.open .nav-link:focus,.nav-pills .nav-item.open .nav-link:hover{color:#fff}.breadcrumb a{color:#fff}.pagination a:hover{text-decoration:none}.close{opacity:0.4}.close:hover,.close:focus{opacity:1}.alert{border:none;color:#fff}.alert a,.alert .alert-link{color:#fff;text-decoration:underline}.alert-primary{background-color:#375a7f}.alert-secondary{background-color:#444}.alert-success{background-color:#00bc8c}.alert-info{background-color:#3498DB}.alert-warning{background-color:#F39C12}.alert-danger{background-color:#E74C3C}.alert-light{background-color:#999}.alert-dark{background-color:#303030}.list-group-item-action{color:#fff}.list-group-item-action:hover,.list-group-item-action:focus{background-color:#444;color:#fff}.list-group-item-action .list-group-item-heading{color:#fff} diff --git a/css/font-awesome.min.css b/css/font-awesome.min.css new file mode 100644 index 00000000..540440ce --- /dev/null +++ b/css/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/el/builds/index.html b/el/builds/index.html new file mode 100644 index 00000000..be97a346 --- /dev/null +++ b/el/builds/index.html @@ -0,0 +1,237 @@ + + + + + + + + + + + Auto-Provisioning - Linux Guide and Hints + + + + + + + + + + + + + + + +
+
+
+
+ +

This page goes over various ways that installs can be automated without the use of PXE. Instead, we can use templated scripts with pre-configured commands, boot images, and mirrors for builds. We cover the following here:

+
    +
  • CentOS Stream 9
  • +
  • Enterprise Linux 8, 9
  • +
  • Fedora
  • +
  • openSUSE 15+
  • +
  • Windows Server
  • +
+
+
+ + + + + + + + + + + + diff --git a/el/freeipa/index.html b/el/freeipa/index.html new file mode 100644 index 00000000..9c551361 --- /dev/null +++ b/el/freeipa/index.html @@ -0,0 +1,2934 @@ + + + + + + + + + + + FreeIPA - Linux Guide and Hints + + + + + + + + + + + + + + + +
+
+
+
+ +

This page is a series of notes and information that goes over how to +install and configure FreeIPA on Enterprise Linux 8/9 servers with +replicas, as well as configuring client machines to connect and utilize +FreeIPA resources, policies (eg sudo), and host based access control +methods. We will also go over a scenario of configuring a trust with an +Active Directory domain. The client setup will work for Fedora users as +the packages are the same, just newer versions.

+

Overview

+

FreeIPA is an integrated security information management system +combining Linux, a Directory Server (389), Kerberos, NTP, DNS, DogTag. +It's a system that can be loosely compared to Active Directory in what +it attempts to solve for Linux and UNIX clients and even mixed +environments. While it is not an active directory, it is an +integrated Identity and Authentication solution for Linux/UNIX +environments, which means it does not support Windows clients. One +problem that FreeIPA attempts to solve is giving back control to the +Linux/UNIX administration teams of access, authentication, and +authorization rather than trying to integrate directly into Active +Directory, where the controls do not work the same or do not work at +all. And because of this, no third party software is required to be +installed.

+

Requirements

+

Here are the list of requirements below.

+
    +
  • Enterprise Linux 8+ or Fedora Linux
  • +
  • An active internet connection to install the packages required or + available internal mirrors
  • +
  • 2 core, 4GB system with at least 10GB+ disk for /var/lib/dirsrv
  • +
  • DNS domain delegation (if a DNS appliance or server already exists)
  • +
+

Tutorial Preface, Notes, and Recommendations

+
+

Potential Pitfalls!

+
    +
  • Leave SELinux enabled at all times. You will not run into SELinux + issues
  • +
  • FreeIPA runs better when it controls the DNS domain that it is + given - It is recommended DNS is delegated or that FreeIPA run DNS + entirely
  • +
  • FreeIPA does not run DHCP. ISC DHCP can be configured to do dynamic + DNS updates to FreeIPA or hosts can be configured to perform dynamic + DNS updates
  • +
+
+
+

Recommended Information

+
    +
  • Keep selinux set to enforcing
  • +
  • +

    DNS - You must be careful when using DNS. Here are + recommendations.1

    +
      +
    • Recommendation 1: FreeIPA runs your entire DNS for your + network - This requires the DHCP servers to set the DNS servers + to the IPA servers. This will be useful in the case that your + clients will have their SSH keys added as SSHFP records to DNS + when enrolled as clients. This also gives you the added benefit + of a client updating its own DNS entries (A and PTR records) if + the client is DHCP enabled and the IP changes if you so choose.
    • +
    • Recommendation 2: FreeIPA is delegated a subdomain of a domain + used already in the network - It's not required for hosts to + live in the subdomain to be a member of the IPA domain, but you + will lose out on kerberos SSO. Do not try to hijack a domain.
    • +
    +
  • +
  • +

    Consider setting up a trust with Active Directory if you are in a + mixed environment, eg Active Directory already exists - winsync is + available, but deprecated and not recommended.

    +
  • +
  • IPA servers should have static assigned addresses - Configured via + nmcli or directly in /etc/sysconfig/network-scripts/ifcfg-*
  • +
  • Try to avoid running FreeIPA without DNS - while possible, you are + creating higher maintenance
  • +
+
+
+

Trust Information

+

If you are in a mixed environment (both Windows and Linux/UNIX), it is +recommended to setup a trust between FreeIPA and Active Directory. +Because of this, they will need to be in different domains (eg, +example.com and ipa.example.com, or example.com and example.net). This +way, you do not have to create duplicate users if a windows user logs +into Linux resources nor use winsync.

+
+

DNS

+

As noted in the previous section, you must try not to hijack a domain. +You can migrate records over to FreeIPA's DNS if you'd like, but care +must be taken with that approach.

+

While FreeIPA can do the typical DNS server work such as forward/reverse +zones and various types of records, it should not be considered a full +solution. It does not support views (eg, you can't have internal and +external views, assuming you have domains that are publically facing). +In the event you need to have views, that's when you need a different +DNS server or service to provide this to you.

+

There are two ways you can have DNS entries updated dynamically: +--enable-dns-updates for ipa-client-install and DHCP dynamic DNS +updates. Both are sufficient. The latter requires additional work and is +outside the scope of this write up.

+

Delegation

+

Throughout this guide, you may find or see examples of domain delegation +where there is an AD trust, as it would be a more real world example of +bringing in FreeIPA to an environment that is already in place, working, +with a DNS hosted by AD or by an appliance. Majority of the examples +assume both IPA and AD is delegated (when it's normally IPA that's +just delegated while AD hosts the actual parent zone). Using this type +of setup, it is not required for clients to have entries in the IPA +domain. In fact, they can be in other domains as long as they have +A/AAAA/PTR records associated with them. This assumes that there could +be dynamic dns associated with DHCP or everything is static and lives in +the parent zones. The caveat to this is SSO will fail.

+

You can setup already existing DNS servers to delegate an entire domain +or a subdomain for FreeIPA. This way, you don't overlap with a domain +that's already in use. So for example, if AD owns example.com, you +could have AD delegate ipa.example.com or even forward example.net. If +AD is not the DNS provider for the environment, you can have the +appliance delegate the domain in the same manner.

+

Below is a bind example of what example.com would look like when +delegating the IPA domain:

+
$ORIGIN example.com.
+@ IN SOA ... ( )
+                        NS      np-ad01
+                        NS      np-ad02
+np-ad01                 A       10.200.0.232
+np-ad02                 A       10.200.0.233
+; Many other records here, pertaining to AD, eg msdcs and SRV records
+
+; IPA records
+$ORIGIN ipa.example.com.
+@                       NS      np-ipa01
+                        NS      np-ipa02
+np-ipa01                A       10.200.0.230
+np-ipa02                A       10.200.0.231
+
+

Note that AD can send nsupdates to a DNS server if given the permissions. As of +this writing, FreeIPA does not do this, which is why DNS delegation is recommended.

+

Server Setup

+

Required Packages

+
    +
  • ipa-server
  • +
  • ipa-client (required as an IPA server is technically a client of the + domain)
  • +
  • ipa-server-dns (required for using the internal DNS)
  • +
  • sssd/sssd-ipa (pulled in as dependencies)
  • +
+

Optional Packages

+
    +
  • ipa-server-trust-ad if using an AD trust
  • +
+

Installation

+

To install the server, make sure the hostname is set to the A records +and NS delegations you've put in DNS (which won't respond to a DNS +lookup). If these are stand-alone, then you can just keep it at the top +level (eg, example.com). You'll also need to modify /etc/hosts, set +static IP addresses, and then run the ipa-server-install command.

+
% hostnamectl set-hostname server1.ipa.example.com
+% nmcli con mod ens192 ipv4.address 10.200.0.230/24
+% nmcli con mod ens192 ipv4.gateway 10.200.0.1
+% nmcli con mod ens192 ipv4.method manual
+% nmcli con up ens192
+% vi /etc/hosts
+. . .
+10.200.0.230 server1.ipa.example.com
+10.200.0.231 server2.ipa.example.com
+
+# Fedora
+% dnf install freeipa-server{,-common,-dns,-trust-ad} -y
+
+# Enterprise Linux 8
+% dnf module enable idm:DL1/{dns,adtrust,client,server,common}
+% dnf install ipa-server ipa-server-dns ipa-client sssd sssd-ipa -y
+
+# Enterprise Linux 9 (there appears to be no modules)
+% dnf install ipa-server ipa-server-dns ipa-client sssd sssd-ipa -y
+
+# Setup
+# Enterprise 8 / 9
+% firewall-cmd --permanent --add-service={freeipa-4,ntp,dns,freeipa-trust}
+% firewall-cmd --complete-reload
+% ipa-server-install \
+    --no_hbac_allow \ <-- If you want to have HBAC allow_all disabled initially
+    --no-ntp \ <-- If you want to host NTP from IPA, take off --no-ntp
+    --setup-dns \
+    --realm IPA.EXAMPLE.COM \
+    --domain example.com 
+
+. . . (show steps here)
+
+

While not officially recommended, you could have two accounts. One for +administration of servers and the domain and one for your workstation, +similar to separating domain users and domain administrators in active +directory. You don't have to follow this, but at least there's a form +of separation.

+
% kinit admin
+% ipa user-add --first=First --last=Last --cn="First Last Admin" --gecos="First Last Admin" flast2
+% ipa group-add-member --users=flast2 admins
+
+

Replica

+

On the replica, ensure you repeat the same steps as above.

+
% hostnamectl set-hostname server2.ipa.example.com
+% nmcli con mod ens192 ipv4.address 10.200.0.231/24
+% nmcli con mod ens192 ipv4.gateway 10.200.0.1
+% nmcli con mod ens192 ipv4.method manual
+% nmcli con up ens192
+% vi /etc/hosts
+. . .
+10.200.0.230 server1.ipa.example.com
+10.200.0.231 server2.ipa.example.com
+
+% dnf install ipa-server ipa-server-dns ipa-client sssd sssd-ipa -y
+# Enterprise 8 / 9
+% firewall-cmd --permanent --add-service={freeipa-4,ntp,dns,freeipa-trust}
+% firewall-cmd --complete-reload
+% ipa-replica-install --no-forwarders --setup-ca --setup-dns --no-ntp --principal admin --admin-password "ChangePass123" --domain ipa.example.com
+. . . (show steps)
+
+

You should now be able to see your replicas.

+
% ipa-replica-manage list
+server1.ipa.example.com: master
+server2.ipa.example.com: master
+
+

Replica Automation

+

It is possible to automate the replica installation. To automate the +replica installation, the following requirements would need to be met:

+
    +
  • Server must be added as a client (ipa-client-install) with an IP + address on the commandline
  • +
  • Server must be added to the ipaservers host group
  • +
  • ipa-replica-install ran without principal and passwords
  • +
+

Once you have a server added as a client and then added to the +ipaservers host group, you would run a command like this:

+
% ipa-replica-install --ssh-trust-dns --unattended --setup-ca --mkhomedir --setup-dns --no-forwarders
+
+

If you have forwarders, use the --forwarders option instead.

+

Server Migration/Upgrade

+

Performing a migration is a multi-step process. Typically you are going +from one major version of Enterprise Linux (such as 7 or 8) to another +(such as 9). Regardless of which version you are migrating from, the +typical beginning steps are:

+
    +
  • System's time is verified for time synchronization like using + ntpstat or equivalent
  • +
  • Server roles are verified in the current environment using + ipa server-role-find --status enabled --server ipa.example.com
  • +
  • New system is installed and enrolled as a client
  • +
  • New system is added as a replica with required server roles
  • +
+
+

EL7 to EL9 / Two Major Version Jumps

+

When jumping from EL7 to EL9 or two major versions in general, it is +recommended that you have an "in between" machine. This means that you +need to add the in between version first and then you can add the latest +version. See this page +for an example.

+
+

The below is in the case of a single master installation and doesn't +take into account of multiple version jumps. Let's say you have two old +Enterprise Linux replicas instead. There are two approaches you can +take:

+
    +
  • Install a new Enterprise Linux system, add it, reinstall old system + to the new version, add it back.
  • +
  • Install two new Enterprise Linux systems, add them as needed, power + off old systems.
  • +
+

Below is an example, with X being the old version, and Y being the new.

+
    +
  • Enterprise Linux Y system is installed and enrolled as a client
  • +
  • Enterprise Linux Y system is added as a replica
  • +
  • Change CRL to Enterprise Linux Y system and adjust settings on + Enterprise Linux X CA master and new Enterprise Linux Y replica for + pki-tomcatd and httpd
  • +
  • Test user is created to ensure DNA range is adjusted
  • +
  • Verify DNA range
  • +
  • Stop first Enterprise Linux X IPA services, remove replica, + uninstall, power off.
  • +
  • Second Enterprise Linux Y system is installed and enrolled as a + client
  • +
  • Second Enterprise Linux Y system is added as a replica
  • +
  • Test user is created again to ensure DNA range is adjusted
  • +
  • Verify DNA range
  • +
  • Stop second Enterprise Linux X IPA services, remove replica, + uninstall, power off.
  • +
+

EL7 to EL8

+
# Enterprise Linux 8
+% dnf module enable idm:DL1
+
+# Install necessary packages, ie AD trust packages if you need them
+% dnf install ipa-server ipa-server-dns -y
+% ipa-client-install --realm EXAMPLE.COM --domain example.com
+% kinit admin
+
+# Add other switches that you feel are necessary, such as forwarders, kra, ntp...
+% ipa-replica-install --setup-dns --setup-ca --ssh-trust-dns --mkhomedir
+
+# Verify all services are in a RUNNING state
+% ipactl status
+Directory Service: RUNNING
+. . .
+
+% ipa-csreplica-manage list
+elX.example.com: master
+elY.example.com: master
+
+% ipa-csreplica-manage list --verbose elY.example.com
+Directory Manager password:
+
+elX.example.com
+  last init status: None
+  last init ended: 1970-01-01 00:00:00+00:00
+  last update status: Error (0) Replica acquired successfully: Incremental update succeeded
+  last update ended: 2019-11-07 22:46:15+00:00
+
+
    +
  • Change CRL to new Enterprise Linux system and adjust settings on + both replicas for pki-tomcatd and httpd
  • +
+
# Change CA master to elY
+% ipa config-mod --ca-renewal-master-server elY.example.com
+
+# Shut down all CRL generation on ELX
+elX% ipa-crlgen-manage status
+CRL generation: enabled
+. . .
+
+elX% ipa-crlgen-manage disable
+Stopping pki-tomcatd
+Editing /var/lib/pki/pki-tomcat/conf/ca/CS.cfg
+Starting pki-tomcatd
+Editing /etc/httpd/conf.d/ipa-pki-proxy.conf
+Restarting httpd
+CRL generation disabled on the local host. Please make sure to configure CRL generation on another master with ipa-crlgen-manage enable.
+The ipa-crlgen-manage command was successful
+
+# Verify that the /etc/httpd/conf.d/ipa-pki-proxy.conf file's RewriteRule is not commented
+# If it is, remove the comment and restart httpd. ipa-crlgen-manage should take care of this.
+% tail -n 1 /etc/httpd/conf.d/ipa-pki-proxy.conf
+RewriteRule ^/ipa/crl/MasterCRL.bin https://elX.example.com/ca/ee/ca/getCRL?op=getCRL&crlIssuingPoint=MasterCRL [L,R=301,NC]
+
+# Turn it on with ELY
+elY% systemctl stop pki-tomcatd@pki-tomcat.service
+
+# The values should be changed from false to true
+elY% vi /etc/pki/pki-tomcat/ca/CS.cfg
+ca.crl.MasterCRL.enableCRLCache=true
+ca.crl.MasterCRL.enableCRLUpdates=true
+
+elY% systemctl start pki-tomcatd@pki-tomcat.service
+
+# Make sure the rewrite rule has a comment on elY
+elY% vi /etc/httpd/conf.d/ipa-pki-proxy.conf
+. . .
+#RewriteRule ^/ipa/crl/MasterCRL.bin https://elY.example.com/ca/ee/ca/getCRL?op=getCRL&crlIssuingPoint=MasterCRL [L,R=301,NC]
+
+elY% systemctl restart httpd
+
+
    +
  • Test user is created to ensure DNA range is adjusted and replication + is working
  • +
+
% ipa user-add --first=testing --last=user testinguser1
+
+# Test on both systems
+elX% ipa user-find testinguser1
+elY% ipa user-find testinguser1
+
+
    +
  • Verify DNA range
  • +
+
# There should be ranges for both replicas
+% ipa-replica-manage dnarange-show
+elX.example.com: ...
+elY.example.com: ...
+
+
    +
  • Stop old Enterprise Linux IPA services, remove replica, uninstall
  • +
+
# Stop all elX services
+elX% ipactl stop
+
+# Delete the elX system from the topology
+elY% ipa server-del elX.example.com
+
+# Uninstall and/or power down system
+elX% ipa-server-install --uninstall
+elX% init 0
+
+

EL8 to EL9

+
# Enterprise Linux 9
+% dnf install ipa-server ipa-server-dns -y
+% ipa-client-install --realm EXAMPLE.COM --domain example.com
+% kinit admin
+
+# Add other switches that you feel are necessary, such as forwarders, kra, ntp...
+% ipa-replica-install --setup-dns --setup-ca --ssh-trust-dns --mkhomedir
+
+# Verify all services are in a RUNNING state
+% ipactl status
+Directory Service: RUNNING
+. . .
+
+% ipa-csreplica-manage list
+elX.example.com: master
+elY.example.com: master
+
+% ipa-csreplica-manage list --verbose elY.example.com
+Directory Manager password:
+
+elX.example.com
+  last init status: None
+  last init ended: 1970-01-01 00:00:00+00:00
+  last update status: Error (0) Replica acquired successfully: Incremental update succeeded
+  last update ended: 2022-08-12 18:11:11+00:00
+
+

Set the CA renewal master to the new system and change the CRL settings

+
% ipa config-mod --ca-renewal-master-server elY.example.com
+
+# Remove the ca.certStatusUpdateInterval entry or set it to 600 (default) on elY
+elY% vim /etc/pki/pki-tomcat/ca/CS.cfg
+
+# Restart the ipa services
+elY% ipactl restart
+
+# Set the value of ca.certStatusUpdateInterval on elX to 0
+elX% vim /etc/pki/pki-tomcat/ca/CS.cfg
+ca.certStatusUpdateInterval=0
+
+elX% ipactl restart
+
+elX% ipa-crlgen-manage status
+CRL generation: enabled
+. . .
+
+elX% ipa-crlgen-manage disable
+Stopping pki-tomcatd
+Editing /var/lib/pki/pki-tomcat/conf/ca/CS.cfg
+Starting pki-tomcatd
+Editing /etc/httpd/conf.d/ipa-pki-proxy.conf
+Restarting httpd
+CRL generation disabled on the local host. Please make sure to configure CRL generation on another master with ipa-crlgen-manage enable.
+The ipa-crlgen-manage command was successful
+
+elX% ipa-crlgen-manage status
+CRL generation: disabled
+
+

Create a test user to ensure DNA range is adjusted and replication is +working

+
elY% ipa user-add --first=testing --last=user testinguser1
+
+# Test on both systems
+elX% ipa user-find testinguser1
+elY% ipa user-find testinguser1
+
+

Verify DNA range.

+
# There should be ranges for both replicas
+% ipa-replica-manage dnarange-show
+elX.example.com: ...
+elY.example.com: ...
+
+

Stop old Enterprise Linux IPA services, remove replica, uninstall.

+
# Stop all elX services
+elX% ipactl stop
+
+# Delete the elX system from the topology
+elY% ipa server-del elX.example.com
+
+# Uninstall and/or power down system
+elX% ipa-server-install --uninstall
+elX% init 0
+
+

See this page +for more information.

+

Active Directory Trust

+

To initiate a trust with your active directory domain, ensure the +following requirements are met.

+
+

Requirements

+

Package installed: ipa-server-trust-ad

+

DNS: Properly configured that FreeIPA can resolve the AD servers A and +SRV records

+

This can either be forwarders to AD, a subdomain that IPA manages, or +delegated subdomain from the master DNS servers in your network. This is +completely dependent on your infrastructure.

+

DNS: AD forest has sites and SRV records, including priorities, are set +correctly

+
+

When the following requirements are met, you have two choices before +continuning. You can either use POSIX or have the id range generated +automatically.

+
+

POSIX vs Non-POSIX

+

If you decide to use POSIX, your AD users are expected to have +uidNumber, gidNumber, loginShell, unixHomeDirectory set. Else, you will +need to setup ID overrides if you already have that information for +current users (assuming this is not a new setup for the environment, ie +you already have UID's for people). If you are not planning a migration +from pure AD over to IPA with a trust, it is recommended to note that +information so you can setup the ID overrides. Afterwards, any new users +will get UID/GID's that you will not have to manage yourself.

+
+

You will need to prep your master(s) for the trust. We will be enabling +compat, adding sids, and adding agents so both masters can provide AD +information.

+
% ipa-adtrust-install --add-sids --add-agents --enable-compat
+
+

This will do what we need. If you do not have legacy clients (Enterprise +Linux 5, Solaris, HP-UX, AIX, SLES 11.4, FreeBSD, the list goes on), then you do +not need to enable compat mode. Though, it could be useful to have it for +certain apps or scenarios.

+

You will now need to open the necessary ports. Do this on all masters.

+
+

Ports

+

TCP: 135, 138, 139, 389, 445, 1024-1300, 3268 UDP: 138, 139, 389, 445

+
+
% firewall-cmd --add-service=freeipa-trust --permanent
+% firewall-cmd --complete-reload
+
+

Now you can initiate the trust. The admin account you use should be part +of the domain admins group or at least have permissions to initiate a +trust. The former is path of least resistance.

+
# If you are using POSIX ID, use ipa-ad-trust-posix.
+% ipa trust-add --type=ad example.com --range-type=ipa-ad-trust --admin adminaccount --password 
+
+

Once the trust is up, verify it.

+
% ipa trust-show example.com
+ Realm name: example.com
+ Domain NetBIOS name: AD
+ Domain Security Identifier: S-X-X-XX-XXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX
+ Trust direction: Trusting forest
+ Trust type: Active Directory domain
+ UPN suffixes: example.com
+
+

You should be able to test for the users now.

+
% id aduser1@example.com
+uid=XXXXX(aduser1@example.com) gid=XXXXX(aduser1@example.com) groups=XXXXX(aduser1@example.com)
+
+

Disable Anonymous Bind

+

In some cases, it is a requirement to disable all anonymous binds. If +this is the case, you will need to modify cn=config on each master as it +is not replicated.

+
+

rootdse

+

Some applications do anonymous binds to the directory server to +determine its version and it supported controls. While it is possible to +disable anonymous binds completely, it is important to know that if you +disable the rootdse binds, applications that do anonymous lookups to get +server information will fail.

+
+
% ldapmodify -xZZ -D "cn=Directory Manager" -W -h server.ipa.example.com
+Enter LDAP Password:
+dn: cn=config
+changetype: modify
+replace: nsslapd-allow-anonymous-access
+nsslapd-allow-anonymous-access: rootdse
+
+modifying entry "cn=config"
+
+

Client Setup

+

Enterprise Linux & Fedora

+

Ensure your /etc/resolv.conf (or other dns settings) are set correctly. +Ensure your hostname is also set correctly.

+
% dnf install ipa-client -y
+% ipa-client-install --realm EXAMPLE.COM --domain example.com --mkhomedir
+
+

Mac Clients

+

MacOS Clients are an interesting workstation to setup as a FreeIPA +client. It takes a little bit of fighting and troubleshooting, but it +can work with the right settings. Note that as of Catalina, you may +not be able to login to your account nor will creating a mobile account +function as you would expect. This may have changed in recent macos +releases, so YMMV.

+
+

Other Guides

+

There are a couple of guides out there that you may have found before +(if you looked) that help setup IPA for Mac. There's one for much older +(I think Lion) and one for Sierra. This section was made mostly for my +own reference because I found some things in both of those guides +didn't address issues I ran into one way or another and couldn't find +any information on. The FreeIPA users mail list didn't have any +archives with people having similar issues.

+

If you are interested in the other guides to compare to, you may see +them here (recent) +and here (older)

+
+
+

AD Users

+

AD Users

+

You cannot login as AD users on a Mac when going through FreeIPA. You +can, in theory, point to the cn=compat tree and set the attribute +mapping to rfc2307. In my tests, I have never been able to get this to +work. This section, I am going to assume you are going to be logging in +as a user in IPA. If you are in a mixed environment, add your Mac to +your AD domain instead.

+
+
+

Anonymous Bind

+

There may be cases where if you have disabled anonymous binds in IPA, +this setup may not work, even if you do use a bind account. You will +need to experiment with this if you plan on using a bind account and +plan on or currently have IPA not allowing anonymous binds.

+
+

Check your system's hostname. You want to make sure it has a hostname +defined for it in the domain the mac sits in, even if it's dynamic via +DHCP/DNS.

+
% sudo scutil --set HostName mac.example.com
+
+

Get the IPA certificate. You'll need to double click it after you get +it and import it.

+
% cd ~/Desktop && curl -OL http://server1.ipa.example.com/ipa/config/ca.crt
+% sudo mkdir /etc/ipa
+% sudo cp ca.crt /etc/ipa/ca.crt
+% sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /etc/ipa/ca.crt
+
+

On the IPA server, you will need to create a host and get the keytab.

+
% ipa host-add mac.example.com --macaddress="00:00:00:00:00:00"
+% ipa-getkeytab -s server1.ipa.example.com -p host/mac.example.com -k /tmp/krb5.keytab
+
+

You will need to transfer that keytab to your mac.

+
% cd ~
+% scp user@server1.ipa.example.com:/tmp/krb5.keytab .
+% sudo mv krb5.keytab /etc/krb5.keytab
+% sudo chmod 600 /etc/krb5.keytab
+% sudo chown root:wheel /etc/krb5.keytab
+
+

Configure /etc/krb5.conf

+
[domain_realm]
+    .ipa.example.com = IPA.EXAMPLE.COM
+    ipa.example.com = IPA.EXAMPLE.COM
+
+[libdefaults]
+    default_realm = IPA.EXAMPLE.COM
+    allow_weak_crypto = yes 
+    dns_lookup_realm = true
+    dns_lookup_kdc = true
+    rdns = false
+    ticket_lifetime = 24h
+    forwardable = yes 
+    renewable = true
+
+[realms]
+    IPA.EXAMPLE.COM = {
+        # You don't need to set these when your DNS is setup correctly, but it doesn't hurt to have a reference.
+        # In my opinion, you shouldn't hardcode these values. You have to have a good reason to.
+        #kdc = tcp/server1.ipa.example.com
+        #kdc = tcp/server2.ipa.example.com
+        #admin_server = tcp/server1.ipa.example.com
+        #admin_server = tcp/server2.ipa.example.com
+        pkinit_anchors = FILE:/etc/ipa/ca.crt
+    }
+
+

You'll want to do a kinit to verify. If it works, you should be able to +go to the FreeIPA webui and check that the host is "enrolled" +(Identity -> Hosts).

+
% kinit username@IPA.EXAMPLE.COM
+
+

You need to modify a couple of pam files. I'll explain why they need to +be changed.

+
% sudo vi /etc/pam.d/authorization
+# authorization: auth account
+# Putting krb5 here twice ensures that you can login via kerberos and also get a keytab
+# If "no_ccache" is here, keytabs will not be available on login
+auth          optional       pam_krb5.so use_first_pass use_kcminit default_principal
+auth          sufficient     pam_krb5.so use_first_pass default_principal
+auth          required       pam_opendirectory.so use_first_pass nullok
+account    required       pam_opendirectory.so
+
+% sudo vi /etc/pam.d/screensaver
+# The krb5 changes do similar to the authorization when on the lock screen after a sleep
+#auth       optional       pam_krb5.so use_first_pass use_kcminit
+auth       optional       pam_krb5.so use_first_pass use_kcminit default_principal
+auth       sufficient     pam_krb5.so use_first_pass default_principal
+auth       required       pam_opendirectory.so use_first_pass nullok
+account    required       pam_opendirectory.so
+account    sufficient     pam_self.so
+account    required       pam_group.so no_warn group=admin,wheel fail_safe
+account    required       pam_group.so no_warn deny group=admin,wheel ruser fail_safe
+
+% sudo vi /etc/pam.d/passwd
+# Helps with kerberos logins
+password   sufficient     pam_krb5.so
+auth       required       pam_permit.so
+account    required       pam_opendirectory.so
+password   required       pam_opendirectory.so
+session    required       pam_permit.so 
+
+

After these changes, you'll need to go into make some changes with the +directory utility. This depends on your macOS version.

+

Monterey and older

+
    +
  1. Go to system preferences -> users & groups -> login options - + Click the 'lock' to make changes
  2. +
  3. Set the following:
  4. +
+
Automatic login: Off
+Display login window as: Name and Password
+Show fast user switching menu as: Full Name
+
+
    +
  1. Click "Join" next to "Network Account Server"
  2. +
  3. Enter one of your IPA servers (you can duplicate it later for backup + purposes) and click Continue.
  4. +
  5. Ensure "Allow network users to log in at login window" is + checked - Make sure it's set to all users
  6. +
  7. Click "edit" next to the "Network Account Server"
  8. +
  9. Click "Open Directory Utility"
  10. +
  11. Click the lock, edit LDAPv3
  12. +
  13. Select your server and click "edit"
  14. +
  15. Set the following options:
  16. +
+
Open/close times out in 5 seconds
+Query times out in 5 seconds
+Connection idles out in 1 minute (this can't be changed)
+Encrypt using SSL (selected)
+
+
    +
  1. Click "Search & Mappings"
  2. +
  3. +

    You may either select "rfc2307" from the dropdown or select + custom. It will ask your base DN (eg, dc=ipa,dc=example,dc=com)

    +
  4. +
  5. +

    If you select rfc2307, it will ask for your base DN (eg, + dc=ipa,dc=example,dc=com)

    +
  6. +
  7. +

    If you select "custom", you will need to do this manually for each + record type. You're better off using rfc2307 and working from + there

    +
  8. +
  9. +

    Click the "+" to add a groups record type or scroll and find + "groups".

    +
  10. +
  11. Select "groups", and ensure the following object classes exist. + You can click the "+" to add them when needed.
  12. +
+
+

Record Type ObjectClasses

+
+

Groups posixGroup

+
                         ipausergroup
+
+                         groupOfNames\*
+
+
+
+

Note

+

"groupOfNames" is optional here, because it seems that the directory +utility doesn't understand this concept.

+
+
    +
  1. Expand "groups" and ensure the following for each record type. You + can click the "+" to add the attribute types as needed.
  2. +
+
+

Attribute Mapping

+
+

PrimaryGroupID gidNumber

+

RecordName cn

+
+
    +
  1. Click the "+" to add a users record type or scroll and find + "users".
  2. +
  3. Select "users" and ensure the following object classes exist. You + can click the "+" to add them when needed.
  4. +
+
+

Record Type ObjectClasses

+
+

Users inetOrgPerson

+
                         posixAccount
+
+                         shadowAccount
+
+                         apple-user
+
+
+
    +
  1. Expand "users" and ensure the following for each record type. You + can click the "+" to add the attribute types as needed. Do not + set homeDirectory otherwise you will fail to login.
  2. +
+
+

Attribute Mapping

+
+

AuthenticationAuthority uid

+

GeneratedUID GeneratedUID or ipaUniqueID

+

HomeDirectory #/Users/\$uid\$

+

NFSHomeDirectory #/Users/\$uid\$

+

PrimaryGroupID gidNumber

+

RealName cn

+

RecordName uid

+

UniqueID uidNumber

+

UserShell loginShell

+

AltSecurityIdentities #Kerberos:\$krbPrincipalName\$

+
+
    +
  1. If using custom mapping, click reach record type you created and + ensure the base DN is set.
  2. +
  3. Make sure each record type is set to all subtrees.
  4. +
  5. Click "security" and set an authentication bind DN if needed
  6. +
  7. Click OK
  8. +
  9. Click OK
  10. +
  11. Click on Search Policy.
  12. +
  13. Double check that "/LDAPV3/server1.ipa.example.com" is listed + beneath "/Local/Default"
  14. +
  15. Close everything until you're back to the users & groups section of + preferences
  16. +
  17. Open a terminal.
  18. +
+
% dscacheutil -flushcache
+% dscacheutil -q user -a name username
+
+

You should get a return.

+

If you want to further verify users and groups after the above succeeds, +open up the directory utility again. Click "Directory Editor", ensure +you are searching for "users" and check that they appear in a list on +the right hand side, optionally doing a search. In a default setup, you +shouldn't need an account to do (some) anonymous lookups. If you +changed that in any way, you will need to create a readonly system +account in cn=sysaccounts,cn=etc.

+

Login to the account for the first time from the login screen. Once the +setup has complete, log out and back to a login account. In a terminal, +you will need to make a mobile account.2

+
% sudo /System/Library/CoreServices/ManagedClient.app/Contents/Resources/createmobileaccount -n username -P
+# Press enter and put in the password. sudo may not function if you don't do this step.
+# OPTIONAL: Allow the mobile account to be an administrator
+% sudo dscl . -append /Groups/admin GroupMembership username
+
+

Go to system preferences, users & groups and ensure the account is a +mobile account.

+

Ventura and likely newer

+
    +
  1. Go to system preferences -> users & groups
  2. +
  3. Set "automatic login" to "off"
  4. +
  5. Click "edit" next to "Network account server"
  6. +
  7. Type in one of your IPA servers (you can duplicate it later for + backup purposes). Press enter and wait for it to be "green".
  8. +
  9. Click "Open Directory Utility"
  10. +
  11. Click the "lock" to unlock the utility
  12. +
  13. Click "LDAPv3" and click the pencil at the bottom left corner
  14. +
  15. +

    Select the "from server" portion under LDAP mappings and clck + RFC2307. You may also leave it as custom.

    +
  16. +
  17. +

    If you select rfc2307, it will ask for your base DN (eg, + dc=ipa,dc=example,dc=com)

    +
  18. +
  19. +

    If you select "custom", you will need to do this manually for each + record type. You're better off using rfc2307 and working from + there

    +
  20. +
  21. +

    Click "edit"

    +
  22. +
  23. Click the "+" to add a groups record type or scroll and find + "groups" and select it. Add the following object classes
  24. +
+
+

Record Type ObjectClasses

+
+

Groups posixGroup

+
                         ipausergroup
+
+                         groupOfNames\*
+
+
+
+

Note

+

"groupOfNames" is optional here, because it seems that the directory +utility doesn't understand this concept.

+
+
    +
  1. Expand "groups" and ensure the following for each record type. You + can click the "+" to add the attribute types as needed.
  2. +
+
+

Attribute Mapping

+
+

PrimaryGroupID gidNumber

+

RecordName cn

+
+
    +
  1. Click the "+" to add a users record type or scroll and find + "users".
  2. +
  3. Select "users" and ensure the following object classes exist. You + can click the "+" to add them when needed.
  4. +
+
+

Record Type ObjectClasses

+
+

Users inetOrgPerson

+
                         posixAccount
+
+                         shadowAccount
+
+                         apple-user
+
+
+
    +
  1. Expand "users" and ensure the following for each record type. You + can click the "+" to add the attribute types as needed. Do not + set homeDirectory otherwise you will fail to login.
  2. +
+
+

Attribute Mapping

+
+

AuthenticationAuthority uid

+

GeneratedUID GeneratedUID or ipaUniqueID

+

NFSHomeDirectory #/Users/\$uid\$

+

PrimaryGroupID gidNumber

+

RealName cn

+

RecordName uid

+

UniqueID uidNumber

+

UserShell loginShell

+

AltSecurityIdentities #Kerberos:\$krbPrincipalName\$

+
+
    +
  1. If using custom mapping, click reach record type you created and + ensure the base DN is set.
  2. +
  3. Make sure each record type is set to all subtrees if needed.
  4. +
  5. Click "security" and set an authentication bind DN if needed
  6. +
  7. Click OK.
  8. +
  9. Click Search Policy
  10. +
  11. Double check that "/LDAPV3/server1.ipa.example.com" is listed + beneath "/Local/Default". If it is not, select "search patch" + and set it to custom and add it. Click Apply after.
  12. +
  13. Close everything until you're back to the users & groups section of + preferences
  14. +
  15. Go to Lock Screen.
  16. +
  17. Set "login window shows" to "name and password"
  18. +
  19. Open a terminal.
  20. +
+
% dscacheutil -flushcache
+% dscacheutil -q user -a name username
+
+

You should get a return.

+

Login to the account for the first time from the login screen. Once the +setup has complete, log out and back to a login account. In a terminal, +you will need to make a mobile account.3

+
% sudo /System/Library/CoreServices/ManagedClient.app/Contents/Resources/createmobileaccount -n username -P
+# Press enter, enter the user's password. sudo may hang if you don't do this.
+# OPTIONAL: Allow the mobile account to be an administrator
+% sudo dscl . -append /Groups/admin GroupMembership username
+
+

Go to system preferences and ensure the account is a mobile account.

+

General macOS Notes

+
+

Group Resolution

+

If you want groups from IPA to resolve to the system, you'll need to +enable the compat tree when using this setup (RFC2307).

+
+
+

Password Notes

+

There are a couple of potential issues with this setup that you should +be aware of as it pertains to mobile accounts.

+
    +
  • If you do a mobile account, changing your password through the + FreeIPA gui does not change your passwords on your system.
  • +
  • If your account does not have any keytabs (eg, you haven't had your + mac on or haven't logged in in over 24 hours), you can login with + the new password and it will suceed. The system will cache the new + password right away. However, your keychain the first time will ask + for the old passwords and this is normal. So you can change them by + hand or you can log out and back in and the system will ask you if + you want to update the password and it will just update + automatically.
  • +
  • There have been reports in a github issue that states you can change + the password in the system preferences but I've been unable to + confirm this.
  • +
+
+

Below is a script that can be adapted for you. It has not been tested on +Monterey and up. This assumes that you took one mac and set it up +properly and you created a tarball with the proper configuration. You +could optionally setup a temporary NFS or samba mount that gets mounted +as root and then unmounted at the end, if you so wish.

+
#!/bin/bash
+serverName=server1.ipa.example.com
+krb5Conf=/etc/krb5.conf
+krb5Tab=/etc/krb5.keytab
+pamDirectory=/etc/pam.d
+
+# Add SSL cert to chain
+mkdir /etc/ipa
+cd /etc/ipa
+curl -OL http://$serverName/ipa/config/ca.crt
+security add-trusted-cert -d -k /Library/Keychains/System.keychain -r trustRoot /etc/ipa/ca.crt
+
+# Stop and flushout the Open Directory
+/usr/sbin/dscacheutil -flushcache
+launchctl unload /System/Library/LaunchDaemons/com.apple.opendirectoryd.plist
+
+# Pull the plist and pam files needed for IPA and deploy them, this assumes you setup one mac and zipped up the configurations
+# You can try your hand at dsconfigldap before pam, but I could never figure it out, honestly.
+# Relevant tar: tar czf /tmp/macconfig.tar.gz /Library/Preferences/OpenDirectory/Configurations /etc/pam.d/authorization \ 
+#                /etc/pam.d/screensaver /etc/pam.d/passwd /etc/krb5.conf
+cd /tmp
+curl -OL http://$serverName/macconfig.tar.gz
+cd /
+tar xzf /tmp/macconfig.tar.gz
+
+# Add steps here for your keytab! Where are you getting it from?
+cp /tmp/mac.keytab /etc/krb5.keytab
+chown root:wheel /etc/krb5.keytab
+chmod 600 /etc/krb5.keytab
+
+# Start directory
+launchctl load /System/Library/LaunchDaemons/com.apple.opendirectoryd.plist
+sleep 30
+
+# Kill the loginwindow
+killall loginwindow
+
+# If the system doesn't reboot here, reboot now.
+
+

If you want to move your local files, you will need to tread lightly +here. I personally believe it's always good to start fresh though. Look +into the ditto command. I suppose something like this can work:

+
# make sure you're logged in as a different account away from your local account
+% sudo su -
+root# cd /Users
+root# ditto localfolder networkfolder (or maybe an mv?)
+root# chown -R user:user folder
+root# /System/Library/CoreServices/ManagedClient.app/Contents/Resources/createmobileaccount -n username -P
+
+

Another issue you may run into, if you have been using your Mac with a +local account for a while, a lot of directories in /Applications will be +owned by localuser:staff or localuser:admin. It's recommended to fix +those too.

+
+

Discovery

+

The directory framework in MacOS has the ability to discover settings +for a particular LDAP server that it is being connected to. FreeIPA does +not contain the schema, plugins, nor the infrastructure to provide the +same things (for example, mDNS/Avahi, among other things). There was a +(WIP) plugin created in 2017 by abbra. However, it is unclear if this +works at all, nor is it clear if it ever did and will in python3 (abbra +noted at the time that it "installs" into python 2 directories, which +hints to not being tested or working on python 3). Please see the +following resources for discussion and information.

+ +
+

SUSE

+

To setup openSUSE with FreeIPA, we'll need to do some manual work. This +applies to SUSE 12 and up where the freeipa-client packages don't exist +in the main repositories.

+
+

freeipa repos

+

There are OpenSUSE repos with the freeipa packages, though they are +considered "experimental". If they show up in the base, then the below +steps will be removed. However, if you are willing to use the +repo, +a lot of the steps below may not be needed. We have not tested this.

+
+
# On an IPA server or client with the IPA utilities...
+% ipa host-add suse.example.com
+% /usr/sbin/ipa-getkeytab -s ipa.example.com -p host/suse.example.com -k /tmp/suse.keytab
+% scp /tmp/suse.keytab suse.example.com:/tmp/krb5.keytab
+
+# On the IPA client...
+% cp /tmp/krb5.keytab /etc
+% chmod 600 /etc/krb5.keytab
+% mkdir /etc/ipa
+% curl -o /etc/ipa/ca.crt http://ipa.example.com/ipa/config/ca.crt
+% curl -o /etc/pki/trust/anchors/ipa.example.com.crt http://ipa.example.com/ipa/config/ca.crt
+% update-ca-certificates
+% zypper install sssd sssd-ipa yast2-auth-client krb5-client openldap2-client cyrus-sasl-gssapi
+
+# Setup SSSD
+% vi /etc/sssd/sssd.conf
+[domain/example.com]
+cache_credentials = True
+krb5_store_password_if_offline = True
+ipa_domain = example.com
+ipa_hostname = suse.example.com
+# Client Specific Settings
+ipa_server = _srv_, ipa.example.com
+dns_discovery_domain = example.com
+# If we have a trust with domain resolution order
+#full_name_format = %1$s
+
+id_provider = ipa
+auth_provider = ipa
+access_provider = ipa
+chpass_provider = ipa
+
+ldap_tls_cacert = /etc/ipa/ca.crt
+
+[sssd]
+services = nss, sudo, pam, ssh
+domains = example.com
+
+[nss]
+filter_users = root,ldap,named,avahi,haldaemon,dbus,radiusd,news,nscd,tomcat,postgres
+homedir_substring = /home
+
+[pam]
+
+[sudo]
+
+[autofs]
+
+[ssh]
+
+# Setup kerberos
+% vi /etc/krb5.conf
+[libdefaults]
+  default_realm = EXAMPLE.COM
+  dns_lookup_realm = true
+  dns_lookup_kdc = true
+  rdns = false
+  dns_canonicalize_hostname = false
+  ticket_lifetime = 24h
+  forwardable = true
+  udp_preference_limit = 0
+  default_ccache_name = KEYRING:persistent:%{uid}
+
+
+[realms]
+  EXAMPLE.COM = {
+    pkinit_anchors = FILE:/var/lib/ipa-client/pki/kdc-ca-bundle.pem
+    pkinit_pool = FILE:/var/lib/ipa-client/pki/ca-bundle.pem
+  }
+
+[domain_realm]
+  .example.com = EXAMPLE.COM
+  example.com = EXAMPLE.COM
+  suse.example.com = EXAMPLE.COM
+
+# Setup pam
+% pam-config -a --sss --mkhomedir --mkhomedir-umask=0077 \
+  --pwhistory --pwhistory-remember=5 --localuser --cracklib \
+  --cracklib-minlen=14 --cracklib-dcredit=-1 --cracklib-ucredit=-1 \
+  --cracklib-lcredit=-1 --cracklib-ocredit=-1 --cracklib-retry=3 --unix-sha512
+
+# Setup nsswitch (you can make it compat sss, but I use files sss)
+% sed -i.bak 's/compat$/files sss/g' /etc/nsswitch.conf
+% echo "sudoers: files sss" >> /etc/nsswitch.conf
+% sed -i '/netgroup/ s/nis/sss/g' /etc/nsswitch.conf
+
+# Depending on your suse version, you may want to set the nisdomainname
+# It does not hurt to set this
+% sed -i.bak '/NETCONFIG_NIS_STATIC_DOMAIN/ s/""/"example.com"/g' /etc/sysconfig/network/config
+% netconfig update -f
+
+# Start sssd
+% systemctl enable sssd --now
+
+# Verify
+% id admin
+
+

In the case of having an IPA-AD trust, you may need to change a line in +your pam configuration.

+
% sed -i 's/use_first_pass/forward_pass/g' /etc/pam.d/common-auth-pc
+
+# The affected line should appear like the below
+auth    sufficient      pam_sss.so      forward_pass
+
+

HBAC

+

When we first setup our IPA servers, we had an option set to make it so +hbac wasn't allowed for everyone. This way we have to create HBAC rules +for our systems. I personally do this out of habit when working with +IPA. What we need to do though is create an "admin" group that can +login to all machines.

+
% ipa idrange-show IPA.EXAMPLE.COM_id_range
+  Range name: IPA.EXAMPLE.COM_id_range
+  First Posix ID of the range: 686600000
+  Number of IDs in the range: 200000
+  First RID of the corresponding RID range: 1000
+  First RID of the secondary RID range: 100000000
+  Range type: local domain range
+% ipa group-add --gid=686610000 linuxadm
+% ipa group-add-member --users=flast linuxadm
+
+

Note for AD Users: In the event that your AD user or group of users +will be an admin, you need to create an "external" group to map the +user or users over. This isn't required if you don't have an AD trust.

+
# Create an external group that the AD user/group goes into
+% ipa group-add --external linuxadm_external
+# Add the user (or group) into the external group
+% ipa group-add-member --users=aduser1@example.com linuxadm_external
+% ipa group-add-member --users=adgroup1@example.com linuxadm_external
+# Add the external group as a member of the IPA posix group.
+# aduser1 and adgroup1 are now effectively members of the linuxadm group in IPA.
+% ipa group-add-member --groups=linuxadm_external linuxadm
+
+

Now, let's create an HBAC for our Linux Administrator account for our +group.

+
% ipa hbacrule-add --hostcat=all --servicecat=all --desc='linux admins all access' linuxadm
+% ipa hbacrule-add-user --groups=linuxadm linuxadm
+% ipa hbactest --rules=All_Systems --user=flast --host=server1.ipa.example.com --service=sshd
+% ipa hbactest --rules=All_Systems --user=aduser1@example.com --host=server1.ipa.example.com --service=sshd
+
+

You might want to create an HBAC rule specifically for your IPA admin +accounts to have ssh access to the IPA servers too. You can follow +something like the above to make it possible. Or you can just add the +IPA admins group into the HBAC rule we just made above.

+
+

Group Types

+

Groups in Active Directory have three types. These three types can +actually change the behavior of how SSSD on the IPA domain controllers +resolve them or if they'll even be resolvable at all. The three types +are 'Domain Local', 'Global', and 'Universal'. If at all possible, +avoid groups being 'Global'. Domain Local or Universal is recommended.

+
+

SUDO

+

Setting up sudo is relatively easy. SSSD (1.16.x and 2.X) supports IPA +as a provider for sudo. Based on the last section, let's create a +sample rule for our Linux admins that can login to every system, we want +to ensure they can run all commands.

+
% ipa sudorule-add --runasusercat=all --hostcat=all --cmdcat=all --desc='linux admins all sudo' all_linux_sudo
+% ipa sudorule-add-user --groups=linuxadm all_linux_sudo
+
+

You can make this a little more specific, such as /bin/bash as everyone +or otherwise. It's your call here. If you want to create a sudo rule +and add some commands to it, you can do something like this.

+
% ipa sudorule-add sudo_rule
+% ipa sudorule-add-allow-command --sudocmds="/usr/bin/less" sudo_rule
+
+

Legacy Client Setup

+

This applies to Solaris, Omnios, others based on Illumos.

+

Solaris 10

+

Setting up Solaris 10 as an IPA client is an interesting feat. However, +it comes with security issues.

+
+

No SSL or TLS Support

+

Note that for Solaris 10 to talk to IPA, you must use clear text +communication. Solaris 10 is too old to use new ciphers. However, while +LDAP may be clear text, kerberos should still be secure enough for the +time being.

+

If you are using an AD trust, the user's passwords will be passed in +clear text. Highly suggested that you decommission Solaris 10 from your +environment. Solaris 10 will eventually be removed from this page.

+
+

Create an ldif for your service account (optional)

+
dn: uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com
+objectclass: account
+objectclass: simplesecurityobject
+uid: solaris
+userPassword: secret123
+passwordExpirationTime: 20380119031407Z
+nsIdleTimeout: 0
+
+

The solaris system account is required. So now, add it in.

+
% ldapadd -xWD 'cn=Directory Manager' -f /tmp/solaris.ldif
+
+

Now, set the nisdomain.

+
% defaultdomain ipa.example.com
+% echo 'ipa.example.com' > /etc/defaultdomain
+
+

Configure kerberos.

+
% vi /etc/krb5/krb5.conf
+[libdefaults]
+default_realm = IPA.EXAMPLE.COM
+dns_lookup_kdc = true
+verify_ap_req_nofail = false
+
+[realms]
+IPA.EXAMPLE.COM = {
+}
+
+[domain_realm]
+ipa.example.com = IPA.EXAMPLE.COM
+.ipa.example.com = IPA.EXAMPLE.COM
+
+[logging]
+default = FILE:/var/krb5/kdc.log
+kdc = FILE:/var/krb5/kdc.log
+kdc_rotate = {
+ period = 1d
+ version = 10
+}
+
+[appdefaults]
+kinit = {
+renewable = true
+forwardable= true
+}
+
+

Generate a keytab and bring it over.

+
# on the ipa server
+% ipa host-add solaris10.example.com
+% ipa-getkeytab -s server1.ipa.example.com -p host/solaris10.example.com -k /tmp/solaris10.keytab
+
+# Transfer the keytab
+% scp /tmp/solaris10.keytab solaris10.example.com:/tmp
+
+# On the solaris 10 machine
+% cp /tmp/solaris10.keytab /etc/krb5/krb5.keytab
+% chmod 600 /etc/krb5/krb5.keytab
+% chmod 644 /etc/krb5/krb5.conf
+% chown root:sys /etc/krb5/*
+% kinit flast2@IPA.EXAMPLE.COM
+
+

Create the LDAP configurations, bring the certificate, and create an NSS +database.

+
% mkdir /etc/ipa /var/ldap
+% cd /etc/ipa
+% wget -O ipa.pem http://server1.ipa.example.com/ipa/config/ca.crt
+% certutil -A -n "ca-cert" -i /etc/ipa/ipa.pem -a -t CT -d .
+% cp * /var/ldap
+% vi /etc/ldap.conf
+base dc=ipa,dc=example,dc=com
+scope sub
+TLS_CACERTDIR /var/ldap
+TLS_CERT /var/ldap/cert8.db
+TLS_CACERT /var/ldap/ipa.pem
+tls_checkpeer no
+ssl off
+bind_timelimit 120
+timelimit 120
+uri ldap://server1.ipa.example.com
+sudoers_base ou=sudoers,dc=ipa,dc=example,dc=com
+pam_lookup_policy yes
+
+

Now init the ldap client.

+
+

No Secure Connection

+

When using this, you are not creating a secure connection. The Solaris +10 SSL libraries are so old that they cannot work with the ciphers that +FreeIPA has turned on.

+
+
+

AD Trust - Different Trees

+

If using an AD trust, you should use the second example, where it looks +at the compat tree for users.

+
+
+

No Service Account

+

If you have configured FreeIPA to not allow any anonymous connections, +you will need to use a proxy account. We have provided the examples for +this configuration.

+
+

Without an AD Trust

+
# Without AD Trust (no proxy)
+% ldapclient manual -a authenticationMethod=none \
+                    -a defaultSearchBase=dc=ipa,dc=example,dc=com \
+                    -a domainName=ipa.example.com \
+                    -a defaultServerList="server1.ipa.example.com server2.ipa.example.com" \
+                    -a followReferrals=true \
+                    -a objectClassMap=shadow:shadowAccount=posixAccount \
+                    -a objectClassMap=passwd:posixAccount=posixaccount \
+                    -a objectClassMap=group:posixGroup=posixgroup \
+                    -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=passwd:cn=users,cn=accounts,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \
+                    -a bindTimeLimit=5
+
+# Without AD Trust (proxy)
+% ldapclient manual -a credentialLevel=proxy \
+                    -a authenticationMethod=simple \
+                    -a proxyDN="uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com" \
+                    -a proxyPassword="secret123" \
+                    -a defaultSearchBase=dc=ipa,dc=example,dc=com \
+                    -a domainName=ipa.example.com \
+                    -a defaultServerList="server1.ipa.example.com server2.ipa.example.com" \
+                    -a followReferrals=true \
+                    -a objectClassMap=shadow:shadowAccount=posixAccount \
+                    -a objectClassMap=passwd:posixAccount=posixaccount \
+                    -a objectClassMap=group:posixGroup=posixgroup \
+                    -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=passwd:cn=users,cn=accounts,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \
+                    -a bindTimeLimit=5
+
+

With an AD Trust

+
# With AD Trust (no proxy)
+% ldapclient manual -a authenticationMethod=none \
+                    -a defaultSearchBase=dc=ipa,dc=example,dc=com \
+                    -a domainName=ipa.example.com \
+                    -a defaultServerList="server1.ipa.example.com server2.ipa.example.com" \
+                    -a followReferrals=true \
+                    -a objectClassMap=shadow:shadowAccount=posixAccount \
+                    -a objectClassMap=passwd:posixAccount=posixaccount \
+                    -a objectClassMap=group:posixGroup=posixgroup \
+                    -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \
+                    -a bindTimeLimit=5
+
+# With AD Trust (proxy)
+% ldapclient manual -a credentialLevel=proxy \
+                    -a authenticationMethod=simple \
+                    -a proxyDN="uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com" \
+                    -a proxyPassword="secret123" \
+                    -a defaultSearchBase=dc=ipa,dc=example,dc=com \
+                    -a domainName=ipa.example.com \
+                    -a defaultServerList="server1.ipa.example.com server2.ipa.example.com" \
+                    -a followReferrals=true \
+                    -a objectClassMap=shadow:shadowAccount=posixAccount \
+                    -a objectClassMap=passwd:posixAccount=posixaccount \
+                    -a objectClassMap=group:posixGroup=posixgroup \
+                    -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \
+                    -a bindTimeLimit=5
+
+

This should succeed. Once it succeeds, you need to configure pam and +nsswitch.

+
+

AD Trust Information

+

In the event you don't have an AD trust, you can change the "binding" +lines to required, remove the pam_ldap lines, and change pam_krb5 +lines to read "required"

+
+
% vi /etc/pam.conf
+
+# Console
+login auth requisite    pam_authtok_get.so.1
+login auth sufficient   pam_krb5.so.1
+login auth required     pam_unix_cred.so.1
+login auth required     pam_dial_auth.so.1
+login auth sufficient   pam_unix_auth.so.1 server_policy
+login auth sufficient   pam_ldap.so.1
+
+rlogin auth sufficient  pam_rhosts_auth.so.1
+rlogin auth requisite   pam_authtok_get.so.1
+rlogin auth required    pam_dhkeys.so.1
+rlogin auth sufficient  pam_krb5.so.1
+rlogin auth required    pam_unix_cred.so.1
+rlogin auth sufficient  pam_unix_auth.so.1 server_policy
+rlogin auth sufficient  pam_ldap.so.1
+
+# Needed for krb
+krlogin auth required   pam_unix_cred.so.1
+krlogin auth sufficient pam_krb5.so.1
+
+# Needed for krb
+krsh auth required      pam_unix_cred.so.1
+krsh auth required      pam_krb5.so.1
+
+# ?
+ppp auth requisite      pam_authtok_get.so.1
+ppp auth required       pam_dhkeys.so.1
+ppp auth sufficient     pam_krb5.so.1
+ppp auth required       pam_dial_auth.so.1
+ppp auth binding        pam_unix_auth.so.1 server_policy
+ppp auth sufficient     pam_ldap.so.1
+
+# Other, used by sshd and "others" as a fallback
+other auth requisite    pam_authtok_get.so.1
+other auth required     pam_dhkeys.so.1
+other auth sufficient   pam_krb5.so.1
+other auth required     pam_unix_cred.so.1
+other auth sufficient   pam_unix_auth.so.1 server_policy
+other auth sufficient   pam_ldap.so.1
+other account requisite pam_roles.so.1
+other account required  pam_projects.so.1
+other account binding   pam_unix_account.so.1 server_policy
+other account sufficient pam_krb5.so.1
+other account sufficient pam_ldap.so.1
+other session required  pam_unix_session.so.1
+other password required pam_dhkeys.so.1
+other password requisite pam_authtok_get.so.1
+other password requisite pam_authtok_check.so.1 force_check
+other password required pam_authtok_store.so.1 server_policy
+
+# passwd and cron
+passwd auth binding    pam_passwd_auth.so.1 server_policy
+passwd auth sufficient pam_ldap.so.1
+cron account required  pam_unix_account.so.1
+
+# SSH Pubkey - Needed for openldap and still probably needed
+sshd-pubkey account required pam_unix_account.so.1
+
+
% vi /etc/nsswitch.conf
+
+# Below are just the minimum changes
+passwd:     files ldap [NOTFOUND=return]
+group:      files ldap [NOTFOUND=return]
+sudoers:    files ldap
+netgroup:   ldap
+# the rest here are just here, up to you if you choose to set them.
+hosts:      files dns
+ipnodes:    files dns
+ethers:     files ldap
+publickey:  files ldap
+automount:  files ldap
+
+

You can test now if you'd like.

+
bash-3.2# ldaplist -l passwd flast2
+dn: uid=flast2,cn=users,cn=compat,dc=ipa,dc=example,dc=com
+        cn: First Last
+        objectClass: posixAccount
+        objectClass: ipaOverrideTarget
+        objectClass: top
+        gidNumber: 1006800001
+        gecos: First Last
+        uidNumber: 1006800001
+        ipaAnchorUUID: :IPA:ipa.example.com:8babb9a8-5aaf-11e7-9769-00505690319e
+        loginShell: /bin/bash
+        homeDirectory: /home/first.last2
+        uid: first.last2
+
+

I recommend setting up sudo at least... if you want to use sudo, +install the sudo-ldap from sudo.ws for Solaris 10.

+

Solaris 11

+

Solaris 11 shares similar configuration to Solaris 10. There are a +couple of manual things we have to do, but they are trivial. Solaris +11/Omnios will use TLS and sudo should just work.

+
+

AD Groups

+

In Solaris 10, users who logged in with AD users (with their short name) +would appear as their full name (name@domain). This allowed their +groups to fully resolve. However, in Solaris 11.4, this was not the +case. Short name logins will work but your groups will not resolve as +the compat tree uses the full name. To avoid running into this problem, +you should be on at least SRU 11.4.7.4.0. Note that on a later SRU, you +may need to setup an ID view (without overrides) for groups and sudo to +work again.

+
+

Below is for the service account like in the previous section, here as a +reference.

+
dn: uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com
+objectclass: account
+objectclass: simplesecurityobject
+uid: solaris
+userPassword: secret123
+passwordExpirationTime: 20380119031407Z
+nsIdleTimeout: 0
+
+
% ldapadd -xWD 'cn=Directory Manager' -f /tmp/solaris.ldif
+
+

Now, set the nisdomain.

+
% defaultdomain ipa.example.com
+% echo 'ipa.example.com' > /etc/defaultdomain
+
+

Configure kerberos.

+
% vi /etc/krb5/krb5.conf
+[libdefaults]
+default_realm = IPA.EXAMPLE.COM
+dns_lookup_kdc = true
+verify_ap_req_nofail = false
+
+[realms]
+IPA.EXAMPLE.COM = {
+}
+
+[domain_realm]
+ipa.example.com = IPA.EXAMPLE.COM
+.ipa.example.com = IPA.EXAMPLE.COM
+
+[logging]
+default = FILE:/var/krb5/kdc.log
+kdc = FILE:/var/krb5/kdc.log
+kdc_rotate = {
+ period = 1d
+ version = 10
+}
+
+[appdefaults]
+kinit = {
+renewable = true
+forwardable= true
+}
+
+

Generate a keytab and bring it over.

+
# on the ipa server
+% ipa host-add solaris11.example.com
+% ipa-getkeytab -s server1.ipa.example.com -p host/solaris11.example.com -k /tmp/solaris11.keytab
+
+# Transfer the keytab
+% scp /tmp/solaris11.keytab solaris11.example.com:/tmp
+
+# On the solaris 11 machine
+% cp /tmp/solaris11.keytab /etc/krb5/krb5.keytab
+% chmod 600 /etc/krb5/krb5.keytab
+% chmod 644 /etc/krb5/krb5.conf
+% chown root:sys /etc/krb5/*
+
+# Check the keytab
+% klist -ket /etc/krb5/krb5.keytab
+
+# Test that you can kinit
+% kinit flast2@IPA.EXAMPLE.COM
+
+

Create the LDAP configurations, bring the certificate, and create an NSS +database.

+
+

Solaris 11.3 vs 11.4

+

Previously we had 11.3 and 11.4 configurations. We have removed 11.3 as +we no longer support it.

+
+
% mkdir /etc/ipa /var/ldap
+% cd /etc/ipa
+% wget -O ipa.pem http://server1.ipa.example.com/ipa/config/ca.crt
+% cp * /var/ldap
+% vi /etc/ldap.conf
+base dc=ipa,dc=example,dc=com
+scope sub
+bind_timelimit 120
+timelimit 120
+uri ldap://server1.ipa.example.com
+sudoers_base ou=sudoers,dc=ipa,dc=example,dc=com
+pam_lookup_policy yes
+TLS_CACERTDIR /var/ldap
+ssl start_tls
+tls_checkpeer no
+
+

Now init the ldap client. We actually get to use a secure connection +here. Kerberos is hit or miss, could never get sasl/GSSAPI to work.

+
+

Different Trees - Trust or not?

+

There are multiple examples of how to setup the trees. If using an AD +trust, you should use the second example, where it looks at the compat +tree for users. However, if you do not have trusts, then it is perfectly +possible to still use the AD Trust example. Try both and see which works +better for your environment.

+
+
+

No Service Account

+

If you have configured FreeIPA to not allow any anonymous connections, +you will need to use a proxy account. We have provided the examples for +this configuration.

+
+

Without AD Trust

+
# Without AD Trust (no proxy)
+% ldapclient manual -a authenticationMethod=tls:simple \
+                    -a defaultSearchBase=dc=ipa,dc=example,dc=com \
+                    -a domainName=ipa.example.com
+                    -a defaultServerList="server1.ipa.example.com server2.ipa.example.com" \
+                    -a followReferrals=true \
+                    -a objectClassMap=shadow:shadowAccount=posixAccount \
+                    -a objectClassMap=passwd:posixAccount=posixaccount \
+                    -a objectClassMap=group:posixGroup=posixgroup \
+                    -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=passwd:cn=users,cn=accounts,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \
+                    -a bindTimeLimit=5
+
+# Without AD Trust (proxy)
+% ldapclient manual -a authenticationMethod=tls:simple \
+                    -a credentialLevel=proxy \
+                    -a proxyDN="uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com" \
+                    -a proxyPassword="secret123" \
+                    -a defaultSearchBase=dc=ipa,dc=example,dc=com \
+                    -a domainName=ipa.example.com \
+                    -a defaultServerList="server1.ipa.example.com server2.ipa.example.com" \
+                    -a followReferrals=true \
+                    -a objectClassMap=shadow:shadowAccount=posixAccount \
+                    -a objectClassMap=passwd:posixAccount=posixaccount \
+                    -a objectClassMap=group:posixGroup=posixgroup \
+                    -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \
+                    -a bindTimeLimit=5
+
+# Without AD Trust (Kerberos) - Only works if Solaris is in the same DNS domain as IPA
+% ldapclient manual -a authenticationMethod=sasl/GSSAPI \
+                    -a credentialLevel=self \
+                    -a defaultSearchBase=dc=ipa,dc=example,dc=com \
+                    -a domainName=ipa.example.com \
+                    -a defaultServerList="server1.ipa.example.com server2.ipa.example.com" \
+                    -a followReferrals=true \
+                    -a objectClassMap=shadow:shadowAccount=posixAccount \
+                    -a objectClassMap=passwd:posixAccount=posixaccount \
+                    -a objectClassMap=group:posixGroup=posixgroup \
+                    -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \
+                    -a bindTimeLimit=5
+
+

With AD Trust

+
# With AD Trust (no proxy)
+% ldapclient manual -a authenticationMethod=tls:simple \
+                    -a defaultSearchBase=dc=ipa,dc=example,dc=com \
+                    -a domainName=ipa.example.com
+                    -a defaultServerList="server1.ipa.example.com server2.ipa.example.com" \
+                    -a followReferrals=true \
+                    -a objectClassMap=shadow:shadowAccount=posixAccount \
+                    -a objectClassMap=passwd:posixAccount=posixaccount \
+                    -a objectClassMap=group:posixGroup=posixgroup \
+                    -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \
+                    -a bindTimeLimit=5
+
+# With AD Trust (proxy)
+% ldapclient manual -a authenticationMethod=tls:simple \
+                    -a credentialLevel=proxy \
+                    -a proxyDN="uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com" \
+                    -a proxyPassword="secret123" \
+                    -a defaultSearchBase=dc=ipa,dc=example,dc=com \
+                    -a domainName=ipa.example.com \
+                    -a defaultServerList="server1.ipa.example.com server2.ipa.example.com" \
+                    -a followReferrals=true \
+                    -a objectClassMap=shadow:shadowAccount=posixAccount \
+                    -a objectClassMap=passwd:posixAccount=posixaccount \
+                    -a objectClassMap=group:posixGroup=posixgroup \
+                    -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \
+                    -a bindTimeLimit=5
+
+# With AD Trust (Kerberos) - Only works if Solaris is in the same DNS domain as IPA
+% ldapclient manual -a authenticationMethod=sasl/GSSAPI \
+                    -a credentialLevel=self \
+                    -a proxyDN="uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com" \
+                    -a proxyPassword="secret123" \
+                    -a defaultSearchBase=dc=ipa,dc=example,dc=com \
+                    -a domainName=ipa.example.com \
+                    -a defaultServerList="server1.ipa.example.com server2.ipa.example.com" \
+                    -a followReferrals=true \
+                    -a objectClassMap=shadow:shadowAccount=posixAccount \
+                    -a objectClassMap=passwd:posixAccount=posixaccount \
+                    -a objectClassMap=group:posixGroup=posixgroup \
+                    -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \
+                    -a bindTimeLimit=5
+
+

This should succeed. Once it succeeds, you need to configure pam and +nsswitch.

+
% /usr/sbin/svccfg -s name-service/switch setprop config/sudoer = astring: "files ldap"
+% /usr/sbin/svccfg -s name-service/switch setprop config/password = astring: "files ldap [NOTFOUND=return]"
+% /usr/sbin/svccfg -s name-service/switch setprop config/group = astring: "files ldap [NOTFOUND=return]"
+
+% /usr/sbin/svcadm refresh svc:/system/name-service/switch
+% /usr/sbin/svcadm restart svc:/system/name-service/switch
+% /usr/sbin/svcadm restart ldap/client
+
+
+

AD Trust Information

+

In the event you don't have an AD trust, you can change the "binding" +lines to required and remove the pam_ldap lines. Optionally, you can +set pam_krb5 to "required", however sufficient should work just fine.

+
+

Without an AD Trust

+
% vi /etc/pam.d/login
+auth definitive         pam_user_policy.so.1
+auth requisite          pam_authtok_get.so.1
+auth required           pam_dhkeys.so.1
+auth sufficient         pam_krb5.so.1
+auth required           pam_unix_cred.so.1
+auth sufficient         pam_unix_auth.so.1 server_policy
+
+% vi /etc/pam.d/other
+auth definitive         pam_user_policy.so.1
+auth requisite          pam_authtok_get.so.1
+auth required           pam_dhkeys.so.1
+auth sufficient         pam_krb5.so.1
+auth required           pam_unix_cred.so.1
+auth sufficient         pam_unix_auth.so.1 server_policy
+
+account requisite       pam_roles.so.1
+account definitive      pam_user_policy.so.1
+account required        pam_unix_account.so.1 server_policy
+account sufficient      pam_krb5.so.1
+
+session definitive      pam_user_policy.so.1
+session required        pam_unix_session.so.1
+
+password definitive     pam_user_policy.so.1
+password include        pam_authtok_common
+password sufficient     pam_krb5.so.1
+password required       pam_authtok_store.so.1 server_policy
+
+% vi /etc/pam.d/sshd-pubkey
+account required        pam_unix_account.so.1
+
+

With an AD Trust

+
% vi /etc/pam.d/login
+auth definitive         pam_user_policy.so.1
+auth requisite          pam_authtok_get.so.1
+auth required           pam_dhkeys.so.1
+auth sufficient         pam_krb5.so.1
+auth required           pam_unix_cred.so.1
+auth sufficient         pam_unix_auth.so.1 server_policy
+auth sufficient         pam_ldap.so.1
+
+% vi /etc/pam.d/other
+auth definitive         pam_user_policy.so.1
+auth requisite          pam_authtok_get.so.1
+auth required           pam_dhkeys.so.1
+auth sufficient         pam_krb5.so.1
+auth required           pam_unix_cred.so.1
+auth sufficient         pam_unix_auth.so.1 server_policy
+auth sufficient         pam_ldap.so.1
+
+account requisite       pam_roles.so.1
+account definitive      pam_user_policy.so.1
+account binding         pam_unix_account.so.1 server_policy
+account sufficient      pam_krb5.so.1
+account sufficient      pam_ldap.so.1
+
+session definitive      pam_user_policy.so.1
+session required        pam_unix_session.so.1
+
+password definitive     pam_user_policy.so.1
+password include        pam_authtok_common
+password sufficient     pam_krb5.so.1
+password required       pam_authtok_store.so.1 server_policy
+
+% vi /etc/pam.d/sshd-pubkey
+account required        pam_unix_account.so.1
+
+

You can test now if you'd like.

+
root@solaris11:~# ldaplist -l passwd flast2
+dn: uid=flast2,cn=users,cn=compat,dc=ipa,dc=example,dc=com
+        cn: First Last
+        objectClass: posixAccount
+        objectClass: ipaOverrideTarget
+        objectClass: top
+        gidNumber: 1006800001
+        gecos: First Last
+        uidNumber: 1006800001
+        ipaAnchorUUID: :IPA:ipa.example.com:8babb9a8-5aaf-11e7-9769-00505690319e
+        loginShell: /bin/bash
+        homeDirectory: /home/first.last2
+        uid: first.last2
+
+

Automated Scripts

+

I at one point built a bunch of scripts to automate Solaris servers +talking to IPA +here. +However, it is likely the scripts no longer work or contain outdated +information.

+

AD Trust Double UID

+

Solaris 11 once in a while gets random regressions when it comes to +authentication and ID's, among many other things they randomly decide +to break. Big shout out to Oracle.

+

In a brief discussion with a user in the #freeipa IRC channel, the user +was trying to find a way to chop off the domain name for logins but also +have sudo still work as there were some random issues in general. We +both discovered that in SRU 11.4.20.4.0, even though both UID's are +present from ldaplist -l passwd, sudo was no longer working properly. +The first thing we tried was to create an ID view and override a user +with a new username. This successfully removed the domain, but did not +solve the sudo problem. He instead got "no account present for that +user". However, I wasn't able to replicate this.

+

However, later, one thing he noticed is after creating an ID view with +no overrides and pointing Solaris 11 to the view in the compat tree, +Solaris 10-esque authentication ID reporting started to occur. Running +ldaplist -l passwd user reported back the double UID as expected, but +the FQDN comes first which resolved his group/sudo issues.

+
# Create a view... no id overrides required here
+% ipa idview-add solaris
+# On Solaris...
+# Take EXTREME care with the group and passwd base DN's, they need to point
+# to the view properly
+# This example uses kerberos to authenticate.
+% ldapclient manual -a authenticationMethod=self \
+                    -a credentialLevel=sasl/GSSAPI \
+                    -a defaultSearchBase=dc=ipa,dc=example,dc=com \
+                    -a domainName=ipa.example.com \
+                    -a defaultServerList="server1.angelsofclockwork.net server2.angelsofclockwork.net" \
+                    -a followReferrals=true \
+                    -a objectClassMap=shadow:shadowAccount=posixAccount \
+                    -a objectClassMap=passwd:posixAccount=posixaccount \
+                    -a objectClassMap=group:posixGroup=posixgroup \
+                    -a serviceSearchDescriptor=group:cn=groups,cn=solaris,cn=views,cn=compat,dc=angelsofclockwork,dc=net \
+                    -a serviceSearchDescriptor=passwd:cn=users,cn=solaris,cn=views,cn=compat,dc=angelsofclockwork,dc=net \
+                    -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \
+                    -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \
+                    -a bindTimeLimit=5
+# Make sure you set your props...
+% /usr/sbin/svccfg -s name-service/switch setprop config/sudoer = astring: "files ldap"
+% /usr/sbin/svccfg -s name-service/switch setprop config/password = astring: "files ldap [NOTFOUND=return]"
+% /usr/sbin/svccfg -s name-service/switch setprop config/group = astring: "files ldap [NOTFOUND=return]"
+
+% /usr/sbin/svcadm refresh svc:/system/name-service/switch
+% /usr/sbin/svcadm restart svc:/system/name-service/switch
+% /usr/sbin/svcadm restart ldap/client
+# Verify...
+% ldaplist -l passwd adusername
+. . .
+% id -a adusername
+. . .
+
+

Thank you to "mewho" on libera for finding this interesting workaround.

+

OmniOS/Illumos

+

Some steps between Solaris 10 and 11 can be followed to make OmniOS +work. However, we have been unable to resolve why sudo will not work +when using an AD trust. If you are using a standalone FreeIPA and no +trust, sudo should work just fine.

+

Legacy HBAC

+

For HBAC to work on Solaris, you will need to compile the pam_hbac +module found here. I would clone +the current master branch or download the master.zip to your Solaris +system. Each OS has their set of instructions for compiling.

+

First, create the following system account. We will need this when we +are configuring our legacy clients.

+
dn: uid=hbac,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com
+objectClass: account
+objectClass: simplesecurityobject
+objectClass: top
+uid: hbac
+userPassword: password
+
+

Solaris 10

+
% /opt/csw/bin/pkgutil -i -y libnet ar binutils gcc4g++ glib2 libglib2_dev gmake
+% /opt/csw/bin/pkgutil -i -y libnet ar binutils gcc4g++ glib2 libglib2_dev gmake
+% PATH=$PATH:/opt/csw/bin
+% export M4=/opt/csw/bin/gm4
+% autoconf -o configure
+% autoreconf -i
+
+# Yes, SSL must be disabled for Solaris 10 to work. The libraries are too old.
+# You may or may not need to set CFLAGS, CXXFLAGS, and LDFLAGS with -m32
+% ./configure AR=/opt/csw/bin/gar --with-pammoddir=/usr/lib/security --sysconfdir=/etc/ --disable-ssl --disable-man-pages
+% make
+% make install
+
+

Solaris 11

+
% pkg install autoconf libtool pkg-config automake gcc docbook
+% autoreconf -if
+% ./configure --with-pammoddir=/usr/lib/security --mandir=/usr/share/man --sysconfdir=/etc/
+% make
+% make install
+
+

Omnios

+
% pkg install developer/build/autoconf developer/build/libtool \
+              developer/pkg-config developer/build/automake    \
+              developer/gcc48 system/header developer/object-file \
+              developer/linker
+% autoreconf -if
+% ./configure --with-pammoddir=/usr/lib/security --mandir=/usr/share/man --sysconfdir=/etc/
+% make
+% make install
+
+

pam_hbac.conf

+
% vim /etc/pam_hbac.conf
+
+# Replace client with your server's FQDN
+URI = ldap://server.ipa.example.com
+BASE = dc=ipa,dc=example,dc=com
+BIND_DN = uid=hbac,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com
+BIND_PW = password
+SSL_PATH = /var/ldap
+HOST_NAME = client
+
+

PAM Configuration

+
# Solaris 10 - /etc/pam.conf
+# Modify the other account section... It should come at the end of the account blocks.
+. . .
+other account required pam_hbac.so ignore_unknown_user ignore_authinfo_unavail
+
+# Solaris 11 - /etc/pam.d/other
+# Same here, only modify the account section
+. . .
+account required        pam_hbac.so ignore_unknown_user ignore_authinfo_unavail
+
+

In the event you cannot login or things aren't working the way you'd +expect, add 'debug' to the end of the pam_hbac line and watch +/var/log/authlog for errors.

+

Login with AD Users to Legacy Clients

+

For AD users to be able to login to legacy clients, you have to enable +system-auth to the IPA servers. Without it, users will be denied access, +regardless of HBAC controls or if you're using the pam_hbac module.

+
% ipa hbacsvc-add system-auth
+% ipa hbacrule-add legacy_client_auth
+% ipa hbacrule-add-host --hostgroups=ipaservers legacy_client_auth
+% ipa hbacrule-mod --usercat=all legacy_client_auth
+
+

Legacy Active Directory Trust Notes

+

Just a section of notes.

+

Domain Resolution Order Oddness

+

If using domain resolution order, AD users get double uid attributes - +but only if they login with their shortname. If they login with fqdn, +double uid's do not occur. But shortnames do not work anymore. Have to +restart the directory server to make short names work again.

+

Solaris Weirdness

+

If using domain resolution order, Solaris 10 gets the group resolution +correct for short named AD users. Solaris 11 does not unless you are on +SRU 11.4.7.4.0 or newer. There is a way to chop off the domain name from +the uid using views.

+

Domain Options

+

This section goes over "situational" scenarios. These scenarios are +reflective of the environment in which IPA is installed and not all will +fit into your environment. These are more or less common situations that +could occur during an IPA deployment or even post-deployment.

+

Remove @realm for AD users

+

A common scenario is that IPA and AD will have a trust, but there will +not be any IPA users with the exception of the engineering team for +managing IPA itself. The common theme is that because of this, the +engineers and customers would rather not login with username@realm.

+
+

Info

+

The following is only applicable in an IPA-AD trust. An IPA-only +scenario would not require any of these steps and most pieces would work +natively (no @realm, sudo, hbac).

+

In the event that you are in an IPA-AD scenario, please take note that +this can adversely affect legacy clients. This will cause ldapsearches +that are done in the compat tree to display multiple uid attributes. In +most cases, this is fine and the user can still login without the realm +name. The whoami and id commands will show the domain. There's no +workaround for this.

+
+

On the IPA servers, you will need to set the domain resolution order. +This was introduced in 4.5.0.

+
% kinit admin
+% ipa config-mod --domain-resolution-order="example.com:ipa.example.com"
+
+

After, you will need to clear out your SSSD cache.

+
# sss_cache -E is insufficient for this.
+% systemctl stop sssd
+% rm -rf /var/lib/sss/db/*
+% systemctl start sssd
+
+

The below is optional. It will remove the @realm off the usernames, like +on the prompt or id or whoami commands. Only do this if required. Only +do this on the clients. Do not make this change on an IPA replica.

+
# vi /etc/sssd/sssd.conf
+
+[domain/ipa.example.com]
+. . .
+full_name_format = %1$s
+
+

This will ensure EL7, EL8, EL9 clients resolve the AD domain first when +attempting logins and optionally drop the @realm off the usernames.

+

AD and IPA group names with short names

+

You may notice that your clients have intermittent issues with name +resolution when the following are true:

+
    +
  • Groups (or users) have the same names in both IPA and AD
  • +
  • You are using domain resolution order
  • +
  • You are shortening names on the clients
  • +
+

You may want to actually search for them to identify the errant groups +and then correct them. You can correct them either on the AD or IPA +side. I would opt for the IPA side.

+
% kinit admin@IPA.EXAMPLE.COM
+% vi /tmp/dupecheck.sh
+#!/bin/bash
+for x in ${ARRAY[*]} ; do
+  ldapsearch -x -b "DC=example,DC=com" -h example.com -LLL -w 'PASSWORD' -D 'username@example.com' samaccountname="$x" samaccountname | grep -q $x
+  if [[ $? -eq 0 ]]; then
+    echo "$x: DUPLICATE"
+  fi
+done
+
+% bash /tmp/dupecheck.sh
+
+

If you run into any duplicates, they should show up in a list for you +address.

+
+

sAMAccountName vs CN

+

The "CN" and "sAMAccountName" attributes are not the same in AD, +depending on who made the group or other factors. The sAMAccountName +attribute is the value used to determine names from AD, whether you are +enrolled with AD or the IPA server SSSD is pulling the information. This +is why we are searching for that attribute, and not the CN.

+
+

Sites and AD DC's

+

By creating a subdomain section in /etc/sssd/sssd.conf on an IPA server, +it is possible to set an AD Site or AD server(s) directly in SSSD. By +default, sssd tries to do location based discovery. There may be a case +where this isn't possible (eg, only a set of AD servers may only be +contacted in certain "air gapped" networks).

+
[domain/ipa.example.com/example.com]
+# If you want a site
+ad_site = Site_Name
+# If you want a server(s)
+ad_server = dc1.example.com, dc2.example.com
+# A backup?
+ad_backup_server = dc3.example.com, dc4.example.com
+
+

If you don't have access or a way to find the sites using the Windows +tools, you can run an ldapsearch to find it (or an equivalent ldap +browsing tool).

+
% ldapsearch -x -h example.com -s one -WD 'CN=username,CN=Users,DC=example,DC=com' \
+  -b 'CN=Sites,CN=Configuration,DC=example,DC=com' cn
+
+

This should report back your sites. If you want to know the servers for +those sites (in case you don't want to deal with the sites, but just +the DC's themselves), you use ldapsearch but use the base DN of the +site name.

+
% ldapsearch -x -h example.com -WD 'CN=username,CN=Users,DC=example,DC=com' \
+  -b 'CN=Servers,CN=Site_Name,CN=Sites,CN=Configuration,DC=example,DC=com' dnsHostName
+
+
+

Hardcoded DC's

+

If the DC's change at any time and they are harded in your sssd.conf, +it is up to you to know when new controllers are being added or removed +as to not disrupt the connectivity from IPA to AD when performing user +or group lookups.

+
+

Enterprise Linux 6 SUDO and Default Domain Suffix

+

This issue with the above section is that once you do this, sudo rules +will begin failing, they will no longer work for Enterprise Linux 6. +This is because sssd was changed to look for cn=sudo rather than +ou=sudoers. To enable the compatibility fall back, you will need to +install a newer SSSD.

+

Set Default Shell for AD Users

+

By default, after a trust has been established, the shell all AD users +get is /bin/sh. To change this, you must change the sssd.conf on the IPA +masters.

+
% vi /etc/sssd/sssd.conf
+[domain/ipa.example.com]
+. . .
+default_shell = /bin/bash
+
+% systemctl restart sssd
+
+

Automated Kerberos Principals

+

Once in a great while, we run into situations where we need to have an +automated process for creating principals and keytabs. This section +takes a look at some of those examples that we've ran into.

+

Hadoop/Cloudera

+

This assumes you are using Cloudera Manager and not Ambari in any form.

+
+

DNS Information

+

It is highly likely that if you are using AWS, your nodes are getting +stupid names like compute.internal. While there is a a way to change +this if +you don't change it, you will need to rely on something like DNSMASQ to +allow the nodes to communicate with FreeIPA. FreeIPA will be upset +about the stupid names because it can't do a rDNS lookup.

+
+

Cloudera Manager Woes

+

It is likely you have Cloudera/Hadoop, it is also very likely you (or +another team) are deploying and using Cloudera Manager (or Director?). +You may be running into issues that involve direct Active Directory +integration. Maybe you're moving away from a standalone LDAP system +over to Active Directory or even FreeIPA. Maybe you have FreeIPA in an +AD trust but the users or contractors absolutely insist on using AD +against their better judgement, despite the problems they're running +into. Whatever the scenario is, we feel your pain. Here are some things +you should probably know:

+
    +
  • +

    Cloudera Manager (or Director?) supports Active Directory out of the + box and obviously not FreeIPA despite the devs wanting to work + something out back in 2015

    +
  • +
  • +

    Ambari has support for FreeIPA, but we are focusing on Cloudera + Manager here.

    +
      +
    • Cloudera Manager supports custom keytab retrieval scripts
    • +
    +
  • +
  • +

    Hostnames that are longer than 15 characters, regardless of the + cloud provider or onprem setup, will ultimately fail

    +
      +
    • The NETBIOS limit in AD is 16 characters, which is 15 + $ at + the end - This means hosts will enroll on top of themselves and + your cluster will be broken
    • +
    +
  • +
+

FreeIPA does not have the name limitation and using an AD trust, AD +users can freely use Hadoop when the cluster is properly setup. +Enrolling the cluster nodes into FreeIPA and using a custom retrieval +script will solve most (if not all) of the issues you may run into as +well when it comes to keytabs, which Hadoop heavily relies on. The +custom script is simply because Cloudera by default likes having direct +access to the kerberos infrastructure, which is a no-go for FreeIPA.

+

The Solution

+

To summarize, here is our proposed solution:

+
    +
  • Create an account called cdh
  • +
  • +

    Create a role called "Kerberos Managers" and apply the following + privileges:

    +
      +
    • System: Manage Host Keytab
    • +
    • System: Manage Host Keytab Permissions
    • +
    • System: Manage Service Keytab
    • +
    • System: Manage Service Keytab Permissions
    • +
    • System: Manage User Principals (was not actually used, but who + knows what we could use the role for later)
    • +
    +
  • +
  • +

    Apply the role to the cdh account

    +
  • +
  • Create a custom script they could use to enroll the servers into + FreeIPA (out of scope here)
  • +
  • Create a custom script that utilizes the cdh account to create + services
  • +
+

So let's create the necessary things we need.

+
# Create the account
+# Note... you may want to make this account non-expiring since it's just a service account
+% ipa user-add --first="Cloudera" --last="Key Manager" cdh
+
+# Create the Kerberos Managers role
+% ipa role-add "Kerberos Managers"
+
+# Create the kerberos manager privilege
+% ipa privilege-add "Privileges - Kerberos Managers"
+% ipa privilege-add-permission "Privileges - Kerberos Managers" \
+    --privileges="System: Manage Host Keytab" \
+    --privileges="System: Manage Host Keytab Permissions" \
+    --privileges="System: Manage Service Keytab" \
+    --privileges="System: Manage Service Keytab Permissions" \
+    --privileges="System: Manage User Principals"
+
+# Add the privilege to the role
+% ipa role-add-privilege "Kerberos Managers" \
+    --privileges="Privileges - Kerberos Managers"
+
+# Add the user to the role
+% ipa role-add-member --users=cdh "Kerberos Managers"
+
+# Optionally, we can export the keytab for the user with a password
+# You will see why in the next script
+% ipa-getkeytab -p cdh@EXAMPLE.COM -k cdh.keytab -P
+
+

Now we need our special kerberos keytab retrieval script.

+
#!/bin/bash
+# Created by: @nazunalika - Louis Abel
+# Purpose: To retrieve keytabs for Cloudera / Hadoop
+# https://github.com/nazunalika/useful-scripts
+
+# Disclaimer: We do not take responsibilities for breaches or misconfigurations of
+#             software. Use at your own risk
+
+# Variables
+# This can be anywhere, but it SHOULD be secure with at least 600 permissions
+CDHKT="/root/.cdh/cdh.keytab"
+CDHUSER="cdh"
+IPAREALM="EXAMPLE.COM"
+# This can be any server. You could make an array and have it randomly selected
+IPASERVER="ipa01.example.com"
+
+# Where is this going?
+DESTINATION="$1"
+# The full principal for the keytab in question
+FULLPRINC="$2"
+# Shortened name
+PRINC=$(echo $FULLPRINC | sed "s/\@$(echo $IPAREALM)//")
+
+00_kinitUser() {
+  # Pick what suits you best, we prefer using a keytab
+  # Password based kinit, based on the keytab we created prior!
+  # You could also have this in a file somewhere, I guess. Just
+  # has to be secured.
+  echo ThisIsAWeakPassword | kinit $CDHUSER@$IPAREALM
+
+  # Keytab based kinit, obviously we created it before right? It just needs to be
+  # on the right system, deployed in some secure manner
+  #kinit -kt $CDHKT $CDHUSER@$IPAREALM
+  if [[ $? == "1" ]]; then
+    echo FAILED TO KINIT
+    exit
+  fi
+}
+
+01_createPrinc() {
+  echo "INFO: Checking for existing principle"
+  if ipa service-find $FULLPRINC; then
+    echo "INFO: Principle found"
+  else
+    echo "INFO: Not found, creating"
+    ipa service-add $FULLPRINC
+  fi
+}
+
+02_createServiceAllows() {
+  # We need to allow the service to create and retrieve keytabs
+  echo "INFO: Ensuring service allows to create and retrieve keytabs"
+  ipa service-allow-create-keytab --users=$CDHUSER $FULLPRINC
+  ipa service-allow-retrieve-keytab --users=$CDHUSER $FULLPRINC
+
+  # Let's retrieve the keytabs
+  if ipa service-show $FULLPRINC | grep 'Keytab' | grep 'False'; then
+    echo "INFO: Creating keytab for $FULLPRINC to $DESTINATION"
+    ipa-getkeytab -s $IPASERVER -p $PRINC -k $DESTINATION
+  else
+    echo "INFO: Retriving keytab for $FULLPRINC to $DESTINATION"
+    ipa-getkeytab -r -s $IPASERVER -p $PRINC -k $DESTINATION
+  fi
+}
+
+00_kinitUser
+01_createPrinc
+02_createServiceAllows
+
+kdestroy
+exit 0
+
+

Place the above script in a file that is accessible by the cloudera +manager such as /usr/local/bin/getKeytabsCDH.sh and ensure it is owned +by cloudera-scm with a permission set of 775.

+

During the kerberos wizard, stop when you are verifying the "cdh" +user. You will need to set the configuration for "Custom Kerberos +Keytab Retrieval Script" to /usr/local/bin/getKeytabsCDH.sh and then +you're almost there.4

+

An important tidbit is currently Enterprise Linux 7+ and higher use +memory based keytabs and java doesn't support them.5 Because of +this, the /etc/krb5.conf should be modified.

+
% cat /etc/krb5.conf
+. . .
+# Make sure the below is commented
+# default_ccache_name = KEYRING:persistent:%{uid}
+. . .
+
+

DNS Forwarding

+

DNS Forwarding to DoT

+

Presently, FreeIPA does not support DoT (DNS over TLS) nor DoH (DNS over +HTTPS) (this appears to be a bind limitation and we can't find +documentation that says otherwise). However, it is possible to setup +unbound to do the forwarding for you, in which you tell your bind +servers (or in this case, the bind DNS servers in your IPA domain) to +forward to that unbound server for all forwarding.

+
+

Keep it Separate

+

It is recommended to keep your unbound service separate from the IPA +servers. Spin up another instance in your network that will run unbound +or run it on a standalone bind server that you may have on a separate +port.

+
+

To forward to the unbound service, modify the DNS global configuration +in IPA:

+
# Replace 10.100.0.224 with the IP of your unbound instance
+% ipa dnsconfig-mod --forward-policy=only --forwarder='10.100.0.224'
+
+# Add 'port xxxx' if you have set unbound to another port
+% ipa dnsconfig-mod --forward-policy=only --forwarder='10.100.0.224 port 9553'
+
+

Logging

+

Audit Logs

+

By default, the audit logs in /var/log/dirsrv/slapd-INSTANCE/audit do +not get populated. And the access logs don't show much in terms of +modifications and what is being changed. There is also /var/log/httpd/* +logs, but it may be useful to see ldif style logging for changes against +FreeIPA.

+
# Modify the DSE configuration by turning on audit logging
+[label@ipa01 ~]# ldapmodify -D "cn=directory manager" -W -p 389 -h localhost
+Enter LDAP Password:
+dn: cn=config
+changetype: modify
+replace: nsslapd-auditlog-logging-enabled
+nsslapd-auditlog-logging-enabled: on
+# Press CTRL+d here
+modifying entry "cn=config"
+
+# To test, I'll add a user to a group
+[label@ipa01 ~]$ ipa group-add-member --users=jbaskets aocusers
+  Group name: aocusers
+  GID: 686600003
+  Member users: ..., jbaskets
+-------------------------
+Number of members added 1
+-------------------------
+# Let's verify the log
+[label@ipa01 ~]$ sudo su -
+[sudo] password for label:
+Last login: Sun Mar 29 16:42:36 MST 2020 on pts/0
+[root@ipa01 ~]# cd /var/log/dirsrv/slapd-EXAMPLE-NET/
+[root@ipa01 slapd-EXAMPLE-NET]# cat audit
+time: 20200329223754
+dn: cn=config
+result: 0
+changetype: modify
+replace: nsslapd-auditlog-logging-enabled
+nsslapd-auditlog-logging-enabled: on
+-
+replace: modifiersname
+modifiersname: cn=directory manager
+-
+replace: modifytimestamp
+modifytimestamp: 20200330053754Z
+-
+
+        389-Directory/1.4.1.3 B2019.323.229
+        ipa01.example.net:636 (/etc/dirsrv/slapd-EXAMPLE-NET)
+
+# Looks like right here the modification happened 
+time: 20200329224007
+dn: cn=aocusers,cn=groups,cn=accounts,dc=example,dc=net
+result: 0
+changetype: modify
+add: member
+member: uid=jbaskets,cn=users,cn=accounts,dc=example,dc=net
+-
+replace: modifiersname
+modifiersname: uid=label,cn=users,cn=accounts,dc=example,dc=net
+-
+replace: modifytimestamp
+modifytimestamp: 20200330054006Z
+-
+replace: entryusn
+entryusn: 900028
+-
+
+

Certificates

+

These are notes of things I've ran into before while dealing with +certificates.

+

Renewed IPA HTTP Certificate Stuck

+

This was something I discovered sort of on accident but never really +"noticed" - Though I'm sure I would've noticed sometime in 2021 when +my certificate expired. I was running ipa-healthcheck --failures-only +as I do sometimes, and noticed some weird certmonger things pop up. But +it made me look at my certificate list...

+
[root@ipa01 ~]# ipa-getcert list
+Number of certificates and requests being tracked: 9.
+Request ID '20191106025922':
+        status: MONITORING
+        stuck: no
+        key pair storage: type=FILE,location='/var/kerberos/krb5kdc/kdc.key'
+        certificate: type=FILE,location='/var/kerberos/krb5kdc/kdc.crt'
+        CA: IPA
+        issuer: CN=Certificate Authority,O=ANGELSOFCLOCKWORK.NET
+        subject: CN=ipa01.angelsofclockwork.net,O=ANGELSOFCLOCKWORK.NET
+        expires: 2021-11-05 19:59:27 MST
+        principal name: krbtgt/ANGELSOFCLOCKWORK.NET@ANGELSOFCLOCKWORK.NET
+        key usage: digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment
+        eku: id-kp-serverAuth,id-pkinit-KPKdc
+        pre-save command:
+        post-save command: /usr/libexec/ipa/certmonger/renew_kdc_cert
+        track: yes
+        auto-renew: yes
+Request ID '20200123075636':
+        status: MONITORING
+        stuck: no
+        key pair storage: type=NSSDB,location='/etc/dirsrv/slapd-ANGELSOFCLOCKWORK-NET',nickname='Server-Cert',token='NSS Certificate DB',pinfile='/etc/dirsrv/slapd-ANGELSOFCLOCKWORK-NET/pwdfile.txt'
+        certificate: type=NSSDB,location='/etc/dirsrv/slapd-ANGELSOFCLOCKWORK-NET',nickname='Server-Cert',token='NSS Certificate DB'
+        CA: IPA
+        issuer: CN=Certificate Authority,O=ANGELSOFCLOCKWORK.NET
+        subject: CN=ipa01.angelsofclockwork.net,O=ANGELSOFCLOCKWORK.NET
+        expires: 2021-11-05 19:55:33 MST
+        dns: ipa01.angelsofclockwork.net
+        principal name: ldap/ipa01.angelsofclockwork.net@ANGELSOFCLOCKWORK.NET
+        key usage: digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment
+        eku: id-kp-serverAuth,id-kp-clientAuth
+        pre-save command:
+        post-save command: /usr/libexec/ipa/certmonger/restart_dirsrv ANGELSOFCLOCKWORK-NET
+        track: yes
+        auto-renew: yes
+Request ID '20200123075639':
+        status: NEWLY_ADDED_NEED_KEYINFO_READ_PIN
+        stuck: yes
+        key pair storage: type=FILE,location='/var/lib/ipa/private/httpd.key'
+        certificate: type=FILE,location='/var/lib/ipa/certs/httpd.crt'
+        CA: IPA
+        issuer: CN=Certificate Authority,O=ANGELSOFCLOCKWORK.NET
+        subject: CN=ipa01.angelsofclockwork.net,O=ANGELSOFCLOCKWORK.NET
+        expires: 2021-11-05 19:55:48 MST
+        dns: ipa01.angelsofclockwork.net
+        principal name: HTTP/ipa01.angelsofclockwork.net@ANGELSOFCLOCKWORK.NET
+        key usage: digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment
+        eku: id-kp-serverAuth,id-kp-clientAuth
+        pre-save command:
+        post-save command: /usr/libexec/ipa/certmonger/restart_httpd
+        track: yes
+        auto-renew: yes
+
+

Interestingly, I wasn't sure what +NEWLY_ADDED_NEED_KEYINFO_READ_PIN meant and I couldn't really find +much on what would cause this to happen. And I know my certificate +isn't expired, according to the output. In fact, I checked with openssl +just in case.

+
[root@ipa01 ~]# openssl x509 -text -noout -in /var/lib/ipa/certs/httpd.crt | grep 'Not After'
+            Not After : Nov  6 02:55:48 2021 GMT
+
+

I'm not sure if this is just a result of migrating from Enterprise +Linux 7 to 8 at the time, but it seemed easy enough to remove the +tracking and put it back in, which ultimately fixed the monitoring state +and now it was no longer "stuck".

+
[root@ipa01 ~]# ipa-getcert stop-tracking -i 20200123075639
+Request "20200123075639" removed.
+[root@ipa01 ~]# ipa-getcert start-tracking -f /var/lib/ipa/certs/httpd.crt -k /var/lib/ipa/private/httpd.key -p /var/lib/ipa/passwds/ipa01.angelsofclockwork.net-443-RSA -C /usr/libexec/ipa/certmonger/restart_httpd -K HTTP/ipa01.angelsofclockwork.net@ANGELSOFCLOCKWORK.NET
+New tracking request "20200504003758" added.
+[root@ipa01 ~]# ipa-getcert list -i "20200504003758"
+Number of certificates and requests being tracked: 9.
+Request ID '20200504003758':
+        status: MONITORING
+        stuck: no
+        key pair storage: type=FILE,location='/var/lib/ipa/private/httpd.key',pinfile='/var/lib/ipa/passwds/ipa01.angelsofclockwork.net-443-RSA'
+        certificate: type=FILE,location='/var/lib/ipa/certs/httpd.crt'
+        CA: IPA
+        issuer: CN=Certificate Authority,O=ANGELSOFCLOCKWORK.NET
+        subject: CN=ipa01.angelsofclockwork.net,O=ANGELSOFCLOCKWORK.NET
+        expires: 2021-11-05 19:55:48 MST
+        dns: ipa01.angelsofclockwork.net
+        principal name: HTTP/ipa01.angelsofclockwork.net@ANGELSOFCLOCKWORK.NET
+        key usage: digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment
+        eku: id-kp-serverAuth,id-kp-clientAuth
+        pre-save command:
+        post-save command: /usr/libexec/ipa/certmonger/restart_httpd
+        track: yes
+        auto-renew: yes
+
+ +

Like with the IPA httpd certificates, I noticed at least 4 certificates +stuck because a PIN was missing. Turns out that it's actually easy to +modify the tracking request and fix the issue entirely. Below is my +example doing this on the auditSigningCert. This seems to only occur on +Enterprise Linux 8.

+
[root@ipa01 alias]# getcert list -i 20200615180351
+Number of certificates and requests being tracked: 9.
+Request ID '20200615180351':
+        status: NEWLY_ADDED_NEED_KEYINFO_READ_PIN
+        stuck: yes
+        key pair storage: type=NSSDB,location='/etc/pki/pki-tomcat/alias',nickname='auditSigningCert cert-pki-ca'
+        certificate: type=NSSDB,location='/etc/pki/pki-tomcat/alias',nickname='auditSigningCert cert-pki-ca'
+        CA: dogtag-ipa-ca-renew-agent
+        issuer:
+        subject:
+        expires: unknown
+        pre-save command: /usr/libexec/ipa/certmonger/stop_pkicad
+        post-save command: /usr/libexec/ipa/certmonger/renew_ca_cert "auditSigningCert cert-pki-ca"
+        track: yes
+        auto-renew: yes
+
+[root@ipa01 alias]# getcert start-tracking -i 20200615180351 -p /etc/pki/pki-tomcat/alias/pwdfile.txt
+Request "20200615180351" modified.
+[root@ipa01 alias]# getcert list -i 20200615180351
+Number of certificates and requests being tracked: 9.
+Request ID '20200615180351':
+        status: MONITORING
+        stuck: no
+        key pair storage: type=NSSDB,location='/etc/pki/pki-tomcat/alias',nickname='auditSigningCert cert-pki-ca',token='NSS Certificate DB',pinfile='/etc/pki/pki-tomcat/alias/pwdfile.txt'
+        certificate: type=NSSDB,location='/etc/pki/pki-tomcat/alias',nickname='auditSigningCert cert-pki-ca',token='NSS Certificate DB'
+        CA: dogtag-ipa-ca-renew-agent
+        issuer: CN=Certificate Authority,O=ANGELSOFCLOCKWORK.NET
+        subject: CN=CA Audit,O=ANGELSOFCLOCKWORK.NET
+        expires: 2021-03-13 23:15:41 MST
+        key usage: digitalSignature,nonRepudiation
+        pre-save command: /usr/libexec/ipa/certmonger/stop_pkicad
+        post-save command: /usr/libexec/ipa/certmonger/renew_ca_cert "auditSigningCert cert-pki-ca"
+        track: yes
+        auto-renew: yes
+
+

Default Certificates with SAN

+

A question that arises now and again is how to setup a load balancer for +FreeIPA's LDAP servers whether it's an actual load balancer (layer 4) +or some sort of DNS record with multiple A records, or perhaps with some +sort of round robin DNS. The issue is that the certificate verification +fails, because the certificate being presented is of the IPA server +itself with no SAN. To address this, you have to create a host that has +the name of the load balancer or DNS record you plan on using and allow +the IPA servers to manage the host.

+

CMS Communication Issues (403)

+

This isn't necessarily certificate issue, but more or less an issue as +it pertains to the certificate system itself. There may be cases where +during upgrades, a configuration in /etc/pki/pki-tomcat/server.xml is +not properly reconfigured. In that file, you'll notice Connector lines +that have a secret and a requiredSecret parameter and they both have +different values.

+
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" address="localhost4" secret="AAA" requiredSecret="BBB"/>
+<Connector address="localhost6" port="8009" protocol="AJP/1.3" redirectPort="8443" secret="AAA" requiredSecret="BBB"/>
+
+

The issue may be that these aren't correct. This generally comes down +to IPA and pki-core conflicting on these attributes. To correct this, +you will need to find the secret in /etc/httpd/conf.d/ipa-pki-proxy.conf +(on the ProxyPass line) and ensure that's the same secret in both +fields.

+
ProxyPassMatch ajp://localhost:8009 secret=AAA
+
+

Make sure they're the same in server.xml

+
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" address="localhost4" secret="AAA" requiredSecret="AAA"/>
+<Connector address="localhost6" port="8009" protocol="AJP/1.3" redirectPort="8443" secret="AAA" requiredSecret="AAA"/>
+
+

After changing, restart the service with +systemctl restart pki-tomcat@pki-tomcatd.service.

+

Kerberos

+

This section goes over some stuff about kerberos that we've ran into +and might find useful someday.

+

Accounts with OTP Enabled

+

When logging into a machine with a password (first factor) and an OTP +token (second factor), this generally works without a problem. You can +easily run klist and you'll see that you have a ticket and everything. +In the cases where you're calling kinit all by itself, this doesn't +work as expected at the time of this writing.

+
% kinit account@REALM
+kinit: Pre-authentication failed: Invalid argument while getting initial credentials
+
+

A bugzilla was +opened about this issue in 2017, a +pagure issue was opened in 2014 +about this exact scenario, where IPA is configured for password+OTP and +a user has an assigned token. There is currently one workaround, which +is using kinit -n to perform anonymous processing.

+

Footnotes

+
+
+
    +
  1. +

    For more information on DNS for FreeIPA, please read this page and this page 

    +
  2. +
  3. +

    The -P asks for the password of the username in question, that way it is cached right away. The directory service on the system then has credentials to compare to. I have found that sometimes if you don't use -P, even if you're logged in as the account, the password does not get cached and you'll get stuck at a background image the next time you login. Again, this is only sometimes. Your mileage may vary here. 

    +
  4. +
  5. +

    The -P asks for the password of the username in question, that way it is cached right away. The directory service on the system then has credentials to compare to. I have found that sometimes if you don't use -P, even if you're logged in as the account, the password does not get cached and you'll get stuck at a background image the next time you login. Again, this is only sometimes. Your mileage may vary here. 

    +
  6. +
  7. +

    Please read this page for more information. 

    +
  8. +
  9. +

    This may have changed. However it is up to you to test if this is the case. 

    +
  10. +
+
+
+
+ + + + + + + + + + + + diff --git a/el/index.html b/el/index.html new file mode 100644 index 00000000..bfe198a1 --- /dev/null +++ b/el/index.html @@ -0,0 +1,230 @@ + + + + + + + + + + + Enterprise Linux - Linux Guide and Hints + + + + + + + + + + + + + + + +
+
+
+
+ +

This section contains various articles on setups for Enterprise Linux and Fedora systems.

+
+
+ + + + + + + + + + + + diff --git a/el/nat/index.html b/el/nat/index.html new file mode 100644 index 00000000..ebb9c15b --- /dev/null +++ b/el/nat/index.html @@ -0,0 +1,318 @@ + + + + + + + + + + + NAT/Router - Linux Guide and Hints + + + + + + + + + + + + + + + +
+
+
+
+ +

This page goes over setting up a router or a simple NAT service for +Enterprise Linux.

+

Requirements

+

Here are the list of requirements below.

+
    +
  • Enterprise Linux 8, 9 or Fedora
  • +
  • An active internet connection to install the packages required or available internal mirrors
  • +
  • A system with at least two (2) network interfaces
  • +
+

Tutorial

+

Interface Setup

+

To properly setup the system, a few things have to be done.

+
    +
  1. One interface must be the WAN interface, in most cases this is set + to DHCP.
  2. +
  3. Another interface must be the LAN interface or a group of interfaces + must become a bridge with a static address
  4. +
  5. ip_forward must be turned on - optionally if you have ipv6, turn on + that forwarding as well
  6. +
+
+

IPv6 and NAT

+

If you have an IPv6 prefix, whether it's from your ISP or it's a +brokered prefix from he.net, NAT is generally not needed. Instead of +using NAT for IPv6, you can just do simple forwarding. This is covered +in a later section.

+
+

FirewallD

+

When using firewalld, Enterprise Linux 7+ and all Fedora\'s can setup a +simple NAT with masquerade without having to know iptables or nftables +syntax. This may be more or less ideal for some users who want to +quickly get a NAT and router going. The drawback is that the syntax and +knowing how the rules work are hidden behind a frontend. To setup a NAT:

+
# Tell eth0 to be our WAN
+% nmcli con mod eth0 connection.zone external
+# Tell eth1 to be our LAN (or a bridge if you have one)
+% nmcli con mod eth1 connection.zone internal
+# Doesn't hurt to re-up
+% nmcli con up eth0 ; nmcli con up eth1
+
+# The external zone already has masquerade on, but just in case
+% firewall-cmd --zone=external --add-masquerade --permanent
+% firewall-cmd --complete-reload
+% firewall-cmd --get-active-zones
+external
+  interfaces: eth0
+internal
+  interfaces: eth1
+
+

nftables

+

This is for Enterprise Linux 8/9 or Fedora where nftables is the default. +While iptables exists for Enterprise Linux 8 still, it is being +superseded by nftables. It is recommended to stick with nftables.

+

The syntax for nftables is a little tricky and quite different from what +we may be used to with iptables. This may be an oversimplification and +may or may not work. For ideas, you can view the files in /etc/nftables. +This is a rough example of what I tried on migration to Enterprise Linux +8.

+
# Disable firewalld, we'll enable nftables later
+% systemctl disable firewalld --now
+% systemctl mask firewalld
+# Flush all rules
+% nft flush ruleset
+
+

Rest coming soon.

+

IPv6 Forwarding

+

Coming soon.

+

DHCP

+

Optional. Coming soon

+
+
+ + + + + + + + + + + + diff --git a/el/openldap/index.html b/el/openldap/index.html new file mode 100644 index 00000000..26f8c9ef --- /dev/null +++ b/el/openldap/index.html @@ -0,0 +1,1398 @@ + + + + + + + + + + + OpenLDAP - Linux Guide and Hints + + + + + + + + + + + + + + + +
+
+ +
+ +

Deprecation

+

Please note that the OpenLDAP server is considered deprecated in RHEL +(and thusly other EL derivatives). This document will stay here as a +reference for those who are still using the server software on +Enterprise Linux 7, potentially using the OpenLDAP LTB version of the +software, or using the openldap-servers package from Rocky Linux 9's +plus repository. It may apply to Fedora users in some contexts, but +there are some differences they may never be documented here. If you see +a need for corrections, please open up an issue on our github.

+

Most of this information should be considered out of date. OpenLDAP +2.6.x makes some changes that the below document may or may not cover.

+

Preface

+

This tutorial goes over how to install OpenLDAP to a Enterprise Linux +Server and options on configuring and setting up accounts for host +access, etc. This how-to is the method of implementation that I used, +and can be modified/changed to any users preferences if preferred. +Enterprise Linux and Fedora users will have the ability to use SSSD for +SUDO (and it's relatively easy to set it up).

+

Overview

+

Simply put, LDAP is a directory service for authentication across a +network. Rather than have local accounts on a bunch of machines, LDAP +can be used to have one account across a bunch of machines. LDAP was +once an easy setup in RHEL 5 but had changed in RHEL 6 and 7, and here +provides the necessary information needed to get a simple LDAP system +running with possible SUDO support and various options of how to support +your LDAP system.

+

Requirements

+

First and foremost, we have a list of requirements. Keep in mind, if you +do not fulfill these requirements, you may run into some issues down the +road.

+
    +
  • Enterprise Linux 8, Enterprise Linux 9
  • +
  • DNS Server (LDAP does NOT appreciate IP addresses for the URI)
  • +
  • An active internet connection to install the packages required
  • +
+

Tutorial Preface, Notes, and Recommendations

+
+

Warning

+

Potential Pitfalls!

+
    +
  • The incorrect configuration in your firewall or other settings can + cause login failures
  • +
  • Not using certificates (TLS/SSL) will cause you not to be able to + login (This is a EL7+ LDAP Client Requirement)
  • +
  • SELinux is a pain when using mounted home directories or + certificates (primarily in Enterprise Linux 7+, will you have + problems with certificates/home directories)
  • +
  • If you use /home as an NFS mount, you'll see some nasty side + effects if you have local users.
  • +
+
+
+

Note

+

Recommended Information

+
    +
  • It's recommended to use colored vim syntax. Root doesn't use vim + when vim-enhanced is installed. You can make an alias for vi to run + vim.
  • +
  • Turn on syntaxing in ~/.vimrc -- syntax on
  • +
  • Make the vim colors brighter in ~/.vimrc -- set background=dark
  • +
  • Export your EDITOR variable in ~/.bash_profile -- export EDITOR=vim
  • +
  • Keep selinux set to enforcing
  • +
+
+
+

Note

+

Database Information

+

We will be using lmdb, which is recommended over using hdb or bdb.

+
+
+

Note

+

EL 9 OpenLDAP

+

Enterprise Linux 9 has fully dropped OpenLDAP where there are no server +packages normally built. In some EL distributions, openldap-servers is +still built and provided in an extra repository. Rocky Linux 9 has +openldap-servers in their plus repository. EPEL also has it available.

+
+

Installation

+

Below details the process for installing OpenLDAP to our system(s). This +includes installing the packages, setting up certificates, and +configuring the LDAP server via LDIF files.

+

Packages

+

You will need the following packages. A couple of them may already be +installed. If you don't plan on migrating local accounts to LDAP, you +can leave out migrationtools.

+
# If you are on Rocky Linux 9, you will need to enable the plus repository
+dnf install openldap openldap-servers migrationtools nss-tools -y
+
+

Certificates

+

Enterprise Linux 7 clients and other newer distributions that are non-el +require TLS/SSL for authentication when going toward LDAP. because of +this, we will need to create certificates, regardless if you are in a +lab or not.

+
+

Note

+

Certificate Information

+

NSS should no longer be required. Anything NSS related has been removed.

+
+

I have two ways of doing it, we can do it manually or through a script. +I prefer using my script to take care of it. First the manual way.

+
mkdir /etc/pki/ldap 
+cd /etc/pki/ldap
+openssl genrsa -des3 -out ca.key 4096  # Remember the password you put here
+
+openssl genrsa -out ldapserver.key 4096
+
+openssl req -new -x509 -key ca.key -out ca.pem -days 3650
+Country Name (2 letter code) [XX]:US
+State or Province Name (full name) []:Arizona
+Locality Name (eg, city) [Default City]:Phoenix
+Organization Name (eg, company) [Default Company Ltd]:SSN Studio
+Organizational Unit Name (eg, section) []:Channel Maintainers
+Common Name (eg, your name or your server's hostname) []:SSN     # If you want to use a server name here, perform this step on another server first
+Email Address []:youremail@mail.com
+
+openssl req -new -key ldapserver.key -out ldapserver.csr
+Country Name (2 letter code) [XX]:US
+State or Province Name (full name) []:Arizona
+Locality Name (eg, city) [Default City]:Phoenix
+Organization Name (eg, company) [Default Company Ltd]:SSN Studio
+Organizational Unit Name (eg, section) []:LDAP Server Maintainer
+Common Name (eg, your name or your server's hostname) []:zera1.angelsofclockwork.net    # Set your common name to your server name for this certificate 
+Email Address []:youremail@mail.com
+
+openssl x509 -req -in ldapserver.csr -out ldapserver.pem -CA ca.pem -CAkey ca.key -days 3650 -set_serial 01
+
+ln -s ca.pem `openssl x509 -hash -in ca.pem -noout`.0
+# Do an ls on the directory and save the hashed name including the .0 somewhere
+
+certutil -N -d /etc/pki/ldap
+# Do not enter any passwords. When asked, just hit enter beyond this point.
+
+chown root:ldap *
+chmod 640 *
+
+

The scripted way.

+
#!/bin/bash
+# CA Information
+CAcountry="US"
+CAstate="Arizona"
+CAlocale="Phoenix"
+CAorganization="SSN Studio"
+CAorganizationalunit="Channel Maintainers"
+# If you set the below to a hostname, you’re screwed. Don’t do it!
+# Only do it if you have an actual hostname you will do CA signing on!
+CAconicalname="SSN"
+CAemail="tucklesepk@gmail.com"
+# LDAP Server information
+country="US"
+state="Arizona"
+locale="Phoenix"
+organization="SSN Studio"
+organizationalunit="LDAP Server Maintainer"
+conicalname="zera1.angelsofclockwork.net"
+email="pc68xl@gmail.com"
+
+certdir="/etc/pki/ldap"
+
+mkdir $certdir ; cd $certdir
+echo "Enter a password when asked."
+openssl genrsa -des3 -out ca.key 4096
+openssl genrsa -out ldapserver.key 4096
+# Create the self-signed CA cert
+openssl req -new -x509 -key ca.key -out ca.pem -days 3650 -subj /C="$CAcountry"/ST="$CAstate"/L="$CAlocale"/O="$CAorganization"/OU="$CAorganizationalunit"/CN="$CAconicalname"/emailAddress="$CAemail"/
+# Create the LDAP server cert
+openssl req -new -key ldapserver.key -out ldapserver.csr -subj /C="$country"/ST="$state"/L="$locale"/O="$organization"/OU="$organizationalunit"/CN="$conicalname"/emailAddress="$email"/
+# Sign it
+openssl x509 -req -in ldapserver.csr -out ldapserver.pem -CA ca.pem -CAkey ca.key -days 3650 -set_serial 01
+ln -s ca.pem `openssl x509 -hash -in ca.pem -noout`.0
+chown root:ldap *
+chmod 640 *
+
+

Make sure to obtain your hash. Your hash will be different from mine.

+
ls -l /etc/pki/ldap | grep '0'
+39642ab3.0
+
+

LDAP Server Configuration

+

Configurations done in OpenLDAP are done via LDIF. Your passwords should +be hashed as well. Before we begin, let's start by generating a +password for our root DN. This is required.

+
slappasswd 
+New password:
+Re-enter new password: 
+{SSHA}CuaKctEx7rl/+ldG0EjktMzJdrxNc46+
+
+

Keep this SSHA output for our configuration files. Next, we'll need to +make a couple LDIFs.

+

This is our suffix.ldif file. This file helps to create the mdb database +for our LDAP structure. It also sets our DIT suffix, root password, etc. +You should change the olcSuffix, olcRootDN, and olcRootPW to whatever +you plan on using. The olcDbMaxSize is set to 20GB. This is normally +sufficient and can be changed. The olcDbEnvFlags can be changed as well.

+
dn: olcDatabase=mdb,cn=config
+objectClass: olcDatabaseConfig
+objectClass: olcMdbConfig
+olcDatabase: mdb
+olcDbDirectory: /var/lib/ldap
+olcSuffix: dc=angelsofclockwork,dc=net
+olcRootDN: cn=manager,dc=angelsofclockwork,dc=net
+olcRootPW: {SSHA}CuaKctEx7rl/+ldG0EjktMzJdrxNc46+   
+olcDbIndex: objectClass eq,pres
+olcDbIndex: ou,cn,mail,surname,givenname eq,pres,sub
+olcLastMod: TRUE
+olcDbEnvFlags: nometasync
+olcDbEnvFlags: writemap
+olcDbMaxSize: 21474836480
+
+

Now, below we have our primary modification ldif. Comments describe what +each one does.

+
# Sets our cert path and information
+# The "CertificateFile" has to be set to the hostname of the LDAP server
+dn: cn=config
+changetype: modify
+replace: olcTLSCACertificatePath
+olcTLSCACertificatePath: /etc/pki/ldap
+-
+replace: olcTLSCertificateFile
+olcTLSCertificateFile: zera1.angelsofclockwork.net
+-
+replace: olcTLSCertificateKeyFile
+olcTLSCertificateKeyFile: /etc/pki/ldap/ldapserver.key
+
+# Adding a rootDN for the config.
+# Note that this isn't fully necessary as you can use -Y EXTERNAL -H ldapi:/// instead
+# So, treat this as an optional thing. If you do want it, consider a different password.
+dn: olcDatabase={0}config,cn=config
+changetype: modify
+replace: olcRootDN
+olcRootDN: cn=config
+-
+replace: olcRootPW
+olcRootPW: {SSHA}CuaKctEx7rl/+ldG0EjktMzJdrxNc46+
+
+# Set the password again in the mdb database
+# This is because sometimes the password set when making the database doesn't 'work' sometimes
+dn: olcDatabase={2}mdb,cn=config
+changetype: modify
+replace: olcRootPW
+olcRootPW: {SSHA}CuaKctEx7rl/+ldG0EjktMzJdrxNc46+ 
+
+# Sets the default password hash to SSHA -- Refer to the 'bug' information if this does not work
+dn: olcDatabase={-1}frontend,cn=config
+changetype: modify
+replace: olcPasswordHash
+olcPasswordHash: {SSHA}
+
+# Changes the rootdn information in the monitor database
+dn: olcDatabase={1}monitor,cn=config
+changetype: modify
+replace: olcAccess
+olcAccess: {0}to * by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read by dn.base="cn=manager,dc=angelsofclockwork,dc=net" read by * none 
+
+

Let's make sure we turn on ldaps. It's recommended to use TLS, but +some applications insist on SSL. (Very few, but they are out there.)

+
# vi /etc/sysconfig/slapd
+
+. . .
+SLAPD_URLS="ldapi:/// ldap:/// ldaps:///"
+
+# slaptest -u
+Config file testing succeeded
+
+# /etc/openldap/ldap.conf
+. . .
+TLS_CACERTDIR /etc/pki/ldap
+
+

Now, we need to add our LDIFs into LDAP.

+
rm -f /etc/openldap/slapd.d/cn=config/olcDatabase=\{2\}hdb.ldif
+chown -R ldap:ldap /var/lib/ldap
+systemctl enable slapd
+systemctl start slapd
+ldapadd -Y EXTERNAL -H ldapi:/// -f suffix.ldif
+ldapmodify -Y EXTERNAL -H ldapi:/// -f info.ldif
+
+

You may end up getting a checksum error in your logs. To solve this, you +need to do a simple operation against the configuration.

+
ldapmodify -h localhost -xWD "cn=config"
+Enter LDAP Password:
+dn: olcDatabase={0}config,cn=config
+changetype: modify
+replace: olcRootDN
+olcRootDN: cn=config
+modifying entry "olcDatabase={0}config,cn=config"
+slaptest -u
+config file testing succeeded
+
+

That should do it. You can do a -Y EXTERNAL -H ldapi:/// instead if you +wanted to. I did the above to show passwords will work for config.

+

LDAP Structure

+

The next piece is to get our backend structure built. In EL7, core is +the only schema that is there. In EL6, it's a good chunk of these. I +like to put them in a file so I can loop through them.

+
+

Note

+

ppolicy schema

+

As of OpenLDAP 2.6.x, the ppolicy schema no longer applies as it is +built-in to the slapo-ppolicy module. See the upgrade +document +for information.

+
+
/etc/openldap/schema/corba.ldif
+/etc/openldap/schema/cosine.ldif
+/etc/openldap/schema/duaconf.ldif
+/etc/openldap/schema/dyngroup.ldif
+/etc/openldap/schema/inetorgperson.ldif
+/etc/openldap/schema/java.ldif
+/etc/openldap/schema/misc.ldif
+/etc/openldap/schema/nis.ldif
+/etc/openldap/schema/openldap.ldif
+/etc/openldap/schema/collective.ldif 
+
+
+

Note

+

rfc2307

+

If you want to be able to combine groupOfNames and posixGroup together +(similar to Active Directory, other open source, and commercial +offerings), don't use nis. Use the +rfc2307bis +schema instead.

+
+

Once you have your list of schema to put in, we can loop through them.

+
for x in $(cat schemaorder) ; do ldapadd -Y EXTERNAL -H ldapi:/// -f $x ; done
+adding new entry "cn=corba,cn=schema,cn=config"
+adding new entry "cn=cosine,cn=schema,cn=config"
+adding new entry "cn=duaconf,cn=schema,cn=config"
+adding new entry "cn=dyngroup,cn=schema,cn=config"
+adding new entry "cn=inetorgperson,cn=schema,cn=config"
+adding new entry "cn=java,cn=schema,cn=config"
+adding new entry "cn=misc,cn=schema,cn=config"
+adding new entry "cn=nis,cn=schema,cn=config"
+adding new entry "cn=openldap,cn=schema,cn=config"
+adding new entry "cn=collective,cn=schema,cn=config"
+
+

I normally like to keep all LDIFs in a folder by themselves to avoid +clutter (non-configuration LDIF).

+
mkdir ldif ; cd ldif
+
+

Let's get our base created. Make sure to replace my DN with your DN +that you chose earlier. Call this base.ldif.

+
dn: dc=angelsofclockwork,dc=net
+dc: angelsofclockwork
+objectClass: top
+objectClass: domain
+
+dn: ou=People,dc=angelsofclockwork,dc=net
+ou: People
+objectClass: top
+objectClass: organizationalUnit
+
+dn: ou=Group,dc=angelsofclockwork,dc=net
+ou: Group
+objectClass: top
+objectClass: organizationalUnit
+
+
ldapadd -xWD "cn=manager,dc=angelsofclockwork,dc=net" -f base.ldif
+Enter LDAP Password:
+adding new entry "dc=angelsofclockwork,dc=net"
+adding new entry "ou=People,dc=angelsofclockwork,dc=net"
+adding new entry "ou=Group,dc=angelsofclockwork,dc=net"
+
+

If this doesn't add, make sure your LDAP server is running, check +/var/log/messages, and ensure you've completed all steps before this.

+
ldapsearch -x -LLL -b 'dc=angelsofclockwork,dc=net'
+dn: dc=angelsofclockwork,dc=net
+dc: angelsofclockwork
+objectClass: top
+objectClass: domain
+
+dn: ou=People,dc=angelsofclockwork,dc=net
+ou: People
+objectClass: top
+objectClass: organizationalUnit
+
+dn: ou=Group,dc=angelsofclockwork,dc=net
+ou: Group
+objectClass: top
+objectClass: organizationalUnit
+
+

Add Users via Migration

+
+

Note

+

But... I don't want to add my users locally

+

You don't have to add your users locally to the system. This just aids +in the creation of users. Go to the next section if you want to add +users and do permissions by hand.

+
+

This is the fun part. We'll need to add some users, set some passwords +and migrate them into the LDAP system. I'll make three users as an +example, give them an ID starting at 10000, home directories in /lhome, +set a password, and proceed to migrate them. If you don't want to use +/lhome, keep them set to /home and their home directories should get +created automatically when logging into another machine.

+
# mkdir /lhome
+# mkdir ldif/user
+# semanage fcontext -a -t home_root_t "/lhome(/.*)?"
+# restorecon -v /lhome \
+  restorecon reset /lhome context unconfined_u:object_r:default_t:s0->unconfined_u:object_r:home_root_t:s0
+# groupadd -g 10000 sokel
+# groupadd -g 10001 suree
+# groupadd -g 10002 ranos
+# useradd -u 10000 -g 10000 -d /lhome/sokel sokel
+# useradd -u 10001 -g 10001 -d /lhome/suree suree
+# useradd -u 10002 -g 10002 -d /lhome/ranos ranos
+# passwd sokel ; passwd suree ; passwd ranos
+# cat /etc/passwd | grep sokel > ldif/user/passwd.sokel
+# cat /etc/passwd | grep suree > ldif/user/passwd.suree
+# cat /etc/passwd | grep ranos > ldif/user/passwd.ranos
+# cat /etc/group | grep sokel > ldif/user/group.sokel
+# cat /etc/group | grep suree > ldif/user/group.suree
+# cat /etc/group | grep ranos > ldif/user/group.ranos
+
+

We'll set some aliases for our migration scripts too

+
# alias miguser='/usr/share/migrationtools/migrate_passwd.pl'
+# alias miggroup='/usr/share/migrationtools/migrate_group.pl'
+
+

Before we continue, we need to modify our migration scripts. This is +extremely important, otherwise our LDIFs will come out incorrect. Change +them to your DN.

+
# sed -i.bak "s/padl.com/angelsofclockwork.net/g" /usr/share/migrationtools/migrate_common.ph
+# sed -i.bak "s/padl,dc=com/angelsofclockwork,dc=net/g" /usr/share/migrationtools/migrate_common.ph
+
+

Now we can use a loop to convert them. You can do it by hand also, but +that's up to you.

+
# for x in sokel suree ranos ; do miguser ldif/user/passwd.$x > ldif/user/$x.ldif ; done
+# for x in sokel suree ranos ; do miggroup ldif/user/group.$x >> ldif/user/$x.ldif ; done
+# cd ldif/user/
+# cat *.ldif >> /tmp/ourusers.ldif
+# ldapadd -xWD "cn=manager,dc=angelsofclockwork,dc=net" -f /tmp/ourusers.ldif
+Enter LDAP Password:
+adding new entry "uid=ranos,ou=People,dc=angelsofclockwork,dc=net"
+
+adding new entry "cn=ranos,ou=Group,dc=angelsofclockwork,dc=net"
+
+adding new entry "uid=sokel,ou=People,dc=angelsofclockwork,dc=net"
+
+adding new entry "cn=sokel,ou=Group,dc=angelsofclockwork,dc=net"
+
+adding new entry "uid=suree,ou=People,dc=angelsofclockwork,dc=net"
+
+adding new entry "cn=suree,ou=Group,dc=angelsofclockwork,dc=net"
+
+

The manual way.

+
# /usr/share/migrationtools/migrate_passwd.pl ldif/user/passwd.sokel > ldif/user/sokel.ldif 
+# /usr/share/migrationtools/migrate_group.pl ldif/user/group.sokel >> ldif/user/sokel.ldif
+# /usr/share/migrationtools/migrate_passwd.pl ldif/user/passwd.suree > ldif/user/suree.ldif 
+# /usr/share/migrationtools/migrate_group.pl ldif/user/group.suree >> ldif/user/suree.ldif
+# /usr/share/migrationtools/migrate_passwd.pl ldif/user/passwd.ranos > ldif/user/ranos.ldif 
+# /usr/share/migrationtools/migrate_group.pl ldif/user/group.ranos >> ldif/user/ranos.ldif
+
+# cd ldif/user/
+# ls
+group.ranos  group.suree   passwd.sokel  ranos.ldif  suree.ldif
+group.sokel  passwd.ranos  passwd.suree  sokel.ldif
+
+# ldapadd -xWD "cn=manager,dc=angelsofclockwork,dc=net" -f sokel.ldif
+Enter LDAP Password:
+adding new entry "uid=sokel,ou=People,dc=angelsofclockwork,dc=net"
+
+adding new entry "cn=sokel,ou=Group,dc=angelsofclockwork,dc=net"
+
+# ldapadd -xWD "cn=manager,dc=angelsofclockwork,dc=net" -f suree.ldif
+Enter LDAP Password:
+adding new entry "uid=suree,ou=People,dc=angelsofclockwork,dc=net"
+
+adding new entry "cn=suree,ou=Group,dc=angelsofclockwork,dc=net"
+
+# ldapadd -xWD "cn=manager,dc=angelsofclockwork,dc=net" -f ranos.ldif
+Enter LDAP Password:
+adding new entry "uid=ranos,ou=People,dc=angelsofclockwork,dc=net"
+
+adding new entry "cn=ranos,ou=Group,dc=angelsofclockwork,dc=net"
+
+

Add Users via LDIF

+

This is for those who don't want to create the account locally. For +each user, you need to create an LDIF that satisfies their account +information such as UID, GID and their group information. If you plan on +having NFS exports to /lhome, make sure homeDirectory is correctly +pointing as such. Otherwise, keep it as /home/username.

+
dn: uid=zera,ou=People,dc=angelsofclockwork,dc=net
+objectClass: posixAccount
+objectClass: top
+objectClass: shadowAccount
+objectClass: inetOrgPerson
+cn: Zera Nalika
+gidNumber: 11000
+sn: Nalika
+uidNumber: 11000
+givenName: Zera
+uid: zera
+loginShell: /bin/bash
+homeDirectory: /home/zera
+displayName: Zera Nalika
+userPassword: changeme2
+
+dn: cn=zera,ou=Group,dc=angelsofclockwork,dc=net
+objectClass: posixGroup
+objectClass: top
+cn: zera
+gidNumber: 11000
+
+

That's about it for that. You create these for each user as needed and +then add them into ldap.

+
# ldapadd -xWD "cn=manager,dc=angelsofclockwork,dc=net" -f zera.ldif
+adding new entry "uid=zera,ou=People,dc=angelsofclockwork,dc=net"
+
+adding new entry "cn=zera,ou=Group,dc=angelsofclockwork,dc=net"
+
+

For users who are doing the /lhome thing, make their directories. When +you are changing ownership, do it by UID and GID number.

+
# mkdir /lhome
+# semanage fcontext -a -t home_root_t "/lhome(/.*)?"
+# mkdir /lhome/zera
+# cp /etc/skel/.* /lhome/zera
+# chown -R 11000:11000 /lhome/zera
+# restorecon -Rv /lhome
+
+

NFS Export Home Directories

+
+

Warning

+

/home vs /lhome

+

If you used /lhome and you want to use NFS mounts, you may continue +here. Otherwise, skip this section entirely. If you use /home and still +want to do NFS, you will need to do persistent NFS to say /export/home, +and then setup AutoFS to use /export/home as a way to automount into +/home.

+
+
+

Warning

+

Potential Pitfall

+

Do NOT use NFSv3. The steps below show how to prevent user squashing to +allow the user to have access to their home directories. Typically, in +an NFSv4 fashion, it tends to mount it with permissions set to nobody. +Other solutions have been to force NFSv3. This is NOT recommended. +YOU HAVE BEEN WARNED.

+
+

First, we'll need to install nfs-utils, set up our exports, and modify +our id map file.

+
# dnf install nfs-utils libnfsidmap -y
+
+# vi /etc/exports
+/lhome *(rw,sync,root_squash,no_all_squash)
+
+# vi /etc/idmapd.conf
+
+# Comment out the first Domain line and make your own
+Domain = zera1.angelsofclockwork.net
+
+# systemctl start nfs-server
+# systemctl enable nfs-server
+
+

Sometimes you'll still run into the nobody problem. Sometimes this +helps.

+
# vi /etc/sysconfig/nfs
+NEED_IDMAPD=yes
+NFSMAPID_DOMAIN=library.angelsofclockwork.net
+
+

Firewall

+
+

Warning

+

Keep your firewall on

+

It is bad practice to turn your firewall off. Don't do it.

+
+

We need to open up our firewall.

+
+

Note

+

Port Reference

+
    +
  • LDAP Ports: 389/tcp 636/tcp
  • +
  • NFS Ports: 111/tcp 111/udp 2049/tcp
  • +
+
+

If using firewalld, you can add these ports by service.

+
# firewall-cmd --add-service=ldap --zone=public --permanent
+# firewall-cmd --add-service=ldaps --zone=public --permanent
+# firewall-cmd --add-service=nfs --zone=public --permanent
+# firewall-cmd --reload
+
+

Client

+

Setting up the client can be straight-forward or troubling, depending on +the distribution you're using. We'll be going over EL7+. Fedora also +works here as well.

+
+

Warning

+

Third-party Repositories

+

If you use third-party repositories, you may want to disable them, at +least temporarily. Depending on the repository, there may be conflicts +when installing the appropriate packages. You may want to consider on +setting up priorities, and ensure your base and updates are higher than +the rest.

+
+

Enterprise Linux/Current Fedora Releases

+

We'll be using SSSD for this. We need to install some key packages +first. Some of these packages may not install because they were either +superceded or obsoleted.

+
# yum install pki-{ca,common,silent} openldap-clients nss-pam-ldapd policycoreutils-python sssd sssd-common sssd-client sssd-ldap
+
+

Use authselect to configure pam and nss. You'll need to configure +/etc/sssd/sssd.conf by hand after.

+
# authselect select sssd with-mkhomedir with-sudo
+
+# vi /etc/sssd/sssd.conf
+[domain/default]
+
+cache_credentials = True
+krb5_realm = #
+ldap_search_base = dc=angelsofclockwork,dc=net
+id_provider = ldap
+auth_provider = ldap
+chpass_provider = ldap
+sudo_provider = ldap
+ldap_uri = ldap://zera1.angelsofclockwork.net
+ldap_id_use_start_tls = True
+ldap_tls_cacertdir = /etc/openldap/certs
+ldap_tls_cacert = /etc/openldap/certs/ca.pem
+# Add the below
+ldap_sudo_search_base = ou=SUDOers,dc=angelsofclockwork,dc=net
+
+[sssd]
+# Modify this line and add sudo to the list
+services = nss, pam, autofs, sudo
+
+[sudo]
+
+

Now, let's get our CA cert that we made way long ago and download it. +If you used a real CA to sign your certificate, obtain their +certificate.

+
+

Note

+

Hash

+

Remember your hash from when you were making your certificates? You need +to obtain it. In both examples, we created it while using a symbolic +link.

+
+
# scp zera1.angelsofclockwork.net:/etc/pki/ldap/ca.pem /etc/openldap/certs/ca.pem
+# cd /etc/openldap/certs
+# ln -s ca.pem 39642ab3.0
+
+

Now, modify /etc/openldap/ldap.conf and add the following to the bottom, +ensuring your BASE is set correctly.

+
URI ldap://library.angelsofclockwork.net
+BASE dc=angelsofclockwork,dc=net
+ssl start_tls
+
+

You can attempt an ldapsearch and it should work. Search for one of your +users.

+
# ldapsearch -x -LLL uid=zera
+
+dn: uid=zera,ou=People,dc=angelsofclockwork,dc=net
+cn: Zera Nalika
+gidNumber: 11000
+uidNumber: 11000
+givenName: Zera
+objectClass: posixAccount
+objectClass: top
+objectClass: shadowAccount
+objectClass: hostObject
+objectClass: radiusprofile
+objectClass: inetOrgPerson
+objectClass: ldapPublicKey
+uid: zera
+loginShell: /bin/bash
+homeDirectory: /lhome/zera
+displayName: Zera Nalika
+
+

Automounting Home Directories

+

If you chose to do /lhome NFS mounting, proceed here.

+
# mkdir /lhome
+# semanage fcontext -a -t autofs_t "/lhome(/.*)?"
+# restorecon -v /lhome
+# setsebool use_nfs_home_dirs 1
+
+

Now, let's get our automounting setup.

+
# vi /etc/auto.master
+. . .
+/lhome /etc/auto.lhome # Add this under the /misc line
+
+

Let's copy the misc template and make a change to it.

+
# cp /etc/auto.misc /etc/auto.lhome
+# vi /etc/auto.lhome
+
+# Comment the cd line, and add our mount under it.
+#cd             -fstype=iso9660,ro,nosuid,nodev :/dev/cdrom
+*               -rw,soft,intr       zera1.angelsofclockwork.net:/lhome/&
+
+# restorecon -v /etc/auto.lhome
+# systemctl enable autofs
+# systemctl start autofs
+
+

Let's make our change to the idmapd configuration.

+
# vi /etc/idmapd.conf
+
+#Domain = local.domain.edu
+Domain = zera1.angelsofclockwork.net
+
+# systemctl restart sssd autofs
+
+

LDAP Structure Add-ons

+

Here you'll find my value-added portions of getting LDAP going further +than what the above presented.

+

SUDO

+

Getting SUDO to work in LDAP can be a real pain. It doesn't have to be.

+

The default sudo schema provided by the LDAP packages, which I have +taken and converted into the proper olc format.

+
dn: cn=sudo,cn=schema,cn=config
+objectClass: olcSchemaConfig
+cn: sudo
+olcAttributeTypes: {0}( 1.3.6.1.4.1.15953.9.1.1 NAME 'sudoUser' DESC 'User(s)
+ who may  run sudo' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMa
+ tch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
+olcAttributeTypes: {1}( 1.3.6.1.4.1.15953.9.1.2 NAME 'sudoHost' DESC 'Host(s)
+ who may run sudo' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMat
+ ch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
+olcAttributeTypes: {2}( 1.3.6.1.4.1.15953.9.1.3 NAME 'sudoCommand' DESC 'Comma
+ nd(s) to be executed by sudo' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1
+ 466.115.121.1.26 )
+olcAttributeTypes: {3}( 1.3.6.1.4.1.15953.9.1.4 NAME 'sudoRunAs' DESC 'User(s)
+  impersonated by sudo (deprecated)' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1
+ .4.1.1466.115.121.1.26 )
+olcAttributeTypes: {4}( 1.3.6.1.4.1.15953.9.1.5 NAME 'sudoOption' DESC 'Option
+ s(s) followed by sudo' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115
+ .121.1.26 )
+olcAttributeTypes: {5}( 1.3.6.1.4.1.15953.9.1.6 NAME 'sudoRunAsUser' DESC 'Use
+ r(s) impersonated by sudo' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466
+ .115.121.1.26 )
+olcAttributeTypes: {6}( 1.3.6.1.4.1.15953.9.1.7 NAME 'sudoRunAsGroup' DESC 'Gr
+ oup(s) impersonated by sudo' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.14
+ 66.115.121.1.26 )
+olcAttributeTypes: {7}( 1.3.6.1.4.1.15953.9.1.8 NAME 'sudoNotBefore' DESC 'Sta
+ rt of time interval for which the entry is valid' EQUALITY generalizedTimeMat
+ ch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24
+  )
+olcAttributeTypes: {8}( 1.3.6.1.4.1.15953.9.1.9 NAME 'sudoNotAfter' DESC 'End
+ of time interval for which the entry is valid' EQUALITY generalizedTimeMatch
+ ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )
+olcAttributeTypes: {9}( 1.3.6.1.4.1.15953.9.1.10 NAME 'sudoOrder' DESC 'an int
+ eger to order the sudoRole entries' EQUALITY integerMatch ORDERING integerOrd
+ eringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )
+olcObjectClasses: {0}( 1.3.6.1.4.1.15953.9.2.1 NAME 'sudoRole' DESC 'Sudoer En
+ tries' SUP top STRUCTURAL MUST cn MAY ( sudoUser $ sudoHost $ sudoCommand $ s
+ udoRunAs $ sudoRunAsUser $ sudoRunAsGroup $ sudoOption $ sudoOrder $ sudoNotB
+ efore $ sudoNotAfter $ description ) )
+
+

Save this as sudoschema.ldif and add it in.

+
# ldapadd -Y EXTERNAL -H ldapi:/// -f sudoschema.ldif
+
+

Let's create our defaults. This will start our sudo OU and give it some +defaults. You may change these if you so desire.

+
# vi sudo.ldif
+
+dn: ou=SUDOers,dc=angelsofclockwork,dc=net
+objectClass: top
+objectClass: organizationalUnit
+ou: SUDOers
+
+dn: cn=defaults,ou=SUDOers,dc=angelsofclockwork,dc=net
+objectClass: top
+objectClass: sudoRole
+cn: defaults
+description: SUDOers Default values
+sudoOption: requiretty
+sudoOption: env_reset
+sudoOption: env_keep =  "COLORS DISPLAY HOSTNAME HISTSIZE INPUTRC KDEDIR LS_COLORS"
+sudoOption: env_keep += "MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE"
+sudoOption: env_keep += "LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES"
+sudoOption: env_keep += "LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE"
+sudoOption: env_keep += "LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY"
+
+# ldapadd -xWD "cn=manager,dc=angelsofclockwork,dc=net" -f sudo.ldif
+
+

Now, let's create our first SUDO container. It will be for our +\"admins\". We could specify \"sudoHost: ALL\" if we wanted. But for the +example, I chose a couple of hosts.

+
# vi admins.ldif
+
+dn: cn=ADMINS,ou=SUDOers,dc=angelsofclockwork,dc=net
+objectClass: sudoRole
+cn: ADMINS
+description: Administration Role
+sudoCommand: ALL
+sudoHost: zera2.angelsofclockwork.net
+sudoHost: zera3.angelsofclockwork.net
+sudoRunAs: ALL
+sudoRunAsGroup: ALL
+sudoRunAsUser: ALL
+sudoUser: zera
+
+# ldapadd -xWD "cn=manager,dc=angelsofclockwork,dc=net" -f admins.ldif
+
+

If you used authselect with the with-sudo option, this should have +turned on sss for sudoers. You may want to verify /etc/nsswitch.conf +just to be sure.

+
+

Note

+

SSSD Cache

+

Sometimes SSSD likes to cache things or never update things for whatever +reason or another. To get around this, stop sssd, delete everything +under /var/lib/sss/db/ and then start sssd again.

+
+

Now, let's test.

+
[root@zera3 ~]# su - zera
+[zera@zera3 ~]$ sudo -l
+[sudo] password for zera:
+Matching Defaults entries for zera on this host:
+    requiretty, env_reset, env_keep="COLORS DISPLAY HOSTNAME HISTSIZE INPUTRC KDEDIR LS_COLORS", env_keep+="MAIL
+    PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE", env_keep+="LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT
+    LC_MESSAGES", env_keep+="LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE", env_keep+="LC_TIME LC_ALL
+    LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY", secure_path=/sbin\:/bin\:/usr/sbin\:/usr/bin, env_reset, requiretty
+
+User sokel may run the following commands on this host:
+    (ALL : ALL) ALL
+
+

Member Groups

+

Member groups are extremely useful, especially for when you're granting +permissions to external applications (and SSSD if you wish).

+
# vi modules.ldif
+
+dn: cn=module,cn=config
+objectClass: olcModuleList
+cn: module
+olcModulePath: /usr/lib64/openldap
+olcModuleLoad: memberof.la
+
+# vi memberof.ldif
+
+dn: olcOverlay=memberof,olcDatabase={2}mdb,cn=config
+objectClass: olcMemberOf
+objectClass: olcOverlayConfig
+objectClass: olcConfig
+objectClass: top
+olcOverlay: memberof
+olcMemberOfDangling: ignore
+olcMemberOfRefInt: TRUE
+olcMemberOfGroupOC: groupOfNames
+olcMemberOfMemberAD: member
+olcMemberOfMemberOfAD: memberOf
+
+# ldapadd -Y EXTERNAL -H ldapi:/// -f modules.ldif
+# ldapadd -Y EXTERNAL -H ldapI:/// -f memberof.ldif
+
+

After that, we can now create our groups. Example.

+
dn: cn=Admins,ou=Group,dc=angelsofclockwork,dc=net
+objectClass: groupOfNames
+cn: Admins
+member: uid=chris,ou=People,dc=angelsofclockwork,dc=net
+member: uid=zera,ou=People,dc=angelsofclockwork,dc=net
+member: uid=sithlord,ou=People,dc=angelsofclockwork,dc=net
+
+

In SSSD, we can make some minor changes.

+
ldap_search_base = dc=angelsofclockwork,dc=net?sub?|(memberOf=cn=Admins,ou=Group,dc=angelsofclockwork,dc=net)
+ldap_access_filter = (|(memberOf=cn=Admins,ou=Group,dc=angelsofclockwork,dc=net))
+# Change this to rfc2307 if you are using nis
+ldap_schema = rfc2307bis 
+enumerate = True
+
+# systemctl stop sssd ; rm -rf /var/lib/sss/db/* ; systemctl start sssd
+
+

If we were to do an ldapsearch, we can see the groups show up.

+
# ldapsearch -x -LLL uid=zera memberOf
+dn: uid=zera,ou=People,dc=angelsofclockwork,dc=net
+memberOf: cn=Admins,ou=Group,dc=angelsofclockwork,dc=net
+
+

Make sure you turn on referential integrity!

+

Referential Integrity

+

Having referential integrity is absolutely important. It basically means +that if a user gets deleted, their group membership disappears also. +This prevents you from having to clean up manually.

+
# vi module.ldif
+
+dn: cn=module,cn=config
+changetype: modify
+replace: olcModuleLoad
+olcModuleLoad: refint.la
+olcModuleLoad: memberof.la
+
+# ldapmodify -Y EXTERNAL -H ldapi:/// -f module.ldif
+
+

You also need the overlay. An overlay allows certain plugins to work on +a DIT.

+
# vi overlay.ldif
+dn: olcOverlay=refint,olcDatabase={2}mdb,cn=config
+objectClass: olcOverlayConfig
+objectClass: olcConfig
+objectClass: olcRefintConfig
+objectClass: top
+olcOverlay: refint
+olcRefintAttribute: memberOf member manager
+
+# ldapmodify -Y EXTERNAL -H ldapi:/// -f overlay.ldif
+
+

ACL

+

An ACL (Access Control List) allows permissions to be given to those in +the LDAP tree. The problem with a default LDAP setup is that, attributes +like userPassword show up in an ldapsearch. This gives little +protection. So, to get around this issue, we have to create ACLs.

+
+

Note

+

The Manager's Rights

+

The manager has all rights to the DIT. In previous implementations, I +have put him in access controls as a reference and would put \"write\" +as his access. This isn't needed, but it doesn't hurt to have it.

+
+

This ldif creates an ACL that allows the Admins group to do anything +they want on the DIT (similar to manager). This also prevents anonymous +searches from pulling up a user's password.

+
# vi acl.ldif
+
+dn: olcDatabase={2}mdb,cn=config
+changetype: modify
+replace: olcAccess
+olcAccess: {0}to attrs=userPassword,shadowLastChange by group.exact="cn=Admins,ou=Group,dc=angelsofclockwork,dc=net" write by anonymous auth by self write by * none break
+olcAccess: {2}to * by group.exact="cn=Admins,ou=Group,dc=angelsofclockwork,dc=net" write by * read
+olcAccess: {3}to dn.base="" by * read
+
+# ldapmodify -Y EXTERNAL -H ldapi:/// -f acl.ldif
+
+

It's highly recommended, however, to disable anonymous searching, +especially if you go production with LDAP. A lot of LDAP implementations +disallow anonymous searching by default. You can do this with ACLs, but +it's not recommended. We cover this in the search.

+

Disable Anonymous Binding

+

It's recommended to disable anonymous searching. This can be handled by +making a modification to the global configuration and the DIT +configuration.

+
dn: cn=config
+changetype: modify
+add: olcDisallows
+olcDisallows: bind_anon
+
+dn: olcDatabase={2}mdb,cn=config
+changetype: modify
+add: olcRequires
+olcRequires: authc
+
+

Once you add this in, all anonymous searching will cease.

+
# ldapsearch -x -LLL uid=zera
+ldap_bind: Inappropriate authentication (48)
+        additional info: anonymous bind disallowed
+
+

LDAP Logging

+

Logging is of course, very important for an LDAP server. There are a few +types of logs we can do. There are the standard logs and then there are +also audit logs. Audit logs allow an administrator to view changes being +done to LDAP in an LDIF form. We can setup both.

+

Let's create our modification LDIF. This will turn on standard logging +and enable the audit module. Run an ldapmodify against this LDIF.

+
dn: cn=config
+changetype: modify
+replace: olcLogFile
+olcLogFile: /var/log/ldap-standard.log
+-
+replace: olcLogLevel
+olcLogLevel: 256
+
+# Keep in mind, if you have other modules being loaded,
+# add them to the list
+dn: cn=module,cn=config
+changetype: modify
+replace: olcModuleLoad
+olcModuleLoad: refint.la
+olcModuleLoad: memberof.la
+olcModuleLoad: auditlog.la
+
+

Now, we need to make sure audit logging is done on our database.

+
dn: olcOverlay=auditlog,olcDatabase={2}mdb,cn=config
+objectClass: olcAuditlogConfig
+objectClass: olcOverlayConfig
+olcOverlay: auditlog
+olcAuditlogFile: /var/log/ldap-audit.log
+
+

It's recommended to have logrotate working for our logs. Here is a file +I've dropped into /etc/logrotate.d. Experiment with these options. +Since I work in an environment that has tons of transactions going all +the time, and thus, my rotations are at 100M and 250M respectively.

+
/var/log/ldap-standard.log {
+missingok
+compress
+notifempty
+daily
+rotate 10
+size=100M
+}
+
+/var/log/ldap-audit.log {
+missingok
+compress
+notifempty
+daily
+rotate 10
+size=250M
+}
+
+

In /etc/rsyslog.conf, optionally, you can create this. If you find that +logs are not appearing after the changes above, use this.

+
local4.*                   /var/log/ldap.log
+
+

Password Policy

+

Password policies are a great asset, especially when working in an +environment that have or require security policies. First, let's load +our module and then add our overlay. This LDIF will do both. You may +want to remove the comments before adding.

+
dn: cn=module,cn=config
+changetype: modify
+replace: olcModuleLoad
+olcModuleLoad: refint.la
+olcModuleLoad: memberof.la
+olcModuleLoad: auditlog.la
+olcModuleLoad: ppolicy.la
+
+dn: olcOverlay=ppolicy,olcDatabase={2}mdb,cn=config
+objectClass: olcOverlayConfig
+objectClass: olcPPolicyConfig
+olcOverlay: ppolicy
+olcPPolicyDefault: cn=default,ou=policies,dc=angelsofclockwork,dc=net
+# Set the below to TRUE if you want users to get locked out after failed attempted
+olcPPolicyUseLockout: TRUE
+# Set the below to TRUE if you want passwords to be hashed.
+# HIGHLY RECOMMENDED YOU SET THIS TO TRUE
+olcPPolicyHashCleartext: TRUE
+
+

Now, we need an LDIF to create our standard password policy. It's +important to have a default password policy and then create separate +ones as needed. Make sure to read the comments. You may want to remove +the comments before adding.

+
dn: cn=default,ou=policies,dc=angelsofclockwork,dc=net
+objectClass: pwdPolicy
+objectClass: person
+objectClass: top
+cn: passwordDefault
+sn: passwordDefault
+pwdAttribute: userPassword
+# If set to 0, quality is not checked.
+# If set to 1, quality is checked by an internal module which you setup.
+# If set to 2, the system used to change the password must have a checking mechanism.
+# Pick your poison.
+pwdCheckQuality: 0
+# Password lives for 84 days
+pwdMinAge: 0
+pwdMaxAge: 7257600
+# Minimum length is 7
+pwdMinLength: 7
+# Password history of 10, cannot use a password that's in history
+pwdInHistory: 10
+# 5 Failures till a lockout, 10 minutes for it to reset, 30 minute lockout.
+pwdMaxFailure: 5
+pwdFailureCountInterval: 600
+pwdLockout: TRUE
+pwdLockoutDuration: 1800
+# A user can change their own password.
+pwdAllowUserChange: TRUE
+# Systems that authenticate to LDAP can warn 14 days before an expiration
+pwdExpireWarning: 1209600
+# Allowed binds on an expired password.
+pwdGraceAuthNLimit: 5
+pwdMustChange: TRUE
+pwdSafeModify: FALSE
+
+

In the instance you want to use the built-in module for password +checking, your LDIF would have these lines.

+
pwdCheckQuality: 1
+pwdCheckModule: check_password.so
+
+
+
+ + + + + + + + + + + + diff --git a/el/pxeboot/index.html b/el/pxeboot/index.html new file mode 100644 index 00000000..d9e953f0 --- /dev/null +++ b/el/pxeboot/index.html @@ -0,0 +1,839 @@ + + + + + + + + + + + PXE (with grub2) - Linux Guide and Hints + + + + + + + + + + + + + + + +
+
+
+
+ +

This page goes over setting up a pxeboot system using tftp on Enterprise +Linux or Fedora

+

Requirements

+

Here are the list of requirements below.

+
    +
  • Enterprise Linux 8, 9, or Fedora
  • +
  • A DHCP server setup that allows you to setup the next_server directive or setup the tftp server location
  • +
  • Optionally if you are using a local mirror, httpd or nginx installed. (This guide assumes httpd)
  • +
+

Tutorial Preface, Notes, and Recommendations

+

In some environments, it may be better (or easier, depending on your +perspective) to setup a PXE server and roll out systems in a lab or +otherwise in that fashion. It's one of the most straight forward ways +to build out systems easily and consistently. The difference between a +typical PXE setup and this is we're using grub2 menus, rather than the +classic menu style. This makes it simpler to keep all configurations +consistent between classic boot and EFI boot.

+

If you plan on using supporting other architectures, it will be easier +to use that architecture to run the grub2-mknetdir command and brings +those to your tftp server.

+

Cobbler

+

While cobbler is a perfectly viable solution to setting up a pxeboot +system for various distros and configurations, it is out of scope for +this article. It is unknown if it sets up or directly supports grub2.

+

Server Setup

+

This section goes over the server setup portion for the tftp server.

+

TFTP

+

Let's install the tftpserver package plus some additional grub +packages. If you are wanting other architectures, you can obtain the +other grub2 module packages from your distribution's BaseOS or +equivalent repository for that architecture and install it manually.

+
# x86_64
+% dnf install \
+  grub2-efi-x64-modules \
+  grub2-tools-extra \
+  grub2-pc-modules \
+  shim-ia32 \              # this does not exist on el9+
+  tftp-server
+
+# aarch64
+% dnf install \
+  grub2-efi-aa64-modules \
+  grub2-tools-extra \
+  tftp-server
+
+

Let's make our initial net directories and ensure the selinux contexts +are correct.

+
% grub2-mknetdir --net-directory /var/lib/tftpboot/
+Netboot directory for i386-pc created. Configure your DHCP server to point to /srv/tftp/boot/grub2/i386-pc/core.0
+Netboot directory for x86_64-efi created. Configure your DHCP server to point to /srv/tftp/boot/grub2/x86_64-efi/core.efi
+
+% restorecon -R /var/lib/tftpboot
+
+

Now you'll need to enable the tftp socket and open the port. +Traditionally, you would use xinetd. It's no longer required for the +tftp service.

+
# Note: This is port 69 with the UDP protocol
+% firewall-cmd --add-service=tftp --permanent
+% systemctl enable tftp.socket --now
+
+

DHCP (ISC)

+

On your DHCP server configuration (typically /etc/dhcp/dhcpd.conf if +running on Fedora or EL), you should set the following options:

+
option pxe-system-type code 93 = unsigned integer 16;
+option rfc3442-classless-static-routes code 121 = array of integer 8;
+option ms-classless-static-routes code 249 = array of integer 8;
+
+option space pxelinux;
+option pxelinux.magic code 208 = string;
+option pxelinux.configfile code 209 = text;
+option pxelinux.pathprefix code 210 = text;
+option pxelinux.reboottime code 211 = unsigned integer 32;
+option architecture-type   code 93 = unsigned integer 16;
+option pxelinux.mtftp-ip    code 1 = ip-address;
+option pxelinux.mtftp-cport code 2 = unsigned integer 16;
+option pxelinux.mtftp-sport code 3 = unsigned integer 16;
+option pxelinux.mtftp-tmout code 4 = unsigned integer 8;
+option pxelinux.mtftp-delay code 5 = unsigned integer 8;
+
+

Whether this section is within a subnet block or not, it is needed to +ensure the right bootloader is called. Note that we're only loading +x86. If you are loading armhfp, use 00:0a. If you are loading aarch64, +use 00:0b.

+
class "pxeclients" {
+        match if substring (option vendor-class-identifier, 0, 9) = "PXEClient";
+        # x86_64 EFI
+        if option pxe-system-type = 00:07 {
+                filename "boot/grub2/x86_64-efi/core.efi";
+        } else if option pxe-system-type = 00:08 {
+                filename "boot/grub2/x86_64-efi/core.efi";
+        } else if option pxe-system-type = 00:09 {
+                filename "boot/grub2/x86_64-efi/core.efi";
+        } else {
+                # BIOS boot only
+                filename "boot/grub2/i386-pc/core.0";
+        }
+}
+
+

Note that in your subnet blocks, you should also mention next_server, +which should point to your TFTP server. The DHCP and TFTP server can be +on the same machine and there's nothing stopping you from doing that; +next_server needs to be set regardless here. See an example below of a +full work dhcpd.conf.

+
ddns-update-style interim;
+
+allow booting;
+allow bootp;
+authoritative;
+log-facility local6;
+
+ignore client-updates;
+set vendorclass = option vendor-class-identifier;
+
+## Allowing EFI Clients
+option pxe-system-type code 93 = unsigned integer 16;
+option rfc3442-classless-static-routes code 121 = array of integer 8;
+option ms-classless-static-routes code 249 = array of integer 8;
+
+option space pxelinux;
+option pxelinux.magic code 208 = string;
+option pxelinux.configfile code 209 = text;
+option pxelinux.pathprefix code 210 = text;
+option pxelinux.reboottime code 211 = unsigned integer 32;
+option architecture-type code 93 = unsigned integer 16;
+
+option pxelinux.mtftp-ip    code 1 = ip-address;
+option pxelinux.mtftp-cport code 2 = unsigned integer 16;
+option pxelinux.mtftp-sport code 3 = unsigned integer 16;
+option pxelinux.mtftp-tmout code 4 = unsigned integer 8;
+option pxelinux.mtftp-delay code 5 = unsigned integer 8;
+
+subnet 10.100.0.0 netmask 255.255.255.0 {
+        interface               br1000;
+        option routers          10.100.0.1;
+        option domain-name-servers      10.100.0.1, 10.100.0.231;
+        option domain-name              "angelsofclockwork.net";
+        option subnet-mask              255.255.255.0;
+        range           10.100.0.110 10.100.0.199;
+        ## EFI Client Catch
+        class "pxeclients" {
+                match if substring (option vendor-class-identifier, 0, 9) = "PXEClient";
+                if option pxe-system-type = 00:07 {
+                        filename "boot/grub2/x86_64-efi/core.efi";
+                } else if option pxe-system-type = 00:08 {
+                        filename "boot/grub2/x86_64-efi/core.efi";
+                } else if option pxe-system-type = 00:09 {
+                        filename "boot/grub2/x86_64-efi/core.efi";
+                } else if option pxe-system-type = 00:0a {
+                        filename "boot/grub2/armv7a-efi/core.efi";
+                } else if option pxe-system-type = 00:0b {
+                        filename "boot/grub2/aarch64-efi/core.efi";
+                } else {
+                        filename "boot/grub2/i386-pc/core.0";
+                }
+        }
+        default-lease-time      21600;
+        max-lease-time  43200;
+        next-server     10.100.0.1;
+}
+
+

Ensure that the dhcpd service is restarted after making the necessary +changes.

+

DHCP (Kea)

+

Kea is a different configuration style from ISC. At this time, we do not +have a full working example.

+

Web Server (httpd)

+

If we plan on hosting the installation mirror in your environment, it's +recommended to stand up a simple web server. It does not require any +kind of special configuration. We'll use the default /var/www/html/ +path. If you wish to use another such as /srv/www, you will need to +setup a virtual host (this is outside the scope of this page).

+
% dnf install httpd -y
+% systemctl enable httpd --now
+% firewall-cmd --add-service=http --permanent
+% firewall-cmd --complete-reload
+
+# create the directories for our distributions
+% mkdir -p /var/www/html/os/{fedora,centos,rocky}
+
+

Setting up Grub

+

When you run grub2-mknetdir, it created a core.* set of files. An +accompanying grub.cfg must sit next to them. To prevent a duplication of +work, it can be simplified by making all grub configurations at +/var/lib/tftpboot and then symlink them next to each directory +containing core.*. Let's make a very, very simple one.

+
set default=0
+set timeout=60
+menuentry 'EFI Firmware System Setup' $menuentry_id_option 'uefi-firmware' {
+  fwsetup
+}
+
+menuentry 'Reboot' {
+  reboot
+}
+
+menuentry 'Shutdown' {
+  halt
+}
+
+

Now let's just symlink it.

+
% cd /var/lib/tftpboot/boot/grub2/x86_64-efi
+% ln -s ../../../grub.cfg
+% cd /var/lib/tftpboot/boot/grub2/i386-pc
+% ln -s ../../../grub.cfg
+
+

This should produce a grub menu for both EFI and BIOS systems that +contain three bootable options.

+

Adding Distributions

+

Now that grub is sort of setup, we should add a distribution to it at +least. Below are a couple examples using Fedora, Rocky Linux, and CentOS +Stream.

+

Rocky Linux

+

Setting up Rocky Linux (or any other Enterprise Linux distribution) +should be straight forward. We'll download both Rocky Linux 8 and Rocky +Linux 9 and setup the menus.

+
+

Note

+

If you plan on not hosting a mirror of the base repositories, ensure +that your inst.repo/inst.stage2 commands are accurate to a mirror of +your choice.

+
+

The below assumes we are hosting a mirror of the downloaded ISO, which +will make installations quicker as it'll be confined to your network.

+
% cd /var/tmp
+# Rocky Linux 8
+% wget https://dl.rockylinux.org/pub/rocky/8/isos/x86_64/Rocky-8-latest-x86_64-dvd.iso
+# Rocky Linux 9
+% wget https://dl.rockylinux.org/pub/rocky/9/isos/x86_64/Rocky-9-latest-x86_64-dvd.iso
+
+# Optionally, if you plan on supporting ARM...
+% wget https://dl.rockylinux.org/pub/rocky/8/isos/aarch64/Rocky-8-latest-aarch64-dvd.iso
+% wget https://dl.rockylinux.org/pub/rocky/9/isos/aarch64/Rocky-9-latest-aarch64-dvd.iso
+
+

Here we'll copy the data we want into the necessary directories. Any +pxeboot related images will go to /var/lib/tftpboot/rocky-X-ARCH (X +being the major version, ARCH being the architecture). If we are keeping +a local mirror of the DVD, we'll put it into +/var/www/html/os/rocky/X/ARCH. Below is for x86_64, but the same steps +can be repeated for aarch64 without any issues. Just replace x86_64 +with aarch64.

+
## Rocky 8
+% mount -o loop Rocky-8-latest-x86_64-dvd.iso /mnt
+% mkdir /var/lib/tftpboot/rocky-8-x86_64
+% cp /mnt/images/pxeboot/* /var/lib/tftpboot/rocky-8-x86_64
+% mkdir -p /var/www/html/os/rocky/8/x86_64
+% rsync -vrlptDSH --delete /mnt/ /var/www/html/os/rocky/8/x86_64
+% umount /mnt
+
+## Rocky 9
+% mount -o loop Rocky-9-latest-x86_64-dvd.iso /mnt
+% mkdir /var/lib/tftpboot/rocky-9-x86_64
+% cp /mnt/images/pxeboot/* /var/lib/tftpboot/rocky-9-x86_64
+% mkdir -p /var/www/html/os/rocky/9/x86_64
+% rsync -vrlptDSH --delete /mnt/ /var/www/html/os/rocky/9/x86_64
+% umount /mnt
+
+% restorecon -R /var/www/html/os/rocky
+
+

At this point, we'll need to setup the grub menus. We'll setup +non-kickstart examples for BIOS and UEFI.

+
. . .
+# Rocky 8
+menuentry 'Install Rocky Linux 8 (No KS) (UEFI)' --class fedora --class gnu-linux --class gnu --class os {
+  echo "Loading Rocky Linux 8 kernel..."
+  linuxefi rocky-8-x86_64/vmlinuz inst.repo=http://10.100.0.1/os/rocky/8/x86_64 inst.stage2=http://10.100.0.1/os/rocky/8/x86_64 ip=dhcp
+  initrdefi rocky-8-x86_64/initrd.img
+}
+menuentry 'Install Rocky Linux 8 (No KS) (BIOS)' --class fedora --class gnu-linux --class gnu --class os {
+  echo "Loading Rocky Linux 8 kernel..."
+  linux16 rocky-8-x86_64/vmlinuz inst.repo=http://10.100.0.1/os/rocky/8/x86_64 inst.stage2=http://10.100.0.1/os/rocky/8/x86_64 ip=dhcp
+  initrd16 rocky-8-x86_64/initrd.img
+}
+
+# if you are setting up arm...
+menuentry 'Install Rocky Linux 8 (No KS) (aarch64)' --class fedora --class gnu-linux --class gnu --class os {
+  echo "Loading Rocky Linux 8 kernel..."
+  linux rocky-9-aarch64/vmlinuz inst.repo=http://10.100.0.1/os/rocky/8/aarch64 inst.stage2=http://10.100.0.1/os/rocky/8/aarch64 ip=dhcp
+  initrd rocky-9-aarch64/initrd.img
+}
+
+
. . .
+# Rocky 9
+menuentry 'Install Rocky Linux 9 (No KS) (UEFI)' --class fedora --class gnu-linux --class gnu --class os {
+  echo "Loading Rocky Linux 9 kernel..."
+  linuxefi rocky-9-x86_64/vmlinuz inst.repo=http://10.100.0.1/os/rocky/9/x86_64 inst.stage2=http://10.100.0.1/os/rocky/9/x86_64 ip=dhcp
+  initrdefi rocky-9-x86_64/initrd.img
+}
+menuentry 'Install Rocky Linux 9 (No KS) (BIOS)' --class fedora --class gnu-linux --class gnu --class os {
+  echo "Loading Rocky Linux 9 kernel..."
+  linux16 rocky-9-x86_64/vmlinuz inst.repo=http://10.100.0.1/os/rocky/9/x86_64 inst.stage2=http://10.100.0.1/os/rocky/9/x86_64 ip=dhcp
+  initrd16 rocky-9-x86_64/initrd.img
+}
+
+# if you are setting up arm...
+menuentry 'Install Rocky Linux 9 (No KS) (aarch64)' --class fedora --class gnu-linux --class gnu --class os {
+  echo "Loading Rocky Linux 9 kernel..."
+  linux rocky-9-aarch64/vmlinuz inst.repo=http://10.100.0.1/os/rocky/9/aarch64 inst.stage2=http://10.100.0.1/os/rocky/9/aarch64 ip=dhcp
+  initrd rocky-9-aarch64/initrd.img
+}
+
+

The Rocky Linuxinstallation should now be bootable.

+

CentOS Stream

+

Much like Rocky Linux (or other derivatives), the path is the same for +setting it up.

+
+

Using upstream mirror path

+

If you plan on not hosting a mirror of the base repositories, ensure +that your inst.repo/inst.stage2 commands are accurate to a mirror of +your choice.

+
+
% cd /var/tmp
+# CentOS Stream 9
+% wget -O CentOS-Stream-9-latest-x86_64-dvd1.iso \
+  'https://mirrors.centos.org/mirrorlist?path=/9-stream/BaseOS/x86_64/iso/CentOS-Stream-9-latest-x86_64-dvd1.iso&redirect=1&protocol=https'
+
+# Optionally, if you plan on supporting ARM...
+% wget -O CentOS-Stream-9-latest-aarch64-dvd1.iso \
+  'https://mirrors.centos.org/mirrorlist?path=/9-stream/BaseOS/aarch64/iso/CentOS-Stream-9-latest-aarch64-dvd1.iso&redirect=1&protocol=https'
+
+

Here we'll copy the data we want into the necessary directories. Any +pxeboot related images will go to /var/lib/tftpboot/rocky-X-ARCH (X +being the major version, ARCH being the architecture). If we are keeping +a local mirror of the DVD, we'll put it into +/var/www/html/os/rocky/X/ARCH. Below is for x86_64, but the same steps +can be repeated for aarch64 without any issues. Just replace x86_64 +with aarch64.

+
## CentOS Stream 9
+% mount -o loop CentOS-Stream-9-latest-x86_64-dvd1.iso /mnt
+% mkdir /var/lib/tftpboot/centos-9-x86_64
+% cp /mnt/images/pxeboot/* /var/lib/tftpboot/centos-9-x86_64
+% mkdir -p /var/www/html/os/centos/9/x86_64
+% rsync -vrlptDSH --delete /mnt/ /var/www/html/os/centos/9/x86_64
+% restorecon -R /var/www/html/os/centos/9
+% umount /mnt}
+
+

At this point, we'll need to setup the grub menus. We'll setup +non-kickstart examples for BIOS and UEFI.

+
. . .
+# CentOS Stream 9
+menuentry 'Install CentOS Stream 9 (No KS) (UEFI)' --class fedora --class gnu-linux --class gnu --class os {
+  echo "Loading CentOS Stream 9 kernel..."
+  linuxefi centos-9-x86_64/vmlinuz inst.repo=http://10.100.0.1/os/centos/9/x86_64 inst.stage2=http://10.100.0.1/os/centos/9/x86_64 ip=dhcp
+  initrdefi centos-9-x86_64/initrd.img
+}
+menuentry 'Install CentOS Stream 9 (No KS) (BIOS)' --class fedora --class gnu-linux --class gnu --class os {
+  echo "Loading CentOS Stream 9 kernel..."
+  linux16 centos-9-x86_64/vmlinuz inst.repo=http://10.100.0.1/os/centos/9/x86_64 inst.stage2=http://10.100.0.1/os/centos/9/x86_64 ip=dhcp
+  initrd16 centos-9-x86_64/initrd.img
+}
+
+# if you are setting up arm...
+menuentry 'Install CentOS Stream 9 (No KS) (aarch64)' --class fedora --class gnu-linux --class gnu --class os {
+  echo "Loading CentOS Stream 9 kernel..."
+  linux centos-9-aarch64/vmlinuz inst.repo=http://10.100.0.1/os/centos/9/aarch64 inst.stage2=http://10.100.0.1/os/centos/9/aarch64 ip=dhcp
+  initrd centos-9-aarch64/initrd.img
+}
+
+

The CentOS Stream installation should now be bootable.

+

Fedora

+

Let's put up a regular installer with no kickstart for Fedora. This +does not involve pulling down any ISO's and will rely entirely on using +upstream repositories.

+
% cd /var/lib/tftpboot
+% mkdir fedora-x86_64
+% cd fedora-x86_64
+# Replace XX with the current fedora version
+% wget https://dl.fedoraproject.org/pub/fedora/linux/releases/XX/Everything/x86_64/os/images/pxeboot/initrd.img
+% wget https://dl.fedoraproject.org/pub/fedora/linux/releases/XX/Everything/x86_64/os/images/pxeboot/vmlinuz
+
+# If you want arm systems... aarch64
+% cd ..
+% mkdir fedora-aarch64
+# Replace XX with the current fedora version
+% wget https://dl.fedoraproject.org/pub/fedora/linux/releases/XX/Everything/aarch64/os/images/pxeboot/initrd.img
+% wget https://dl.fedoraproject.org/pub/fedora/linux/releases/XX/Everything/aarch64/os/images/pxeboot/vmlinuz
+
+

Now we can add a couple menu entry items for Fedora. I'm making both +EFI and Classic entries to ensure we can boot both EFI and BIOS systems +from the same menu.

+
. . .
+menuentry 'Install Fedora Linux (EFI)' --class fedora --class gnu-linux --class gnu --class os {
+  linuxefi fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os ip=dhcp
+  initrdefi fedora-x86_64/initrd.img
+}
+menuentry 'Install Fedora Linux (Classic)' --class fedora --class gnu-linux --class gnu --class os {
+  linux16 fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ ip=dhcp
+  initrd16 fedora-x86_64/initrd.img
+}
+# Add the below for ARM systems
+menuentry 'Install Fedora Linux (ARM)' --class fedora --class gnu-linux --class gnu --class os {
+  linux fedora-aarch64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os/ ip=dhcp
+  initrd fedora-aarch64/initrd.img
+}
+
+

Now the Fedora installation should be bootable.

+

Customizing Grub

+

Grub is customizable. It is possible to setup background images, choose +menu colors, setup themes, and so on.

+

Colors and Backgrounds

+

Colors and backgrounds are perfectly possible, even in an EFI setup. +We'll need to load a few modules and then set the colors and background +we want. Note that if you're using a background, it should live in +/var/lib/tftpboot to make things easier.

+
. . .
+insmod all_video
+insmod gfxterm
+insmod gfxterm_menu
+insmod gfxmenu
+insmod gfxterm_background
+insmod png
+terminal_output gfxterm
+background_image -m stretch /bg.png
+
+set menu_color_highlight=cyan/black
+set menu_color_normal=white/black
+set color_normal=white/black
+. . .
+
+

The background would be /var/lib/tftpboot/bg.png in this example. +Selected items will appear to be cyan and the typical gray selection box +is now transparent, which is done by setting it to black. Everything +else should appear as white text with a transparent background. Example +below.

+

image

+

Special Submenus

+

Submenus are easily created using submenu in the grub configuration. For +example:

+
submenu 'Fedora Linux' --class fedora --class gnu-linux --class gnu --class os {
+   set menu_color_highlight=black/light-cyan
+   set menu_color_normal=white/black
+   set color_normal=white/black
+
+   menuentry 'Install Fedora Linux (EFI)' --class fedora --class gnu-linux --class gnu --class os {
+     linuxefi fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os ip=dhcp
+     initrdefi fedora-x86_64/initrd.img
+   }
+   menuentry 'Install Fedora Linux (Classic)' --class fedora --class gnu-linux --class gnu --class os {
+     linux16 fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ ip=dhcp
+     initrd16 fedora-x86_64/initrd.img
+   }
+   menuentry 'Install Fedora Linux (ARM)' --class fedora --class gnu-linux --class gnu --class os {
+     linux fedora-aarch64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os/ ip=dhcp
+     initrd fedora-aarch64/initrd.img
+   }
+}
+
+

This now means "Fedora Linux" will show up as a menu option and it +will take you to a brand new menu with the two listed items, and another +color scheme. Note that we created color items because submenus will +reset the theme options. Example of how it looks is below.

+

grub menu example

+

It is also possible to place everything into separate source-able files. +Note that when you do this, you will need to symlink those files just +like you did with grub.cfg.

+
submenu 'Fedora Linux' --class fedora --class gnu-linux --class gnu --class os {
+  set menu_color_highlight=black/light-cyan
+  set menu_color_normal=white/black
+  set color_normal=white/black
+  source fedora.cfg
+}
+
+
menuentry 'Install Fedora Linux (EFI)' --class fedora --class gnu-linux --class gnu --class os {
+  linuxefi fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os ip=dhcp
+  initrdefi fedora-x86_64/initrd.img
+}
+menuentry 'Install Fedora Linux (Classic)' --class fedora --class gnu-linux --class gnu --class os {
+  linux16 fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ ip=dhcp
+  initrd16 fedora-x86_64/initrd.img
+}
+menuentry 'Install Fedora Linux (ARM)' --class fedora --class gnu-linux --class gnu --class os {
+  linux fedora-aarch64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os/ ip=dhcp
+  initrd fedora-aarch64/initrd.img
+}
+
+

Submenus can be nested too. Here's a deeper, working example of my own +setup using Fedora 35.

+
# grub.cfg
+set default=0
+set timeout=60
+insmod all_video
+insmod gfxterm
+insmod gfxterm_menu
+insmod gfxmenu
+insmod gfxterm_background
+insmod png
+terminal_output gfxterm
+loadfont /unicode.pf2
+background_image -m stretch /bg.png
+
+set menu_color_highlight=cyan/black
+set menu_color_normal=white/black
+set color_normal=white/black
+
+submenu 'Fedora Linux' --class fedora --class gnu-linux --class gnu --class os {
+  set menu_color_highlight=black/light-cyan
+  set menu_color_normal=white/black
+  set color_normal=white/black
+  source fedora.cfg
+}
+
+menuentry 'EFI System Setup' $menuentry_id_option 'uefi-firmware' {
+  fwsetup
+}
+
+menuentry 'Reboot' {
+  reboot
+}
+
+menuentry 'Shutdown' {
+  halt
+}
+
+# fedora.cfg
+submenu 'Fedora Linux (latest stable)' --class fedora --class gnu-linux --class gnu --class os {
+  set menu_color_highlight=black/light-cyan
+  set menu_color_normal=white/black
+  set color_normal=white/black
+
+  # EFI Only
+  submenu 'EFI Mode' --class fedora --class gnu-linux --class gnu --class os {
+    set menu_color_highlight=black/light-cyan
+    set menu_color_normal=white/black
+    set color_normal=white/black
+
+    menuentry 'Install Fedora Linux (No KS)' --class fedora --class gnu-linux --class gnu --class os {
+      linuxefi fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os ip=dhcp
+      initrdefi fedora-x86_64/initrd.img
+    }
+
+    menuentry 'Install Fedora Linux' --class fedora --class gnu-linux --class gnu --class os {
+      linuxefi fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os ip=dhcp
+      initrdefi fedora-x86_64/initrd.img
+    }
+
+    menuentry 'Fedora Linux (Rescue Mode)' --class fedora --class gnu-linux --class gnu --class os {
+      linuxefi fedora-x86_64/vmlinuz inst.rescue inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os
+      initrdefi fedora-x86_64/initrd.img
+    }
+  }
+
+  # Classic Only
+  submenu 'Classic Mode' --class fedora --class gnu-linux --class gnu --class os {
+    set menu_color_highlight=black/light-cyan
+    set menu_color_normal=white/black
+    set color_normal=white/black
+
+    menuentry 'Install Fedora Linux (No KS)' --class fedora --class gnu-linux --class gnu --class os {
+      linux16 fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ ip=dhcp
+      initrd16 fedora-x86_64/initrd.img
+    }
+
+     menuentry 'Install Fedora Linux' --class fedora --class gnu-linux --class gnu --class os {
+      linux16 fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ ip=dhcp
+      initrd16 fedora-x86_64/initrd.img
+    }
+
+    menuentry 'Fedora Linux (Rescue Mode)' --class fedora --class gnu-linux --class gnu --class os {
+      linux16 fedora-x86_64/vmlinuz inst.rescue inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/
+      initrd16 fedora-x86_64/initrd.img
+    }
+  }
+
+  # EFI mode for ARM
+  submenu 'EFI Mode (aarch64)' --class fedora --class gnu-linux --class gnu --class os {
+    set menu_color_highlight=black/light-cyan
+    set menu_color_normal=white/black
+    set color_normal=white/black
+
+    menuentry 'Install Fedora Linux (No KS)' --class fedora --class gnu-linux --class gnu --class os {
+      linuxefi fedora-aarch64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os ip=dhcp
+      initrdefi fedora-aarch64/initrd.img
+    }
+
+    menuentry 'Install Fedora Linux' --class fedora --class gnu-linux --class gnu --class os {
+      linuxefi fedora-aarch64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os ip=dhcp
+      initrdefi fedora-aarch64/initrd.img
+    }
+
+    menuentry 'Fedora Linux (Rescue Mode)' --class fedora --class gnu-linux --class gnu --class os {
+      linuxefi fedora-aarch64/vmlinuz inst.rescue inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os
+      initrdefi fedora-aarch64/initrd.img
+    }
+  }
+}
+
+
+
+ + + + + + + + + + + + diff --git a/el/sysadmin/index.html b/el/sysadmin/index.html new file mode 100644 index 00000000..254db0fd --- /dev/null +++ b/el/sysadmin/index.html @@ -0,0 +1,673 @@ + + + + + + + + + + + The System Administrator Experience - Linux Guide and Hints + + + + + + + + + + + + + + + +
+
+
+
+ +

This write up provides steps on the System Administrator experience. +This is not an end-all, be-all, and has many variables to keep in mind. +But can provide a baseline for you.

+

Please keep in mind, this is for Red Hat based distributions, mainly +Enterprise Linux 8 and 9. CentOS Stream works within reason.

+

Also note that it will be recommended that you do things in ansible. The +RHCE for RHEL 9 will require you to be able to use ansible. As such, we +will be focusing on RHEL 9.

+

Recommendations

+
+

Note

+

Software Replacements

+
    +
  • Postgresql can be replaced with MySQL/MariaDB
  • +
  • Use Katello/Foreman, straight Pulp, or Uyuni
  • +
  • You can use any hypervisor other than KVM if you wish, with + specific caveats
  • +
  • nagios can be replaced with icinga
  • +
  • You can replace firewalld with the regular nftables service. This + may be required for your virtual host
  • +
+
+
+

Note

+

General Notes

+
    +
  • It's recommended to use colored vim syntax. Root doesn't use vim + when vim-enhanced is installed. You can make an alias for vi to run + vim (not recommended).
  • +
  • Turn on syntaxing in ~/.vimrc with syntax on
  • +
  • Make the vim colors brighter in ~/.vimrc with set background=dark
  • +
  • Export your EDITOR variable in ~/.bash_profile with export EDITOR=vim
  • +
  • Keep selinux set to enforcing
  • +
+
+
+

Note

+

Hardware Requirements

+
    +
  • RAM - Minimum: 32GB, Recommended: 64GB
  • +
  • CPU - Minimum: Intel or AMD Quad Core, Recommended: 8 Core with HyperThreading
  • +
  • Storage - Minimum: 4TB, Recommended: 8TB
  • +
  • Network - minium 1gb link recommended
  • +
+
+

Please consider on building an actual lab machine that you can do this +on.

+

Certification Completions

+

Certification guidelines will be updated later.

+

Notes and Changelog

+
+

Note

+

Post Experience Notes

+

While this write up uses KVM exclusively, you may want to enhance your +learning after the fact by setting up another virtualization platform +on your virtual host. It may require you to redesign everything or +even start over, but it is something you can consider which you like +best overall.

+
+ + + + + + + + + + + + + +
DateChanges
January 07, 2024Restructure with markdown
+

Begin

+

We'll now begin the system administrator experience. We will provide +from beginning to end, what to do, without giving away what has to be +done or has to be configured. This is on you to perform. At the end, +there is a "wiki" that you create where you will have a chance to +document everything you did. I recommend writing down or putting in a +word document what you are doing or have done throughout so it'll make +your wiki documentation much, much better.

+

Setup a KVM Hypervisor

+

Now you'll need to setup a KVM Hypervisor. You can do this on Fedora +39+ or Enterprise Linux 9. Because EL9 is a stable platform for libvirt, +I recommend using going that route. If you want the latest features for +the cost of some stability, Fedora will work for you.

+

You may want to make sure your hardware supports virtualization.

+
egrep --color 'vmx|svm' /proc/cpuinfo
+
+

Recommendations and Options

+
    +
  • +

    Create multiple datastores (storage pools where the VM images will + sit)

    +
      +
    • Example, 2x2TB means you can make two datastores, 4x1TB means + you can make four.
    • +
    +
  • +
  • +

    Attempt to use LVM as the backing for the store.

    +
      +
    • You can create a Volume Group and have the VM's live as Logical + Volumes.
    • +
    • You can create a Volume Group and have one or more partitions to + make "more" datastores
    • +
    +
  • +
  • +

    Destroy the "built in" network that libvirt already provides and + make your own

    +
      +
    • It already makes virbr0. Make your own OR modify it to not + support DHCP/DNS (eg, static only)
    • +
    +
  • +
+

Hints

+
    +
  • +

    Most of your commands will be from the following:

    +
      +
    • virsh
    • +
    • fdisk/parted
    • +
    • pvcreate & vgcreate
    • +
    • mkfs
    • +
    +
  • +
  • +

    To get the most performance out of your VM's disk wise, consider + these options:

    +
      +
    • Avoid QCOW2
    • +
    • Use virtio for the hardware whenever possible
    • +
    • Set caching to "none" for hard disks on VM's when using raw + volumes or partitions
    • +
    +
  • +
+

DHCP and DNS

+

You'll need to setup a DHCP and DNS server. You have a few choices.

+
    +
  1. Create two VM's to run DHCP for HA and create FreeIPA servers to + handle DNS (two replicas, doubles as authentication for Linux/UNIX + clients)
  2. +
  3. Create two VM's to run DHCP for HA and create two standalone BIND + servers as master/slave
  4. +
  5. Use your hypervisor to host DHCP and BIND (not recommended)
  6. +
+

It would be sensible to do "1", if you do "2", you at least get more +exposure to how zone files are created and the like. For ease of use, +we recommend choosing option 1.

+

Also, it is possible to allow cobbler handle DHCP and DNS or integrate +directly into DNS such as making changes, but this is outside the scope +of this write up.

+
+

Warning

+

Do NOT run DHCP from the FreeIPA replicas. The FreeIPA servers should +have STATIC addresses set.

+
+
+

Note

+

When you are setting up DHCP and DNS on separate servers (such as +FreeIPA replicas), the DHCP server needs to be configured to tell all +the clients the true gateway (this is either a VM in on KVM or a +hypervisor of your choice if you are doing straight KVM) and the DNS servers.

+
+

Setup a VM or your hypervisor as the gateway to the internet.

+
    +
  1. IP forwarding enabled (/etc/sysctl.conf)
  2. +
  3. NAT enabled (firewalld can help you with this, check out the zones)
  4. +
  5. A virtual interface (hypervisor) or a second interface for your + network (as a VM)
  6. +
+

When setting up DHCP and DNS:

+
    +
  1. Decide on a domain name. This can be a domain you own or one you + make up internally. I personally used one of my four domains for + this lab. RFC expects that internal networks have world routable + domains. This is up to you. Do NOT use '.local' domains
  2. +
  3. Setup DNS forwarders to ensure your VM's can get DNS requests from + the internet. You create a forwarders { } block with each outside + DNS IP listed in BIND or you can optionally set them in the + FreeIPA interface. You can list as many as you want. With a default + configuration of FreeIPA, forwarders are not strictly required. Do + NOT put these extra DNS servers in your dhcpd.conf configuration
  4. +
  5. You need two zones. Forward Zone: This is for your domain, name to + an IP. Reverse Zone: This is for reverse IP lookups, IP to a name. + FreeIPA handles this for you on setup if you state you are handling + a reverse zone and what the subnet is.
  6. +
+

Bonus Points

+
    +
  • Setup Dynamic DNS - This requires an almost specific configuration + between dhcpd and named (bind) or FreeIPA's named.
  • +
  • Dynamic DNS needs to be aware of a domain name
  • +
  • Use SSSD for the IPA clients to update their DNS automatically + (FreeIPA only) - this may not be required if dhcpd and named are + configured correctly
  • +
  • Setup an unbound service running on port 9053 that forwards to + 1.1.1.1 for encrypted DNS
  • +
+

From this point forward, you are to ensure each of your VM's that you +create have DNS entries. If you have Dynamic DNS running, you will NOT +need to do any manual changes. If using FreeIPA, you may not need to +make these changes. You can use nsupdate or the ipa equivalent to add +additional entries as needed if you are implementing static A records or +CNAME records.

+

Server and Content Management

+

At this point, you'll need to setup Foreman/Katello, Pulp, or Uyuni on +a VM. I recommend using Pulp if you want something smaller and simpler. +If you want something close to Red Hat Satellite, go through +katello. It is a combination of pulp, candlepin, foreman. This +recommendation is primarily because of Satellite 6 existing in a large +amount of Red Hat shops.

+

Katello, go here.

+
+

Note

+

Heads up

+
    +
  • You're going to be hosting repositories, I SERIOUSLY recommend + creating a VM that has at least 250GB starting and going from there. + Don't try to host Fedora.
  • +
  • Katello is resource heavy, you may need to tune it.
  • +
  • Pulp may be easier on you, resource wise.
  • +
+
+

Bonus Points

+
    +
  • Setup errata importation for the Enterprise Linux + Channels/Repositories to properly see Advisories and Information for + package updates if the repos you are importing does not contain them
  • +
  • Create custom kickstarts for your systems (this will help you out + later)
  • +
+

Kickstart examples can be found at my +github.

+

Connect Content Management to Hypervisor

+

Next you will need to connect your Content Management to your +hypervisor. View their documentation to get an idea of how it works.

+

Spin Up VM's Using Katello/Spacewalk or PXE Server

+

You will need to spin up two EL8 or EL9 VM's via Katello or PXE. Do not spin +them up using virt-install, virt-manager, or anything else. This will require +you to connect Katello to the hypervisor. Ensure they are registered +properly to your content management server.

+

If you find the clients aren't registering on Katello, click +here.

+

If you find that you do not want to use Katello to perform this task, +then you can setup cobbler and work it out from there. I currently do +not have a tutorial for this, but there is plenty of documentation +online. There are also ansible playbooks you could look at for +examples if you wanted to go that route, but it may be time consuming +and something to setup at the very end.

+

Setup FreeIPA

+

Setup FreeIPA with two replicas, using CA and DNS built in +configuration. This is recommended if you do not want to setup BIND by +hand. FreeIPA also provides authentication to your systems without +having to go through the hassle of setting up OpenLDAP by hand nor +having Windows AD.

+ +

I recommend against setting up OpenLDAP for the case of UNIX +authentication. For anything else, go for it.

+

Once FreeIPA is available, all systems should be using FreeIPA as your +DNS servers and they should all be enrolled to your domain.

+

Spin Up Two VM's for Databases

+

Create two new VM's from your Content Management or PXE system that are +EL9 and install the default postgresql on them.

+

Attempt to install and configure pgpool-II for master-master +replication. Note that this may not be default in Enterprise Linux and +you can safely skip this.

+

Spin Up Configuration Management

+

While Katello has some form of ansible built in, it may be +better to create a solitary configuration management VM and hook it in. +Spin up a VM that is EL9 and install a master for configuration +management.

+

It is HIGHLY recommended that you use ansible. Ansible is the supported +and recommended system by Red Hat and is utilized in the certification +exams for EL9. At some point, you could spin up a docker container for +AWX if you wanted, but this is not a strict requirement.

+

Spin Up VM for NFS/iSCSI

+

This VM should be EL9. Ensure it has an extra 20GB disk attached to it. +Install the following:

+
    +
  1. An NFS server (nfs-utils)
  2. +
  3. An iSCSI server (scsi-target-utils, targetcli)
  4. +
+

You are to:

+
    +
  1. Export an NFS directory
  2. +
  3. Export a LUN to any server
  4. +
+

iSCSI for RHEL 9

+

Deploy Bacula Server

+

Bacula is a backup service. It is actually confusing to setup. It's not +easy. There are plenty of write-ups for bacula and RHEL/Enterprise Linux. +The digital ocean write-ups are complete, but do NOT give you everything +you need to know to do it "correct" or to succeed completing this portion.

+

Your server will need the following:

+
    +
  1. Ensure the system has a large disk or a large second disk (this can + be any size, start small though) - You can also use your NFS server + or iSCSI's LUN.
  2. +
  3. Ensure it is partitioned for ext4
  4. +
  5. Ensure it is mounted to /bacula
  6. +
  7. If using iSCSI or NFS, ensure the disk from that server is bigger + than 20GB. 50 should suffice.
  8. +
  9. Bakula will need to be configured to use postgresql (digital ocean + does NOT use postgresql, you will need to do some reading)
  10. +
  11. Register each machine you have to it, storing to flatfile
  12. +
+

Deploy Two/Four VM's

+
    +
  1. First one/two will be web servers running apache (httpd)
  2. +
  3. Next one/two will be app servers
  4. +
+

This is a typical "web/app" configuration. Some shops use apache +frontends to weblogic backends. Sometimes it's tomcat backends. Some +shops opt for other methods and software too.

+

If wish to setup Wildfly and host a wiki, you will need to do the +following:

+
    +
  1. Setup Wildfly Wiki or on your app servers
  2. +
  3. Setup apache to forward requests to your tomcat servers for the wiki
  4. +
  5. Do this as a VirtualHost configuration with the ServerName as + "wiki.domain.tld", replacing "domain.tld" with your domain
  6. +
  7. Set a ServerAlias as wiki
  8. +
+

If you wish to setup a Git

+

Deploy Load Balancer VM

+

This will be considered a "VIP" of sorts for your wiki and other +applications. This VM can either use iptables round-robin or HAProxy. I +highly recommend trying both to see what's easier for you. HAProxy is +recommended, because it's an actual load balancer application.

+

You will need the following:

+
    +
  1. A DNS CNAME for this machine called "wiki.domain.tld", replacing + domain.tld with your domain
  2. +
  3. You will need to configure apache to respond to requests for + "wiki.domain.tld" (virtual host configuration) and forward them on + to the app servers
  4. +
  5. HAProxy will need to forward 80 and 443 requests to the two web + servers
  6. +
+
+

Warning

+

Dynamic DNS

+

If you are using Dynamic DNS, you may need to run rndc sync before +making changes in the case of standalone BIND. You will want to use the +nsupdate command to make changes to your Dynamic Zones. If you are using +FreeIPA DNS this is not required.

+
+

Deploy Postfix VM

+

You will need to do the following:

+
    +
  1. Ensure postfix is listening on all interfaces
  2. +
  3. Ensure postfix is setup to send and receive messages only from your + internal network
  4. +
  5. Setup a gmail account or another relay to allow the above to work to + outside mail (this is sort of tricky for gmail, but doable)
  6. +
+

Bonus Points

+
    +
  • Create two relays as "mailhost1" and "mailhost2" for your domain + with the same configurations
  • +
  • +

    Create a CNAME for "mailhost.domain.tld" for your load balancer, + forwarding port 25 to both servers

    +
      +
    • Optionally, you can use round-robin DNS instead of HAProxy
    • +
    +
  • +
+

Setup Nagios VM

+

This will be a monitoring server on EL9. You will need to set it up to +use snmp to monitor the communication state of every service above. This +means:

+
    +
  1. Is the right port open?
  2. +
  3. I got the right kind of response.
  4. +
  5. Filesystem Space, too full?
  6. +
+

If you are planning to use full on SNMP, all servers will need the +appropriate SNMP ports open and they will need the snmpd clients +installed (with a monitor snmpd account)

+

Setup Syslog VM

+

Setup this server as a syslog server. It can be EL8 or higher. Ensure +that it is listening on port 514 UDP and TCP in the configuration and +that those ports are open.

+

You will need to go to your servers and setup /etc/rsyslog.conf to send +ALL logs to this syslog server

+

Optionally, setup an all inclusive logging solution, like graylog, +elastic search, mongodb, fluentd. The sky is the limit here!

+

Document Your Work

+

On your new wiki, document everything you did, right now, on your new +wiki.

+

RPM Build Server

+

For fun, you can setup a new server that is your designated RPM building +machine. You will need to install mock to do this. Optionally, you +can setup koji, bodhi, the things that the Fedora project uses. This is +not for the faint of heart.

+

Git Server

+

Also for fun, you can setup a git server. There are many options out +there. A popular opensource one is Gitea.

+

Ansible

+

Consider setting up ansible and the open source tower. Automate +everything via ansible.

+
+
+ + + + + + + + + + + + diff --git a/el/unbound/index.html b/el/unbound/index.html new file mode 100644 index 00000000..6849ff84 --- /dev/null +++ b/el/unbound/index.html @@ -0,0 +1,302 @@ + + + + + + + + + + + Unbound - Linux Guide and Hints + + + + + + + + + + + + + + + +
+
+
+
+ +

Requirements

+

Setup

+

Installation

+
% yum install unbound -y
+% systemctl enable unbound
+
+

DNS over TLS (DoT)

+

Setting up DoT with unbound is straight forward, whether you already have a DNS server already or not. Let's go over the most basic configuration.

+
% vi /etc/unbound/unbound.conf
+server:
+        . . .
+        # Set the below to an IP address if you wish - as I have multiple VLAN's
+        # it is just easier for me to listen everywhere
+        interface: 0.0.0.0
+        interface: ::
+        # Optionally set a port - I have bind already running, so port 9053 works
+        interface-automatic: no
+        port: 9053
+        . . .
+        # Set access control rules here. I'll show a few examples with just two of
+        # my networks
+        # REFUSE everything
+        access-control: 0.0.0.0/0 refuse
+        access-control: ::0/0 refuse
+        # Allow localhost to snoop
+        access-control: 127.0.0.1/32 allow_snoop
+        access-control: ::1 allow_snoop
+        # Allow the entire localhost subnet
+        access-control: 127.0.0.0/8 allow
+        access-control: ::ffff:127.0.0.1 allow
+        # Allow my main network and sandbox network
+        access-control: 10.100.0.0/24 allow
+        access-control: 10.100.1.0/24 allow
+        . . .
+        # Ensure tls-cert-bundle is set
+        tls-cert-bundle: /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem
+        . . .
+# Create the forward zone for DoT queries
+forward-zone:
+        name: "."
+        forward-tls-upstream: yes
+        # Cloudflare
+        forward-addr: 1.1.1.1@853#cloudflare-dns.com
+        forward-addr: 1.0.0.1@853#cloudflare-dns.com
+        forward-addr: 2606:4700:4700::1111@853#cloudflare-dns.com
+        forward-addr: 2606:4700:4700::1001@853#cloudflare-dns.com
+        # Quad9
+        forward-addr: 9.9.9.9@853#dns.quad9.net
+        forward-addr: 149.112.112.112@853#dns.quad9.net
+
+% systemctl enable unbound --now
+# If you are using bind already with forwarders, you should edit it. Example.
+% vi /etc/named.conf
+options {
+        . . .
+        forwarders {
+                # This assumes your bind server and unbound server are on
+                # the same server like I did.
+                127.0.0.1 port 9053;
+        };
+        forward only;
+        . . .
+
+
+
+ + + + + + + + + + + + diff --git a/fonts/fontawesome-webfont.eot b/fonts/fontawesome-webfont.eot new file mode 100644 index 00000000..e9f60ca9 Binary files /dev/null and b/fonts/fontawesome-webfont.eot differ diff --git a/fonts/fontawesome-webfont.svg b/fonts/fontawesome-webfont.svg new file mode 100644 index 00000000..855c845e --- /dev/null +++ b/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fonts/fontawesome-webfont.ttf b/fonts/fontawesome-webfont.ttf new file mode 100644 index 00000000..35acda2f Binary files /dev/null and b/fonts/fontawesome-webfont.ttf differ diff --git a/fonts/fontawesome-webfont.woff b/fonts/fontawesome-webfont.woff new file mode 100644 index 00000000..400014a4 Binary files /dev/null and b/fonts/fontawesome-webfont.woff differ diff --git a/fonts/fontawesome-webfont.woff2 b/fonts/fontawesome-webfont.woff2 new file mode 100644 index 00000000..4d13fc60 Binary files /dev/null and b/fonts/fontawesome-webfont.woff2 differ diff --git a/img/favicon.ico b/img/favicon.ico new file mode 100644 index 00000000..e85006a3 Binary files /dev/null and b/img/favicon.ico differ diff --git a/img/grid.png b/img/grid.png new file mode 100644 index 00000000..878c3ed5 Binary files /dev/null and b/img/grid.png differ diff --git a/index.html b/index.html new file mode 100644 index 00000000..47cd9996 --- /dev/null +++ b/index.html @@ -0,0 +1,254 @@ + + + + + + + + + + + Linux Guide and Hints + + + + + + + + + + + + + + + +
+
+
+
+ +

The source code for this page can be found on github. This page contains tutorials and generally useful information regarding packages and system administration in Fedora and Enterprise Linux (Rocky Linux, CentOS Stream).

+ +

Here are some quick links to solid documentation:

+ +

Notes

+

Due to the number of articles out there about disabling SELinux, we felt this note was important.

+

Disabling SELinux is and almost always will be a terrible idea. See our antipatterns page as well as the Red Hat Enterprise Linux documentation, Fedora Docs, and Rocky Linux documentation.

+
+
+ + + + + + + + + + + + + + diff --git a/js/base.js b/js/base.js new file mode 100644 index 00000000..b0f4726b --- /dev/null +++ b/js/base.js @@ -0,0 +1,283 @@ +function getSearchTerm() { + var sPageURL = window.location.search.substring(1); + var sURLVariables = sPageURL.split('&'); + for (var i = 0; i < sURLVariables.length; i++) { + var sParameterName = sURLVariables[i].split('='); + if (sParameterName[0] == 'q') { + return sParameterName[1]; + } + } +} + +function applyTopPadding() { + // Update various absolute positions to match where the main container + // starts. This is necessary for handling multi-line nav headers, since + // that pushes the main container down. + var offset = $('body > .container').offset(); + $('html').css('scroll-padding-top', offset.top + 'px'); + $('.bs-sidebar.affix').css('top', offset.top + 'px'); +} + +$(document).ready(function() { + + applyTopPadding(); + + var search_term = getSearchTerm(), + $search_modal = $('#mkdocs_search_modal'), + $keyboard_modal = $('#mkdocs_keyboard_modal'); + + if (search_term) { + $search_modal.modal(); + } + + // make sure search input gets autofocus every time modal opens. + $search_modal.on('shown.bs.modal', function() { + $search_modal.find('#mkdocs-search-query').focus(); + }); + + // Close search modal when result is selected + // The links get added later so listen to parent + $('#mkdocs-search-results').click(function(e) { + if ($(e.target).is('a')) { + $search_modal.modal('hide'); + } + }); + + // Populate keyboard modal with proper Keys + $keyboard_modal.find('.help.shortcut kbd')[0].innerHTML = keyCodes[shortcuts.help]; + $keyboard_modal.find('.prev.shortcut kbd')[0].innerHTML = keyCodes[shortcuts.previous]; + $keyboard_modal.find('.next.shortcut kbd')[0].innerHTML = keyCodes[shortcuts.next]; + $keyboard_modal.find('.search.shortcut kbd')[0].innerHTML = keyCodes[shortcuts.search]; + + // Keyboard navigation + document.addEventListener("keydown", function(e) { + if ($(e.target).is(':input')) return true; + var key = e.which || e.keyCode || window.event && window.event.keyCode; + var page; + switch (key) { + case shortcuts.next: + page = $('.navbar a[rel="next"]:first').prop('href'); + break; + case shortcuts.previous: + page = $('.navbar a[rel="prev"]:first').prop('href'); + break; + case shortcuts.search: + e.preventDefault(); + $keyboard_modal.modal('hide'); + $search_modal.modal('show'); + $search_modal.find('#mkdocs-search-query').focus(); + break; + case shortcuts.help: + $search_modal.modal('hide'); + $keyboard_modal.modal('show'); + break; + default: break; + } + if (page) { + $keyboard_modal.modal('hide'); + window.location.href = page; + } + }); + + $('table').addClass('table table-striped table-hover'); + + // Improve the scrollspy behaviour when users click on a TOC item. + $(".bs-sidenav a").on("click", function() { + var clicked = this; + setTimeout(function() { + var active = $('.nav li.active a'); + active = active[active.length - 1]; + if (clicked !== active) { + $(active).parent().removeClass("active"); + $(clicked).parent().addClass("active"); + } + }, 50); + }); + + function showInnerDropdown(item) { + var popup = $(item).next('.dropdown-menu'); + popup.addClass('show'); + $(item).addClass('open'); + + // First, close any sibling dropdowns. + var container = $(item).parent().parent(); + container.find('> .dropdown-submenu > a').each(function(i, el) { + if (el !== item) { + hideInnerDropdown(el); + } + }); + + var popupMargin = 10; + var maxBottom = $(window).height() - popupMargin; + var bounds = item.getBoundingClientRect(); + + popup.css('left', bounds.right + 'px'); + if (bounds.top + popup.height() > maxBottom && + bounds.top > $(window).height() / 2) { + popup.css({ + 'top': (bounds.bottom - popup.height()) + 'px', + 'max-height': (bounds.bottom - popupMargin) + 'px', + }); + } else { + popup.css({ + 'top': bounds.top + 'px', + 'max-height': (maxBottom - bounds.top) + 'px', + }); + } + } + + function hideInnerDropdown(item) { + var popup = $(item).next('.dropdown-menu'); + popup.removeClass('show'); + $(item).removeClass('open'); + + popup.scrollTop(0); + popup.find('.dropdown-menu').scrollTop(0).removeClass('show'); + popup.find('.dropdown-submenu > a').removeClass('open'); + } + + $('.dropdown-submenu > a').on('click', function(e) { + if ($(this).next('.dropdown-menu').hasClass('show')) { + hideInnerDropdown(this); + } else { + showInnerDropdown(this); + } + + e.stopPropagation(); + e.preventDefault(); + }); + + $('.dropdown-menu').parent().on('hide.bs.dropdown', function(e) { + $(this).find('.dropdown-menu').scrollTop(0); + $(this).find('.dropdown-submenu > a').removeClass('open'); + $(this).find('.dropdown-menu .dropdown-menu').removeClass('show'); + }); +}); + +$(window).on('resize', applyTopPadding); + +$('body').scrollspy({ + target: '.bs-sidebar', + offset: 100 +}); + +/* Prevent disabled links from causing a page reload */ +$("li.disabled a").click(function() { + event.preventDefault(); +}); + +// See https://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes +// We only list common keys below. Obscure keys are omitted and their use is discouraged. +var keyCodes = { + 8: 'backspace', + 9: 'tab', + 13: 'enter', + 16: 'shift', + 17: 'ctrl', + 18: 'alt', + 19: 'pause/break', + 20: 'caps lock', + 27: 'escape', + 32: 'spacebar', + 33: 'page up', + 34: 'page down', + 35: 'end', + 36: 'home', + 37: '←', + 38: '↑', + 39: '→', + 40: '↓', + 45: 'insert', + 46: 'delete', + 48: '0', + 49: '1', + 50: '2', + 51: '3', + 52: '4', + 53: '5', + 54: '6', + 55: '7', + 56: '8', + 57: '9', + 65: 'a', + 66: 'b', + 67: 'c', + 68: 'd', + 69: 'e', + 70: 'f', + 71: 'g', + 72: 'h', + 73: 'i', + 74: 'j', + 75: 'k', + 76: 'l', + 77: 'm', + 78: 'n', + 79: 'o', + 80: 'p', + 81: 'q', + 82: 'r', + 83: 's', + 84: 't', + 85: 'u', + 86: 'v', + 87: 'w', + 88: 'x', + 89: 'y', + 90: 'z', + 91: 'Left Windows Key / Left ⌘', + 92: 'Right Windows Key', + 93: 'Windows Menu / Right ⌘', + 96: 'numpad 0', + 97: 'numpad 1', + 98: 'numpad 2', + 99: 'numpad 3', + 100: 'numpad 4', + 101: 'numpad 5', + 102: 'numpad 6', + 103: 'numpad 7', + 104: 'numpad 8', + 105: 'numpad 9', + 106: 'multiply', + 107: 'add', + 109: 'subtract', + 110: 'decimal point', + 111: 'divide', + 112: 'f1', + 113: 'f2', + 114: 'f3', + 115: 'f4', + 116: 'f5', + 117: 'f6', + 118: 'f7', + 119: 'f8', + 120: 'f9', + 121: 'f10', + 122: 'f11', + 123: 'f12', + 124: 'f13', + 125: 'f14', + 126: 'f15', + 127: 'f16', + 128: 'f17', + 129: 'f18', + 130: 'f19', + 131: 'f20', + 132: 'f21', + 133: 'f22', + 134: 'f23', + 135: 'f24', + 144: 'num lock', + 145: 'scroll lock', + 186: ';', + 187: '=', + 188: ',', + 189: '‐', + 190: '.', + 191: '?', + 192: '`', + 219: '[', + 220: '\', + 221: ']', + 222: ''', +}; diff --git a/js/bootstrap.min.js b/js/bootstrap.min.js new file mode 100644 index 00000000..ca013b70 --- /dev/null +++ b/js/bootstrap.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.3.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t=t||self).bootstrap={},t.jQuery,t.Popper)}(this,function(t,g,u){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)g(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:Ee},je="show",He="out",Re={HIDE:"hide"+De,HIDDEN:"hidden"+De,SHOW:"show"+De,SHOWN:"shown"+De,INSERTED:"inserted"+De,CLICK:"click"+De,FOCUSIN:"focusin"+De,FOCUSOUT:"focusout"+De,MOUSEENTER:"mouseenter"+De,MOUSELEAVE:"mouseleave"+De},xe="fade",Fe="show",Ue=".tooltip-inner",We=".arrow",qe="hover",Me="focus",Ke="click",Qe="manual",Be=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Fe))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(xe);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,{placement:a,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:We},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),g(o).addClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===He&&e._leave(null,e)};if(g(this.tip).hasClass(xe)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=g.Event(this.constructor.Event.HIDE),o=function(){e._hoverState!==je&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),g(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(g(this.element).trigger(i),!i.isDefaultPrevented()){if(g(n).removeClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[Ke]=!1,this._activeTrigger[Me]=!1,this._activeTrigger[qe]=!1,g(this.tip).hasClass(xe)){var r=_.getTransitionDurationFromElement(n);g(n).one(_.TRANSITION_END,o).emulateTransitionEnd(r)}else o();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Ae+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(Ue)),this.getTitle()),g(t).removeClass(xe+" "+Fe)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=Se(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return Pe[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==Qe){var e=t===qe?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===qe?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),g(this.element).closest(".modal").on("hide.bs.modal",function(){i.element&&i.hide()}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Me:qe]=!0),g(e.getTipElement()).hasClass(Fe)||e._hoverState===je?e._hoverState=je:(clearTimeout(e._timeout),e._hoverState=je,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===je&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Me:qe]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=He,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===He&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=g(this.element).data();return Object.keys(e).forEach(function(t){-1!==Oe.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(be,t,this.constructor.DefaultType),t.sanitize&&(t.template=Se(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ne);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(xe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(Ie),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(Ie,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return Le}},{key:"NAME",get:function(){return be}},{key:"DATA_KEY",get:function(){return Ie}},{key:"Event",get:function(){return Re}},{key:"EVENT_KEY",get:function(){return De}},{key:"DefaultType",get:function(){return ke}}]),i}();g.fn[be]=Be._jQueryInterface,g.fn[be].Constructor=Be,g.fn[be].noConflict=function(){return g.fn[be]=we,Be._jQueryInterface};var Ve="popover",Ye="bs.popover",ze="."+Ye,Xe=g.fn[Ve],$e="bs-popover",Ge=new RegExp("(^|\\s)"+$e+"\\S+","g"),Je=l({},Be.Default,{placement:"right",trigger:"click",content:"",template:''}),Ze=l({},Be.DefaultType,{content:"(string|element|function)"}),tn="fade",en="show",nn=".popover-header",on=".popover-body",rn={HIDE:"hide"+ze,HIDDEN:"hidden"+ze,SHOW:"show"+ze,SHOWN:"shown"+ze,INSERTED:"inserted"+ze,CLICK:"click"+ze,FOCUSIN:"focusin"+ze,FOCUSOUT:"focusout"+ze,MOUSEENTER:"mouseenter"+ze,MOUSELEAVE:"mouseleave"+ze},sn=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var o=i.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.addAttachmentClass=function(t){g(this.getTipElement()).addClass($e+"-"+t)},o.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},o.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(nn),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(on),e),t.removeClass(tn+" "+en)},o._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},o._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ge);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 0) { + var tokenMetadata = lunr.utils.clone(metadata) || {} + tokenMetadata["position"] = [sliceStart, sliceLength] + tokenMetadata["index"] = tokens.length + + tokens.push( + new lunr.Token ( + str.slice(sliceStart, sliceEnd), + tokenMetadata + ) + ) + } + + sliceStart = sliceEnd + 1 + } + + } + + return tokens +} + +/** + * The separator used to split a string into tokens. Override this property to change the behaviour of + * `lunr.tokenizer` behaviour when tokenizing strings. By default this splits on whitespace and hyphens. + * + * @static + * @see lunr.tokenizer + */ +lunr.tokenizer.separator = /[\s\-]+/ +/*! + * lunr.Pipeline + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.Pipelines maintain an ordered list of functions to be applied to all + * tokens in documents entering the search index and queries being ran against + * the index. + * + * An instance of lunr.Index created with the lunr shortcut will contain a + * pipeline with a stop word filter and an English language stemmer. Extra + * functions can be added before or after either of these functions or these + * default functions can be removed. + * + * When run the pipeline will call each function in turn, passing a token, the + * index of that token in the original list of all tokens and finally a list of + * all the original tokens. + * + * The output of functions in the pipeline will be passed to the next function + * in the pipeline. To exclude a token from entering the index the function + * should return undefined, the rest of the pipeline will not be called with + * this token. + * + * For serialisation of pipelines to work, all functions used in an instance of + * a pipeline should be registered with lunr.Pipeline. Registered functions can + * then be loaded. If trying to load a serialised pipeline that uses functions + * that are not registered an error will be thrown. + * + * If not planning on serialising the pipeline then registering pipeline functions + * is not necessary. + * + * @constructor + */ +lunr.Pipeline = function () { + this._stack = [] +} + +lunr.Pipeline.registeredFunctions = Object.create(null) + +/** + * A pipeline function maps lunr.Token to lunr.Token. A lunr.Token contains the token + * string as well as all known metadata. A pipeline function can mutate the token string + * or mutate (or add) metadata for a given token. + * + * A pipeline function can indicate that the passed token should be discarded by returning + * null, undefined or an empty string. This token will not be passed to any downstream pipeline + * functions and will not be added to the index. + * + * Multiple tokens can be returned by returning an array of tokens. Each token will be passed + * to any downstream pipeline functions and all will returned tokens will be added to the index. + * + * Any number of pipeline functions may be chained together using a lunr.Pipeline. + * + * @interface lunr.PipelineFunction + * @param {lunr.Token} token - A token from the document being processed. + * @param {number} i - The index of this token in the complete list of tokens for this document/field. + * @param {lunr.Token[]} tokens - All tokens for this document/field. + * @returns {(?lunr.Token|lunr.Token[])} + */ + +/** + * Register a function with the pipeline. + * + * Functions that are used in the pipeline should be registered if the pipeline + * needs to be serialised, or a serialised pipeline needs to be loaded. + * + * Registering a function does not add it to a pipeline, functions must still be + * added to instances of the pipeline for them to be used when running a pipeline. + * + * @param {lunr.PipelineFunction} fn - The function to check for. + * @param {String} label - The label to register this function with + */ +lunr.Pipeline.registerFunction = function (fn, label) { + if (label in this.registeredFunctions) { + lunr.utils.warn('Overwriting existing registered function: ' + label) + } + + fn.label = label + lunr.Pipeline.registeredFunctions[fn.label] = fn +} + +/** + * Warns if the function is not registered as a Pipeline function. + * + * @param {lunr.PipelineFunction} fn - The function to check for. + * @private + */ +lunr.Pipeline.warnIfFunctionNotRegistered = function (fn) { + var isRegistered = fn.label && (fn.label in this.registeredFunctions) + + if (!isRegistered) { + lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\n', fn) + } +} + +/** + * Loads a previously serialised pipeline. + * + * All functions to be loaded must already be registered with lunr.Pipeline. + * If any function from the serialised data has not been registered then an + * error will be thrown. + * + * @param {Object} serialised - The serialised pipeline to load. + * @returns {lunr.Pipeline} + */ +lunr.Pipeline.load = function (serialised) { + var pipeline = new lunr.Pipeline + + serialised.forEach(function (fnName) { + var fn = lunr.Pipeline.registeredFunctions[fnName] + + if (fn) { + pipeline.add(fn) + } else { + throw new Error('Cannot load unregistered function: ' + fnName) + } + }) + + return pipeline +} + +/** + * Adds new functions to the end of the pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction[]} functions - Any number of functions to add to the pipeline. + */ +lunr.Pipeline.prototype.add = function () { + var fns = Array.prototype.slice.call(arguments) + + fns.forEach(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + this._stack.push(fn) + }, this) +} + +/** + * Adds a single function after a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline. + * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline. + */ +lunr.Pipeline.prototype.after = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + if (pos == -1) { + throw new Error('Cannot find existingFn') + } + + pos = pos + 1 + this._stack.splice(pos, 0, newFn) +} + +/** + * Adds a single function before a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline. + * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline. + */ +lunr.Pipeline.prototype.before = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + if (pos == -1) { + throw new Error('Cannot find existingFn') + } + + this._stack.splice(pos, 0, newFn) +} + +/** + * Removes a function from the pipeline. + * + * @param {lunr.PipelineFunction} fn The function to remove from the pipeline. + */ +lunr.Pipeline.prototype.remove = function (fn) { + var pos = this._stack.indexOf(fn) + if (pos == -1) { + return + } + + this._stack.splice(pos, 1) +} + +/** + * Runs the current list of functions that make up the pipeline against the + * passed tokens. + * + * @param {Array} tokens The tokens to run through the pipeline. + * @returns {Array} + */ +lunr.Pipeline.prototype.run = function (tokens) { + var stackLength = this._stack.length + + for (var i = 0; i < stackLength; i++) { + var fn = this._stack[i] + var memo = [] + + for (var j = 0; j < tokens.length; j++) { + var result = fn(tokens[j], j, tokens) + + if (result === null || result === void 0 || result === '') continue + + if (Array.isArray(result)) { + for (var k = 0; k < result.length; k++) { + memo.push(result[k]) + } + } else { + memo.push(result) + } + } + + tokens = memo + } + + return tokens +} + +/** + * Convenience method for passing a string through a pipeline and getting + * strings out. This method takes care of wrapping the passed string in a + * token and mapping the resulting tokens back to strings. + * + * @param {string} str - The string to pass through the pipeline. + * @param {?object} metadata - Optional metadata to associate with the token + * passed to the pipeline. + * @returns {string[]} + */ +lunr.Pipeline.prototype.runString = function (str, metadata) { + var token = new lunr.Token (str, metadata) + + return this.run([token]).map(function (t) { + return t.toString() + }) +} + +/** + * Resets the pipeline by removing any existing processors. + * + */ +lunr.Pipeline.prototype.reset = function () { + this._stack = [] +} + +/** + * Returns a representation of the pipeline ready for serialisation. + * + * Logs a warning if the function has not been registered. + * + * @returns {Array} + */ +lunr.Pipeline.prototype.toJSON = function () { + return this._stack.map(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + + return fn.label + }) +} +/*! + * lunr.Vector + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A vector is used to construct the vector space of documents and queries. These + * vectors support operations to determine the similarity between two documents or + * a document and a query. + * + * Normally no parameters are required for initializing a vector, but in the case of + * loading a previously dumped vector the raw elements can be provided to the constructor. + * + * For performance reasons vectors are implemented with a flat array, where an elements + * index is immediately followed by its value. E.g. [index, value, index, value]. This + * allows the underlying array to be as sparse as possible and still offer decent + * performance when being used for vector calculations. + * + * @constructor + * @param {Number[]} [elements] - The flat list of element index and element value pairs. + */ +lunr.Vector = function (elements) { + this._magnitude = 0 + this.elements = elements || [] +} + + +/** + * Calculates the position within the vector to insert a given index. + * + * This is used internally by insert and upsert. If there are duplicate indexes then + * the position is returned as if the value for that index were to be updated, but it + * is the callers responsibility to check whether there is a duplicate at that index + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @returns {Number} + */ +lunr.Vector.prototype.positionForIndex = function (index) { + // For an empty vector the tuple can be inserted at the beginning + if (this.elements.length == 0) { + return 0 + } + + var start = 0, + end = this.elements.length / 2, + sliceLength = end - start, + pivotPoint = Math.floor(sliceLength / 2), + pivotIndex = this.elements[pivotPoint * 2] + + while (sliceLength > 1) { + if (pivotIndex < index) { + start = pivotPoint + } + + if (pivotIndex > index) { + end = pivotPoint + } + + if (pivotIndex == index) { + break + } + + sliceLength = end - start + pivotPoint = start + Math.floor(sliceLength / 2) + pivotIndex = this.elements[pivotPoint * 2] + } + + if (pivotIndex == index) { + return pivotPoint * 2 + } + + if (pivotIndex > index) { + return pivotPoint * 2 + } + + if (pivotIndex < index) { + return (pivotPoint + 1) * 2 + } +} + +/** + * Inserts an element at an index within the vector. + * + * Does not allow duplicates, will throw an error if there is already an entry + * for this index. + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @param {Number} val - The value to be inserted into the vector. + */ +lunr.Vector.prototype.insert = function (insertIdx, val) { + this.upsert(insertIdx, val, function () { + throw "duplicate index" + }) +} + +/** + * Inserts or updates an existing index within the vector. + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @param {Number} val - The value to be inserted into the vector. + * @param {function} fn - A function that is called for updates, the existing value and the + * requested value are passed as arguments + */ +lunr.Vector.prototype.upsert = function (insertIdx, val, fn) { + this._magnitude = 0 + var position = this.positionForIndex(insertIdx) + + if (this.elements[position] == insertIdx) { + this.elements[position + 1] = fn(this.elements[position + 1], val) + } else { + this.elements.splice(position, 0, insertIdx, val) + } +} + +/** + * Calculates the magnitude of this vector. + * + * @returns {Number} + */ +lunr.Vector.prototype.magnitude = function () { + if (this._magnitude) return this._magnitude + + var sumOfSquares = 0, + elementsLength = this.elements.length + + for (var i = 1; i < elementsLength; i += 2) { + var val = this.elements[i] + sumOfSquares += val * val + } + + return this._magnitude = Math.sqrt(sumOfSquares) +} + +/** + * Calculates the dot product of this vector and another vector. + * + * @param {lunr.Vector} otherVector - The vector to compute the dot product with. + * @returns {Number} + */ +lunr.Vector.prototype.dot = function (otherVector) { + var dotProduct = 0, + a = this.elements, b = otherVector.elements, + aLen = a.length, bLen = b.length, + aVal = 0, bVal = 0, + i = 0, j = 0 + + while (i < aLen && j < bLen) { + aVal = a[i], bVal = b[j] + if (aVal < bVal) { + i += 2 + } else if (aVal > bVal) { + j += 2 + } else if (aVal == bVal) { + dotProduct += a[i + 1] * b[j + 1] + i += 2 + j += 2 + } + } + + return dotProduct +} + +/** + * Calculates the similarity between this vector and another vector. + * + * @param {lunr.Vector} otherVector - The other vector to calculate the + * similarity with. + * @returns {Number} + */ +lunr.Vector.prototype.similarity = function (otherVector) { + return this.dot(otherVector) / this.magnitude() || 0 +} + +/** + * Converts the vector to an array of the elements within the vector. + * + * @returns {Number[]} + */ +lunr.Vector.prototype.toArray = function () { + var output = new Array (this.elements.length / 2) + + for (var i = 1, j = 0; i < this.elements.length; i += 2, j++) { + output[j] = this.elements[i] + } + + return output +} + +/** + * A JSON serializable representation of the vector. + * + * @returns {Number[]} + */ +lunr.Vector.prototype.toJSON = function () { + return this.elements +} +/* eslint-disable */ +/*! + * lunr.stemmer + * Copyright (C) 2020 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + */ + +/** + * lunr.stemmer is an english language stemmer, this is a JavaScript + * implementation of the PorterStemmer taken from http://tartarus.org/~martin + * + * @static + * @implements {lunr.PipelineFunction} + * @param {lunr.Token} token - The string to stem + * @returns {lunr.Token} + * @see {@link lunr.Pipeline} + * @function + */ +lunr.stemmer = (function(){ + var step2list = { + "ational" : "ate", + "tional" : "tion", + "enci" : "ence", + "anci" : "ance", + "izer" : "ize", + "bli" : "ble", + "alli" : "al", + "entli" : "ent", + "eli" : "e", + "ousli" : "ous", + "ization" : "ize", + "ation" : "ate", + "ator" : "ate", + "alism" : "al", + "iveness" : "ive", + "fulness" : "ful", + "ousness" : "ous", + "aliti" : "al", + "iviti" : "ive", + "biliti" : "ble", + "logi" : "log" + }, + + step3list = { + "icate" : "ic", + "ative" : "", + "alize" : "al", + "iciti" : "ic", + "ical" : "ic", + "ful" : "", + "ness" : "" + }, + + c = "[^aeiou]", // consonant + v = "[aeiouy]", // vowel + C = c + "[^aeiouy]*", // consonant sequence + V = v + "[aeiou]*", // vowel sequence + + mgr0 = "^(" + C + ")?" + V + C, // [C]VC... is m>0 + meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$", // [C]VC[V] is m=1 + mgr1 = "^(" + C + ")?" + V + C + V + C, // [C]VCVC... is m>1 + s_v = "^(" + C + ")?" + v; // vowel in stem + + var re_mgr0 = new RegExp(mgr0); + var re_mgr1 = new RegExp(mgr1); + var re_meq1 = new RegExp(meq1); + var re_s_v = new RegExp(s_v); + + var re_1a = /^(.+?)(ss|i)es$/; + var re2_1a = /^(.+?)([^s])s$/; + var re_1b = /^(.+?)eed$/; + var re2_1b = /^(.+?)(ed|ing)$/; + var re_1b_2 = /.$/; + var re2_1b_2 = /(at|bl|iz)$/; + var re3_1b_2 = new RegExp("([^aeiouylsz])\\1$"); + var re4_1b_2 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var re_1c = /^(.+?[^aeiou])y$/; + var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + + var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + + var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + var re2_4 = /^(.+?)(s|t)(ion)$/; + + var re_5 = /^(.+?)e$/; + var re_5_1 = /ll$/; + var re3_5 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var porterStemmer = function porterStemmer(w) { + var stem, + suffix, + firstch, + re, + re2, + re3, + re4; + + if (w.length < 3) { return w; } + + firstch = w.substr(0,1); + if (firstch == "y") { + w = firstch.toUpperCase() + w.substr(1); + } + + // Step 1a + re = re_1a + re2 = re2_1a; + + if (re.test(w)) { w = w.replace(re,"$1$2"); } + else if (re2.test(w)) { w = w.replace(re2,"$1$2"); } + + // Step 1b + re = re_1b; + re2 = re2_1b; + if (re.test(w)) { + var fp = re.exec(w); + re = re_mgr0; + if (re.test(fp[1])) { + re = re_1b_2; + w = w.replace(re,""); + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = re_s_v; + if (re2.test(stem)) { + w = stem; + re2 = re2_1b_2; + re3 = re3_1b_2; + re4 = re4_1b_2; + if (re2.test(w)) { w = w + "e"; } + else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,""); } + else if (re4.test(w)) { w = w + "e"; } + } + } + + // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say) + re = re_1c; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem + "i"; + } + + // Step 2 + re = re_2; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step2list[suffix]; + } + } + + // Step 3 + re = re_3; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step3list[suffix]; + } + } + + // Step 4 + re = re_4; + re2 = re2_4; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + if (re.test(stem)) { + w = stem; + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = re_mgr1; + if (re2.test(stem)) { + w = stem; + } + } + + // Step 5 + re = re_5; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + re2 = re_meq1; + re3 = re3_5; + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) { + w = stem; + } + } + + re = re_5_1; + re2 = re_mgr1; + if (re.test(w) && re2.test(w)) { + re = re_1b_2; + w = w.replace(re,""); + } + + // and turn initial Y back to y + + if (firstch == "y") { + w = firstch.toLowerCase() + w.substr(1); + } + + return w; + }; + + return function (token) { + return token.update(porterStemmer); + } +})(); + +lunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer') +/*! + * lunr.stopWordFilter + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.generateStopWordFilter builds a stopWordFilter function from the provided + * list of stop words. + * + * The built in lunr.stopWordFilter is built using this generator and can be used + * to generate custom stopWordFilters for applications or non English languages. + * + * @function + * @param {Array} token The token to pass through the filter + * @returns {lunr.PipelineFunction} + * @see lunr.Pipeline + * @see lunr.stopWordFilter + */ +lunr.generateStopWordFilter = function (stopWords) { + var words = stopWords.reduce(function (memo, stopWord) { + memo[stopWord] = stopWord + return memo + }, {}) + + return function (token) { + if (token && words[token.toString()] !== token.toString()) return token + } +} + +/** + * lunr.stopWordFilter is an English language stop word list filter, any words + * contained in the list will not be passed through the filter. + * + * This is intended to be used in the Pipeline. If the token does not pass the + * filter then undefined will be returned. + * + * @function + * @implements {lunr.PipelineFunction} + * @params {lunr.Token} token - A token to check for being a stop word. + * @returns {lunr.Token} + * @see {@link lunr.Pipeline} + */ +lunr.stopWordFilter = lunr.generateStopWordFilter([ + 'a', + 'able', + 'about', + 'across', + 'after', + 'all', + 'almost', + 'also', + 'am', + 'among', + 'an', + 'and', + 'any', + 'are', + 'as', + 'at', + 'be', + 'because', + 'been', + 'but', + 'by', + 'can', + 'cannot', + 'could', + 'dear', + 'did', + 'do', + 'does', + 'either', + 'else', + 'ever', + 'every', + 'for', + 'from', + 'get', + 'got', + 'had', + 'has', + 'have', + 'he', + 'her', + 'hers', + 'him', + 'his', + 'how', + 'however', + 'i', + 'if', + 'in', + 'into', + 'is', + 'it', + 'its', + 'just', + 'least', + 'let', + 'like', + 'likely', + 'may', + 'me', + 'might', + 'most', + 'must', + 'my', + 'neither', + 'no', + 'nor', + 'not', + 'of', + 'off', + 'often', + 'on', + 'only', + 'or', + 'other', + 'our', + 'own', + 'rather', + 'said', + 'say', + 'says', + 'she', + 'should', + 'since', + 'so', + 'some', + 'than', + 'that', + 'the', + 'their', + 'them', + 'then', + 'there', + 'these', + 'they', + 'this', + 'tis', + 'to', + 'too', + 'twas', + 'us', + 'wants', + 'was', + 'we', + 'were', + 'what', + 'when', + 'where', + 'which', + 'while', + 'who', + 'whom', + 'why', + 'will', + 'with', + 'would', + 'yet', + 'you', + 'your' +]) + +lunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter') +/*! + * lunr.trimmer + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.trimmer is a pipeline function for trimming non word + * characters from the beginning and end of tokens before they + * enter the index. + * + * This implementation may not work correctly for non latin + * characters and should either be removed or adapted for use + * with languages with non-latin characters. + * + * @static + * @implements {lunr.PipelineFunction} + * @param {lunr.Token} token The token to pass through the filter + * @returns {lunr.Token} + * @see lunr.Pipeline + */ +lunr.trimmer = function (token) { + return token.update(function (s) { + return s.replace(/^\W+/, '').replace(/\W+$/, '') + }) +} + +lunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer') +/*! + * lunr.TokenSet + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A token set is used to store the unique list of all tokens + * within an index. Token sets are also used to represent an + * incoming query to the index, this query token set and index + * token set are then intersected to find which tokens to look + * up in the inverted index. + * + * A token set can hold multiple tokens, as in the case of the + * index token set, or it can hold a single token as in the + * case of a simple query token set. + * + * Additionally token sets are used to perform wildcard matching. + * Leading, contained and trailing wildcards are supported, and + * from this edit distance matching can also be provided. + * + * Token sets are implemented as a minimal finite state automata, + * where both common prefixes and suffixes are shared between tokens. + * This helps to reduce the space used for storing the token set. + * + * @constructor + */ +lunr.TokenSet = function () { + this.final = false + this.edges = {} + this.id = lunr.TokenSet._nextId + lunr.TokenSet._nextId += 1 +} + +/** + * Keeps track of the next, auto increment, identifier to assign + * to a new tokenSet. + * + * TokenSets require a unique identifier to be correctly minimised. + * + * @private + */ +lunr.TokenSet._nextId = 1 + +/** + * Creates a TokenSet instance from the given sorted array of words. + * + * @param {String[]} arr - A sorted array of strings to create the set from. + * @returns {lunr.TokenSet} + * @throws Will throw an error if the input array is not sorted. + */ +lunr.TokenSet.fromArray = function (arr) { + var builder = new lunr.TokenSet.Builder + + for (var i = 0, len = arr.length; i < len; i++) { + builder.insert(arr[i]) + } + + builder.finish() + return builder.root +} + +/** + * Creates a token set from a query clause. + * + * @private + * @param {Object} clause - A single clause from lunr.Query. + * @param {string} clause.term - The query clause term. + * @param {number} [clause.editDistance] - The optional edit distance for the term. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.fromClause = function (clause) { + if ('editDistance' in clause) { + return lunr.TokenSet.fromFuzzyString(clause.term, clause.editDistance) + } else { + return lunr.TokenSet.fromString(clause.term) + } +} + +/** + * Creates a token set representing a single string with a specified + * edit distance. + * + * Insertions, deletions, substitutions and transpositions are each + * treated as an edit distance of 1. + * + * Increasing the allowed edit distance will have a dramatic impact + * on the performance of both creating and intersecting these TokenSets. + * It is advised to keep the edit distance less than 3. + * + * @param {string} str - The string to create the token set from. + * @param {number} editDistance - The allowed edit distance to match. + * @returns {lunr.Vector} + */ +lunr.TokenSet.fromFuzzyString = function (str, editDistance) { + var root = new lunr.TokenSet + + var stack = [{ + node: root, + editsRemaining: editDistance, + str: str + }] + + while (stack.length) { + var frame = stack.pop() + + // no edit + if (frame.str.length > 0) { + var char = frame.str.charAt(0), + noEditNode + + if (char in frame.node.edges) { + noEditNode = frame.node.edges[char] + } else { + noEditNode = new lunr.TokenSet + frame.node.edges[char] = noEditNode + } + + if (frame.str.length == 1) { + noEditNode.final = true + } + + stack.push({ + node: noEditNode, + editsRemaining: frame.editsRemaining, + str: frame.str.slice(1) + }) + } + + if (frame.editsRemaining == 0) { + continue + } + + // insertion + if ("*" in frame.node.edges) { + var insertionNode = frame.node.edges["*"] + } else { + var insertionNode = new lunr.TokenSet + frame.node.edges["*"] = insertionNode + } + + if (frame.str.length == 0) { + insertionNode.final = true + } + + stack.push({ + node: insertionNode, + editsRemaining: frame.editsRemaining - 1, + str: frame.str + }) + + // deletion + // can only do a deletion if we have enough edits remaining + // and if there are characters left to delete in the string + if (frame.str.length > 1) { + stack.push({ + node: frame.node, + editsRemaining: frame.editsRemaining - 1, + str: frame.str.slice(1) + }) + } + + // deletion + // just removing the last character from the str + if (frame.str.length == 1) { + frame.node.final = true + } + + // substitution + // can only do a substitution if we have enough edits remaining + // and if there are characters left to substitute + if (frame.str.length >= 1) { + if ("*" in frame.node.edges) { + var substitutionNode = frame.node.edges["*"] + } else { + var substitutionNode = new lunr.TokenSet + frame.node.edges["*"] = substitutionNode + } + + if (frame.str.length == 1) { + substitutionNode.final = true + } + + stack.push({ + node: substitutionNode, + editsRemaining: frame.editsRemaining - 1, + str: frame.str.slice(1) + }) + } + + // transposition + // can only do a transposition if there are edits remaining + // and there are enough characters to transpose + if (frame.str.length > 1) { + var charA = frame.str.charAt(0), + charB = frame.str.charAt(1), + transposeNode + + if (charB in frame.node.edges) { + transposeNode = frame.node.edges[charB] + } else { + transposeNode = new lunr.TokenSet + frame.node.edges[charB] = transposeNode + } + + if (frame.str.length == 1) { + transposeNode.final = true + } + + stack.push({ + node: transposeNode, + editsRemaining: frame.editsRemaining - 1, + str: charA + frame.str.slice(2) + }) + } + } + + return root +} + +/** + * Creates a TokenSet from a string. + * + * The string may contain one or more wildcard characters (*) + * that will allow wildcard matching when intersecting with + * another TokenSet. + * + * @param {string} str - The string to create a TokenSet from. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.fromString = function (str) { + var node = new lunr.TokenSet, + root = node + + /* + * Iterates through all characters within the passed string + * appending a node for each character. + * + * When a wildcard character is found then a self + * referencing edge is introduced to continually match + * any number of any characters. + */ + for (var i = 0, len = str.length; i < len; i++) { + var char = str[i], + final = (i == len - 1) + + if (char == "*") { + node.edges[char] = node + node.final = final + + } else { + var next = new lunr.TokenSet + next.final = final + + node.edges[char] = next + node = next + } + } + + return root +} + +/** + * Converts this TokenSet into an array of strings + * contained within the TokenSet. + * + * This is not intended to be used on a TokenSet that + * contains wildcards, in these cases the results are + * undefined and are likely to cause an infinite loop. + * + * @returns {string[]} + */ +lunr.TokenSet.prototype.toArray = function () { + var words = [] + + var stack = [{ + prefix: "", + node: this + }] + + while (stack.length) { + var frame = stack.pop(), + edges = Object.keys(frame.node.edges), + len = edges.length + + if (frame.node.final) { + /* In Safari, at this point the prefix is sometimes corrupted, see: + * https://github.com/olivernn/lunr.js/issues/279 Calling any + * String.prototype method forces Safari to "cast" this string to what + * it's supposed to be, fixing the bug. */ + frame.prefix.charAt(0) + words.push(frame.prefix) + } + + for (var i = 0; i < len; i++) { + var edge = edges[i] + + stack.push({ + prefix: frame.prefix.concat(edge), + node: frame.node.edges[edge] + }) + } + } + + return words +} + +/** + * Generates a string representation of a TokenSet. + * + * This is intended to allow TokenSets to be used as keys + * in objects, largely to aid the construction and minimisation + * of a TokenSet. As such it is not designed to be a human + * friendly representation of the TokenSet. + * + * @returns {string} + */ +lunr.TokenSet.prototype.toString = function () { + // NOTE: Using Object.keys here as this.edges is very likely + // to enter 'hash-mode' with many keys being added + // + // avoiding a for-in loop here as it leads to the function + // being de-optimised (at least in V8). From some simple + // benchmarks the performance is comparable, but allowing + // V8 to optimize may mean easy performance wins in the future. + + if (this._str) { + return this._str + } + + var str = this.final ? '1' : '0', + labels = Object.keys(this.edges).sort(), + len = labels.length + + for (var i = 0; i < len; i++) { + var label = labels[i], + node = this.edges[label] + + str = str + label + node.id + } + + return str +} + +/** + * Returns a new TokenSet that is the intersection of + * this TokenSet and the passed TokenSet. + * + * This intersection will take into account any wildcards + * contained within the TokenSet. + * + * @param {lunr.TokenSet} b - An other TokenSet to intersect with. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.prototype.intersect = function (b) { + var output = new lunr.TokenSet, + frame = undefined + + var stack = [{ + qNode: b, + output: output, + node: this + }] + + while (stack.length) { + frame = stack.pop() + + // NOTE: As with the #toString method, we are using + // Object.keys and a for loop instead of a for-in loop + // as both of these objects enter 'hash' mode, causing + // the function to be de-optimised in V8 + var qEdges = Object.keys(frame.qNode.edges), + qLen = qEdges.length, + nEdges = Object.keys(frame.node.edges), + nLen = nEdges.length + + for (var q = 0; q < qLen; q++) { + var qEdge = qEdges[q] + + for (var n = 0; n < nLen; n++) { + var nEdge = nEdges[n] + + if (nEdge == qEdge || qEdge == '*') { + var node = frame.node.edges[nEdge], + qNode = frame.qNode.edges[qEdge], + final = node.final && qNode.final, + next = undefined + + if (nEdge in frame.output.edges) { + // an edge already exists for this character + // no need to create a new node, just set the finality + // bit unless this node is already final + next = frame.output.edges[nEdge] + next.final = next.final || final + + } else { + // no edge exists yet, must create one + // set the finality bit and insert it + // into the output + next = new lunr.TokenSet + next.final = final + frame.output.edges[nEdge] = next + } + + stack.push({ + qNode: qNode, + output: next, + node: node + }) + } + } + } + } + + return output +} +lunr.TokenSet.Builder = function () { + this.previousWord = "" + this.root = new lunr.TokenSet + this.uncheckedNodes = [] + this.minimizedNodes = {} +} + +lunr.TokenSet.Builder.prototype.insert = function (word) { + var node, + commonPrefix = 0 + + if (word < this.previousWord) { + throw new Error ("Out of order word insertion") + } + + for (var i = 0; i < word.length && i < this.previousWord.length; i++) { + if (word[i] != this.previousWord[i]) break + commonPrefix++ + } + + this.minimize(commonPrefix) + + if (this.uncheckedNodes.length == 0) { + node = this.root + } else { + node = this.uncheckedNodes[this.uncheckedNodes.length - 1].child + } + + for (var i = commonPrefix; i < word.length; i++) { + var nextNode = new lunr.TokenSet, + char = word[i] + + node.edges[char] = nextNode + + this.uncheckedNodes.push({ + parent: node, + char: char, + child: nextNode + }) + + node = nextNode + } + + node.final = true + this.previousWord = word +} + +lunr.TokenSet.Builder.prototype.finish = function () { + this.minimize(0) +} + +lunr.TokenSet.Builder.prototype.minimize = function (downTo) { + for (var i = this.uncheckedNodes.length - 1; i >= downTo; i--) { + var node = this.uncheckedNodes[i], + childKey = node.child.toString() + + if (childKey in this.minimizedNodes) { + node.parent.edges[node.char] = this.minimizedNodes[childKey] + } else { + // Cache the key for this node since + // we know it can't change anymore + node.child._str = childKey + + this.minimizedNodes[childKey] = node.child + } + + this.uncheckedNodes.pop() + } +} +/*! + * lunr.Index + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * An index contains the built index of all documents and provides a query interface + * to the index. + * + * Usually instances of lunr.Index will not be created using this constructor, instead + * lunr.Builder should be used to construct new indexes, or lunr.Index.load should be + * used to load previously built and serialized indexes. + * + * @constructor + * @param {Object} attrs - The attributes of the built search index. + * @param {Object} attrs.invertedIndex - An index of term/field to document reference. + * @param {Object} attrs.fieldVectors - Field vectors + * @param {lunr.TokenSet} attrs.tokenSet - An set of all corpus tokens. + * @param {string[]} attrs.fields - The names of indexed document fields. + * @param {lunr.Pipeline} attrs.pipeline - The pipeline to use for search terms. + */ +lunr.Index = function (attrs) { + this.invertedIndex = attrs.invertedIndex + this.fieldVectors = attrs.fieldVectors + this.tokenSet = attrs.tokenSet + this.fields = attrs.fields + this.pipeline = attrs.pipeline +} + +/** + * A result contains details of a document matching a search query. + * @typedef {Object} lunr.Index~Result + * @property {string} ref - The reference of the document this result represents. + * @property {number} score - A number between 0 and 1 representing how similar this document is to the query. + * @property {lunr.MatchData} matchData - Contains metadata about this match including which term(s) caused the match. + */ + +/** + * Although lunr provides the ability to create queries using lunr.Query, it also provides a simple + * query language which itself is parsed into an instance of lunr.Query. + * + * For programmatically building queries it is advised to directly use lunr.Query, the query language + * is best used for human entered text rather than program generated text. + * + * At its simplest queries can just be a single term, e.g. `hello`, multiple terms are also supported + * and will be combined with OR, e.g `hello world` will match documents that contain either 'hello' + * or 'world', though those that contain both will rank higher in the results. + * + * Wildcards can be included in terms to match one or more unspecified characters, these wildcards can + * be inserted anywhere within the term, and more than one wildcard can exist in a single term. Adding + * wildcards will increase the number of documents that will be found but can also have a negative + * impact on query performance, especially with wildcards at the beginning of a term. + * + * Terms can be restricted to specific fields, e.g. `title:hello`, only documents with the term + * hello in the title field will match this query. Using a field not present in the index will lead + * to an error being thrown. + * + * Modifiers can also be added to terms, lunr supports edit distance and boost modifiers on terms. A term + * boost will make documents matching that term score higher, e.g. `foo^5`. Edit distance is also supported + * to provide fuzzy matching, e.g. 'hello~2' will match documents with hello with an edit distance of 2. + * Avoid large values for edit distance to improve query performance. + * + * Each term also supports a presence modifier. By default a term's presence in document is optional, however + * this can be changed to either required or prohibited. For a term's presence to be required in a document the + * term should be prefixed with a '+', e.g. `+foo bar` is a search for documents that must contain 'foo' and + * optionally contain 'bar'. Conversely a leading '-' sets the terms presence to prohibited, i.e. it must not + * appear in a document, e.g. `-foo bar` is a search for documents that do not contain 'foo' but may contain 'bar'. + * + * To escape special characters the backslash character '\' can be used, this allows searches to include + * characters that would normally be considered modifiers, e.g. `foo\~2` will search for a term "foo~2" instead + * of attempting to apply a boost of 2 to the search term "foo". + * + * @typedef {string} lunr.Index~QueryString + * @example Simple single term query + * hello + * @example Multiple term query + * hello world + * @example term scoped to a field + * title:hello + * @example term with a boost of 10 + * hello^10 + * @example term with an edit distance of 2 + * hello~2 + * @example terms with presence modifiers + * -foo +bar baz + */ + +/** + * Performs a search against the index using lunr query syntax. + * + * Results will be returned sorted by their score, the most relevant results + * will be returned first. For details on how the score is calculated, please see + * the {@link https://lunrjs.com/guides/searching.html#scoring|guide}. + * + * For more programmatic querying use lunr.Index#query. + * + * @param {lunr.Index~QueryString} queryString - A string containing a lunr query. + * @throws {lunr.QueryParseError} If the passed query string cannot be parsed. + * @returns {lunr.Index~Result[]} + */ +lunr.Index.prototype.search = function (queryString) { + return this.query(function (query) { + var parser = new lunr.QueryParser(queryString, query) + parser.parse() + }) +} + +/** + * A query builder callback provides a query object to be used to express + * the query to perform on the index. + * + * @callback lunr.Index~queryBuilder + * @param {lunr.Query} query - The query object to build up. + * @this lunr.Query + */ + +/** + * Performs a query against the index using the yielded lunr.Query object. + * + * If performing programmatic queries against the index, this method is preferred + * over lunr.Index#search so as to avoid the additional query parsing overhead. + * + * A query object is yielded to the supplied function which should be used to + * express the query to be run against the index. + * + * Note that although this function takes a callback parameter it is _not_ an + * asynchronous operation, the callback is just yielded a query object to be + * customized. + * + * @param {lunr.Index~queryBuilder} fn - A function that is used to build the query. + * @returns {lunr.Index~Result[]} + */ +lunr.Index.prototype.query = function (fn) { + // for each query clause + // * process terms + // * expand terms from token set + // * find matching documents and metadata + // * get document vectors + // * score documents + + var query = new lunr.Query(this.fields), + matchingFields = Object.create(null), + queryVectors = Object.create(null), + termFieldCache = Object.create(null), + requiredMatches = Object.create(null), + prohibitedMatches = Object.create(null) + + /* + * To support field level boosts a query vector is created per + * field. An empty vector is eagerly created to support negated + * queries. + */ + for (var i = 0; i < this.fields.length; i++) { + queryVectors[this.fields[i]] = new lunr.Vector + } + + fn.call(query, query) + + for (var i = 0; i < query.clauses.length; i++) { + /* + * Unless the pipeline has been disabled for this term, which is + * the case for terms with wildcards, we need to pass the clause + * term through the search pipeline. A pipeline returns an array + * of processed terms. Pipeline functions may expand the passed + * term, which means we may end up performing multiple index lookups + * for a single query term. + */ + var clause = query.clauses[i], + terms = null, + clauseMatches = lunr.Set.empty + + if (clause.usePipeline) { + terms = this.pipeline.runString(clause.term, { + fields: clause.fields + }) + } else { + terms = [clause.term] + } + + for (var m = 0; m < terms.length; m++) { + var term = terms[m] + + /* + * Each term returned from the pipeline needs to use the same query + * clause object, e.g. the same boost and or edit distance. The + * simplest way to do this is to re-use the clause object but mutate + * its term property. + */ + clause.term = term + + /* + * From the term in the clause we create a token set which will then + * be used to intersect the indexes token set to get a list of terms + * to lookup in the inverted index + */ + var termTokenSet = lunr.TokenSet.fromClause(clause), + expandedTerms = this.tokenSet.intersect(termTokenSet).toArray() + + /* + * If a term marked as required does not exist in the tokenSet it is + * impossible for the search to return any matches. We set all the field + * scoped required matches set to empty and stop examining any further + * clauses. + */ + if (expandedTerms.length === 0 && clause.presence === lunr.Query.presence.REQUIRED) { + for (var k = 0; k < clause.fields.length; k++) { + var field = clause.fields[k] + requiredMatches[field] = lunr.Set.empty + } + + break + } + + for (var j = 0; j < expandedTerms.length; j++) { + /* + * For each term get the posting and termIndex, this is required for + * building the query vector. + */ + var expandedTerm = expandedTerms[j], + posting = this.invertedIndex[expandedTerm], + termIndex = posting._index + + for (var k = 0; k < clause.fields.length; k++) { + /* + * For each field that this query term is scoped by (by default + * all fields are in scope) we need to get all the document refs + * that have this term in that field. + * + * The posting is the entry in the invertedIndex for the matching + * term from above. + */ + var field = clause.fields[k], + fieldPosting = posting[field], + matchingDocumentRefs = Object.keys(fieldPosting), + termField = expandedTerm + "/" + field, + matchingDocumentsSet = new lunr.Set(matchingDocumentRefs) + + /* + * if the presence of this term is required ensure that the matching + * documents are added to the set of required matches for this clause. + * + */ + if (clause.presence == lunr.Query.presence.REQUIRED) { + clauseMatches = clauseMatches.union(matchingDocumentsSet) + + if (requiredMatches[field] === undefined) { + requiredMatches[field] = lunr.Set.complete + } + } + + /* + * if the presence of this term is prohibited ensure that the matching + * documents are added to the set of prohibited matches for this field, + * creating that set if it does not yet exist. + */ + if (clause.presence == lunr.Query.presence.PROHIBITED) { + if (prohibitedMatches[field] === undefined) { + prohibitedMatches[field] = lunr.Set.empty + } + + prohibitedMatches[field] = prohibitedMatches[field].union(matchingDocumentsSet) + + /* + * Prohibited matches should not be part of the query vector used for + * similarity scoring and no metadata should be extracted so we continue + * to the next field + */ + continue + } + + /* + * The query field vector is populated using the termIndex found for + * the term and a unit value with the appropriate boost applied. + * Using upsert because there could already be an entry in the vector + * for the term we are working with. In that case we just add the scores + * together. + */ + queryVectors[field].upsert(termIndex, clause.boost, function (a, b) { return a + b }) + + /** + * If we've already seen this term, field combo then we've already collected + * the matching documents and metadata, no need to go through all that again + */ + if (termFieldCache[termField]) { + continue + } + + for (var l = 0; l < matchingDocumentRefs.length; l++) { + /* + * All metadata for this term/field/document triple + * are then extracted and collected into an instance + * of lunr.MatchData ready to be returned in the query + * results + */ + var matchingDocumentRef = matchingDocumentRefs[l], + matchingFieldRef = new lunr.FieldRef (matchingDocumentRef, field), + metadata = fieldPosting[matchingDocumentRef], + fieldMatch + + if ((fieldMatch = matchingFields[matchingFieldRef]) === undefined) { + matchingFields[matchingFieldRef] = new lunr.MatchData (expandedTerm, field, metadata) + } else { + fieldMatch.add(expandedTerm, field, metadata) + } + + } + + termFieldCache[termField] = true + } + } + } + + /** + * If the presence was required we need to update the requiredMatches field sets. + * We do this after all fields for the term have collected their matches because + * the clause terms presence is required in _any_ of the fields not _all_ of the + * fields. + */ + if (clause.presence === lunr.Query.presence.REQUIRED) { + for (var k = 0; k < clause.fields.length; k++) { + var field = clause.fields[k] + requiredMatches[field] = requiredMatches[field].intersect(clauseMatches) + } + } + } + + /** + * Need to combine the field scoped required and prohibited + * matching documents into a global set of required and prohibited + * matches + */ + var allRequiredMatches = lunr.Set.complete, + allProhibitedMatches = lunr.Set.empty + + for (var i = 0; i < this.fields.length; i++) { + var field = this.fields[i] + + if (requiredMatches[field]) { + allRequiredMatches = allRequiredMatches.intersect(requiredMatches[field]) + } + + if (prohibitedMatches[field]) { + allProhibitedMatches = allProhibitedMatches.union(prohibitedMatches[field]) + } + } + + var matchingFieldRefs = Object.keys(matchingFields), + results = [], + matches = Object.create(null) + + /* + * If the query is negated (contains only prohibited terms) + * we need to get _all_ fieldRefs currently existing in the + * index. This is only done when we know that the query is + * entirely prohibited terms to avoid any cost of getting all + * fieldRefs unnecessarily. + * + * Additionally, blank MatchData must be created to correctly + * populate the results. + */ + if (query.isNegated()) { + matchingFieldRefs = Object.keys(this.fieldVectors) + + for (var i = 0; i < matchingFieldRefs.length; i++) { + var matchingFieldRef = matchingFieldRefs[i] + var fieldRef = lunr.FieldRef.fromString(matchingFieldRef) + matchingFields[matchingFieldRef] = new lunr.MatchData + } + } + + for (var i = 0; i < matchingFieldRefs.length; i++) { + /* + * Currently we have document fields that match the query, but we + * need to return documents. The matchData and scores are combined + * from multiple fields belonging to the same document. + * + * Scores are calculated by field, using the query vectors created + * above, and combined into a final document score using addition. + */ + var fieldRef = lunr.FieldRef.fromString(matchingFieldRefs[i]), + docRef = fieldRef.docRef + + if (!allRequiredMatches.contains(docRef)) { + continue + } + + if (allProhibitedMatches.contains(docRef)) { + continue + } + + var fieldVector = this.fieldVectors[fieldRef], + score = queryVectors[fieldRef.fieldName].similarity(fieldVector), + docMatch + + if ((docMatch = matches[docRef]) !== undefined) { + docMatch.score += score + docMatch.matchData.combine(matchingFields[fieldRef]) + } else { + var match = { + ref: docRef, + score: score, + matchData: matchingFields[fieldRef] + } + matches[docRef] = match + results.push(match) + } + } + + /* + * Sort the results objects by score, highest first. + */ + return results.sort(function (a, b) { + return b.score - a.score + }) +} + +/** + * Prepares the index for JSON serialization. + * + * The schema for this JSON blob will be described in a + * separate JSON schema file. + * + * @returns {Object} + */ +lunr.Index.prototype.toJSON = function () { + var invertedIndex = Object.keys(this.invertedIndex) + .sort() + .map(function (term) { + return [term, this.invertedIndex[term]] + }, this) + + var fieldVectors = Object.keys(this.fieldVectors) + .map(function (ref) { + return [ref, this.fieldVectors[ref].toJSON()] + }, this) + + return { + version: lunr.version, + fields: this.fields, + fieldVectors: fieldVectors, + invertedIndex: invertedIndex, + pipeline: this.pipeline.toJSON() + } +} + +/** + * Loads a previously serialized lunr.Index + * + * @param {Object} serializedIndex - A previously serialized lunr.Index + * @returns {lunr.Index} + */ +lunr.Index.load = function (serializedIndex) { + var attrs = {}, + fieldVectors = {}, + serializedVectors = serializedIndex.fieldVectors, + invertedIndex = Object.create(null), + serializedInvertedIndex = serializedIndex.invertedIndex, + tokenSetBuilder = new lunr.TokenSet.Builder, + pipeline = lunr.Pipeline.load(serializedIndex.pipeline) + + if (serializedIndex.version != lunr.version) { + lunr.utils.warn("Version mismatch when loading serialised index. Current version of lunr '" + lunr.version + "' does not match serialized index '" + serializedIndex.version + "'") + } + + for (var i = 0; i < serializedVectors.length; i++) { + var tuple = serializedVectors[i], + ref = tuple[0], + elements = tuple[1] + + fieldVectors[ref] = new lunr.Vector(elements) + } + + for (var i = 0; i < serializedInvertedIndex.length; i++) { + var tuple = serializedInvertedIndex[i], + term = tuple[0], + posting = tuple[1] + + tokenSetBuilder.insert(term) + invertedIndex[term] = posting + } + + tokenSetBuilder.finish() + + attrs.fields = serializedIndex.fields + + attrs.fieldVectors = fieldVectors + attrs.invertedIndex = invertedIndex + attrs.tokenSet = tokenSetBuilder.root + attrs.pipeline = pipeline + + return new lunr.Index(attrs) +} +/*! + * lunr.Builder + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.Builder performs indexing on a set of documents and + * returns instances of lunr.Index ready for querying. + * + * All configuration of the index is done via the builder, the + * fields to index, the document reference, the text processing + * pipeline and document scoring parameters are all set on the + * builder before indexing. + * + * @constructor + * @property {string} _ref - Internal reference to the document reference field. + * @property {string[]} _fields - Internal reference to the document fields to index. + * @property {object} invertedIndex - The inverted index maps terms to document fields. + * @property {object} documentTermFrequencies - Keeps track of document term frequencies. + * @property {object} documentLengths - Keeps track of the length of documents added to the index. + * @property {lunr.tokenizer} tokenizer - Function for splitting strings into tokens for indexing. + * @property {lunr.Pipeline} pipeline - The pipeline performs text processing on tokens before indexing. + * @property {lunr.Pipeline} searchPipeline - A pipeline for processing search terms before querying the index. + * @property {number} documentCount - Keeps track of the total number of documents indexed. + * @property {number} _b - A parameter to control field length normalization, setting this to 0 disabled normalization, 1 fully normalizes field lengths, the default value is 0.75. + * @property {number} _k1 - A parameter to control how quickly an increase in term frequency results in term frequency saturation, the default value is 1.2. + * @property {number} termIndex - A counter incremented for each unique term, used to identify a terms position in the vector space. + * @property {array} metadataWhitelist - A list of metadata keys that have been whitelisted for entry in the index. + */ +lunr.Builder = function () { + this._ref = "id" + this._fields = Object.create(null) + this._documents = Object.create(null) + this.invertedIndex = Object.create(null) + this.fieldTermFrequencies = {} + this.fieldLengths = {} + this.tokenizer = lunr.tokenizer + this.pipeline = new lunr.Pipeline + this.searchPipeline = new lunr.Pipeline + this.documentCount = 0 + this._b = 0.75 + this._k1 = 1.2 + this.termIndex = 0 + this.metadataWhitelist = [] +} + +/** + * Sets the document field used as the document reference. Every document must have this field. + * The type of this field in the document should be a string, if it is not a string it will be + * coerced into a string by calling toString. + * + * The default ref is 'id'. + * + * The ref should _not_ be changed during indexing, it should be set before any documents are + * added to the index. Changing it during indexing can lead to inconsistent results. + * + * @param {string} ref - The name of the reference field in the document. + */ +lunr.Builder.prototype.ref = function (ref) { + this._ref = ref +} + +/** + * A function that is used to extract a field from a document. + * + * Lunr expects a field to be at the top level of a document, if however the field + * is deeply nested within a document an extractor function can be used to extract + * the right field for indexing. + * + * @callback fieldExtractor + * @param {object} doc - The document being added to the index. + * @returns {?(string|object|object[])} obj - The object that will be indexed for this field. + * @example Extracting a nested field + * function (doc) { return doc.nested.field } + */ + +/** + * Adds a field to the list of document fields that will be indexed. Every document being + * indexed should have this field. Null values for this field in indexed documents will + * not cause errors but will limit the chance of that document being retrieved by searches. + * + * All fields should be added before adding documents to the index. Adding fields after + * a document has been indexed will have no effect on already indexed documents. + * + * Fields can be boosted at build time. This allows terms within that field to have more + * importance when ranking search results. Use a field boost to specify that matches within + * one field are more important than other fields. + * + * @param {string} fieldName - The name of a field to index in all documents. + * @param {object} attributes - Optional attributes associated with this field. + * @param {number} [attributes.boost=1] - Boost applied to all terms within this field. + * @param {fieldExtractor} [attributes.extractor] - Function to extract a field from a document. + * @throws {RangeError} fieldName cannot contain unsupported characters '/' + */ +lunr.Builder.prototype.field = function (fieldName, attributes) { + if (/\//.test(fieldName)) { + throw new RangeError ("Field '" + fieldName + "' contains illegal character '/'") + } + + this._fields[fieldName] = attributes || {} +} + +/** + * A parameter to tune the amount of field length normalisation that is applied when + * calculating relevance scores. A value of 0 will completely disable any normalisation + * and a value of 1 will fully normalise field lengths. The default is 0.75. Values of b + * will be clamped to the range 0 - 1. + * + * @param {number} number - The value to set for this tuning parameter. + */ +lunr.Builder.prototype.b = function (number) { + if (number < 0) { + this._b = 0 + } else if (number > 1) { + this._b = 1 + } else { + this._b = number + } +} + +/** + * A parameter that controls the speed at which a rise in term frequency results in term + * frequency saturation. The default value is 1.2. Setting this to a higher value will give + * slower saturation levels, a lower value will result in quicker saturation. + * + * @param {number} number - The value to set for this tuning parameter. + */ +lunr.Builder.prototype.k1 = function (number) { + this._k1 = number +} + +/** + * Adds a document to the index. + * + * Before adding fields to the index the index should have been fully setup, with the document + * ref and all fields to index already having been specified. + * + * The document must have a field name as specified by the ref (by default this is 'id') and + * it should have all fields defined for indexing, though null or undefined values will not + * cause errors. + * + * Entire documents can be boosted at build time. Applying a boost to a document indicates that + * this document should rank higher in search results than other documents. + * + * @param {object} doc - The document to add to the index. + * @param {object} attributes - Optional attributes associated with this document. + * @param {number} [attributes.boost=1] - Boost applied to all terms within this document. + */ +lunr.Builder.prototype.add = function (doc, attributes) { + var docRef = doc[this._ref], + fields = Object.keys(this._fields) + + this._documents[docRef] = attributes || {} + this.documentCount += 1 + + for (var i = 0; i < fields.length; i++) { + var fieldName = fields[i], + extractor = this._fields[fieldName].extractor, + field = extractor ? extractor(doc) : doc[fieldName], + tokens = this.tokenizer(field, { + fields: [fieldName] + }), + terms = this.pipeline.run(tokens), + fieldRef = new lunr.FieldRef (docRef, fieldName), + fieldTerms = Object.create(null) + + this.fieldTermFrequencies[fieldRef] = fieldTerms + this.fieldLengths[fieldRef] = 0 + + // store the length of this field for this document + this.fieldLengths[fieldRef] += terms.length + + // calculate term frequencies for this field + for (var j = 0; j < terms.length; j++) { + var term = terms[j] + + if (fieldTerms[term] == undefined) { + fieldTerms[term] = 0 + } + + fieldTerms[term] += 1 + + // add to inverted index + // create an initial posting if one doesn't exist + if (this.invertedIndex[term] == undefined) { + var posting = Object.create(null) + posting["_index"] = this.termIndex + this.termIndex += 1 + + for (var k = 0; k < fields.length; k++) { + posting[fields[k]] = Object.create(null) + } + + this.invertedIndex[term] = posting + } + + // add an entry for this term/fieldName/docRef to the invertedIndex + if (this.invertedIndex[term][fieldName][docRef] == undefined) { + this.invertedIndex[term][fieldName][docRef] = Object.create(null) + } + + // store all whitelisted metadata about this token in the + // inverted index + for (var l = 0; l < this.metadataWhitelist.length; l++) { + var metadataKey = this.metadataWhitelist[l], + metadata = term.metadata[metadataKey] + + if (this.invertedIndex[term][fieldName][docRef][metadataKey] == undefined) { + this.invertedIndex[term][fieldName][docRef][metadataKey] = [] + } + + this.invertedIndex[term][fieldName][docRef][metadataKey].push(metadata) + } + } + + } +} + +/** + * Calculates the average document length for this index + * + * @private + */ +lunr.Builder.prototype.calculateAverageFieldLengths = function () { + + var fieldRefs = Object.keys(this.fieldLengths), + numberOfFields = fieldRefs.length, + accumulator = {}, + documentsWithField = {} + + for (var i = 0; i < numberOfFields; i++) { + var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]), + field = fieldRef.fieldName + + documentsWithField[field] || (documentsWithField[field] = 0) + documentsWithField[field] += 1 + + accumulator[field] || (accumulator[field] = 0) + accumulator[field] += this.fieldLengths[fieldRef] + } + + var fields = Object.keys(this._fields) + + for (var i = 0; i < fields.length; i++) { + var fieldName = fields[i] + accumulator[fieldName] = accumulator[fieldName] / documentsWithField[fieldName] + } + + this.averageFieldLength = accumulator +} + +/** + * Builds a vector space model of every document using lunr.Vector + * + * @private + */ +lunr.Builder.prototype.createFieldVectors = function () { + var fieldVectors = {}, + fieldRefs = Object.keys(this.fieldTermFrequencies), + fieldRefsLength = fieldRefs.length, + termIdfCache = Object.create(null) + + for (var i = 0; i < fieldRefsLength; i++) { + var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]), + fieldName = fieldRef.fieldName, + fieldLength = this.fieldLengths[fieldRef], + fieldVector = new lunr.Vector, + termFrequencies = this.fieldTermFrequencies[fieldRef], + terms = Object.keys(termFrequencies), + termsLength = terms.length + + + var fieldBoost = this._fields[fieldName].boost || 1, + docBoost = this._documents[fieldRef.docRef].boost || 1 + + for (var j = 0; j < termsLength; j++) { + var term = terms[j], + tf = termFrequencies[term], + termIndex = this.invertedIndex[term]._index, + idf, score, scoreWithPrecision + + if (termIdfCache[term] === undefined) { + idf = lunr.idf(this.invertedIndex[term], this.documentCount) + termIdfCache[term] = idf + } else { + idf = termIdfCache[term] + } + + score = idf * ((this._k1 + 1) * tf) / (this._k1 * (1 - this._b + this._b * (fieldLength / this.averageFieldLength[fieldName])) + tf) + score *= fieldBoost + score *= docBoost + scoreWithPrecision = Math.round(score * 1000) / 1000 + // Converts 1.23456789 to 1.234. + // Reducing the precision so that the vectors take up less + // space when serialised. Doing it now so that they behave + // the same before and after serialisation. Also, this is + // the fastest approach to reducing a number's precision in + // JavaScript. + + fieldVector.insert(termIndex, scoreWithPrecision) + } + + fieldVectors[fieldRef] = fieldVector + } + + this.fieldVectors = fieldVectors +} + +/** + * Creates a token set of all tokens in the index using lunr.TokenSet + * + * @private + */ +lunr.Builder.prototype.createTokenSet = function () { + this.tokenSet = lunr.TokenSet.fromArray( + Object.keys(this.invertedIndex).sort() + ) +} + +/** + * Builds the index, creating an instance of lunr.Index. + * + * This completes the indexing process and should only be called + * once all documents have been added to the index. + * + * @returns {lunr.Index} + */ +lunr.Builder.prototype.build = function () { + this.calculateAverageFieldLengths() + this.createFieldVectors() + this.createTokenSet() + + return new lunr.Index({ + invertedIndex: this.invertedIndex, + fieldVectors: this.fieldVectors, + tokenSet: this.tokenSet, + fields: Object.keys(this._fields), + pipeline: this.searchPipeline + }) +} + +/** + * Applies a plugin to the index builder. + * + * A plugin is a function that is called with the index builder as its context. + * Plugins can be used to customise or extend the behaviour of the index + * in some way. A plugin is just a function, that encapsulated the custom + * behaviour that should be applied when building the index. + * + * The plugin function will be called with the index builder as its argument, additional + * arguments can also be passed when calling use. The function will be called + * with the index builder as its context. + * + * @param {Function} plugin The plugin to apply. + */ +lunr.Builder.prototype.use = function (fn) { + var args = Array.prototype.slice.call(arguments, 1) + args.unshift(this) + fn.apply(this, args) +} +/** + * Contains and collects metadata about a matching document. + * A single instance of lunr.MatchData is returned as part of every + * lunr.Index~Result. + * + * @constructor + * @param {string} term - The term this match data is associated with + * @param {string} field - The field in which the term was found + * @param {object} metadata - The metadata recorded about this term in this field + * @property {object} metadata - A cloned collection of metadata associated with this document. + * @see {@link lunr.Index~Result} + */ +lunr.MatchData = function (term, field, metadata) { + var clonedMetadata = Object.create(null), + metadataKeys = Object.keys(metadata || {}) + + // Cloning the metadata to prevent the original + // being mutated during match data combination. + // Metadata is kept in an array within the inverted + // index so cloning the data can be done with + // Array#slice + for (var i = 0; i < metadataKeys.length; i++) { + var key = metadataKeys[i] + clonedMetadata[key] = metadata[key].slice() + } + + this.metadata = Object.create(null) + + if (term !== undefined) { + this.metadata[term] = Object.create(null) + this.metadata[term][field] = clonedMetadata + } +} + +/** + * An instance of lunr.MatchData will be created for every term that matches a + * document. However only one instance is required in a lunr.Index~Result. This + * method combines metadata from another instance of lunr.MatchData with this + * objects metadata. + * + * @param {lunr.MatchData} otherMatchData - Another instance of match data to merge with this one. + * @see {@link lunr.Index~Result} + */ +lunr.MatchData.prototype.combine = function (otherMatchData) { + var terms = Object.keys(otherMatchData.metadata) + + for (var i = 0; i < terms.length; i++) { + var term = terms[i], + fields = Object.keys(otherMatchData.metadata[term]) + + if (this.metadata[term] == undefined) { + this.metadata[term] = Object.create(null) + } + + for (var j = 0; j < fields.length; j++) { + var field = fields[j], + keys = Object.keys(otherMatchData.metadata[term][field]) + + if (this.metadata[term][field] == undefined) { + this.metadata[term][field] = Object.create(null) + } + + for (var k = 0; k < keys.length; k++) { + var key = keys[k] + + if (this.metadata[term][field][key] == undefined) { + this.metadata[term][field][key] = otherMatchData.metadata[term][field][key] + } else { + this.metadata[term][field][key] = this.metadata[term][field][key].concat(otherMatchData.metadata[term][field][key]) + } + + } + } + } +} + +/** + * Add metadata for a term/field pair to this instance of match data. + * + * @param {string} term - The term this match data is associated with + * @param {string} field - The field in which the term was found + * @param {object} metadata - The metadata recorded about this term in this field + */ +lunr.MatchData.prototype.add = function (term, field, metadata) { + if (!(term in this.metadata)) { + this.metadata[term] = Object.create(null) + this.metadata[term][field] = metadata + return + } + + if (!(field in this.metadata[term])) { + this.metadata[term][field] = metadata + return + } + + var metadataKeys = Object.keys(metadata) + + for (var i = 0; i < metadataKeys.length; i++) { + var key = metadataKeys[i] + + if (key in this.metadata[term][field]) { + this.metadata[term][field][key] = this.metadata[term][field][key].concat(metadata[key]) + } else { + this.metadata[term][field][key] = metadata[key] + } + } +} +/** + * A lunr.Query provides a programmatic way of defining queries to be performed + * against a {@link lunr.Index}. + * + * Prefer constructing a lunr.Query using the {@link lunr.Index#query} method + * so the query object is pre-initialized with the right index fields. + * + * @constructor + * @property {lunr.Query~Clause[]} clauses - An array of query clauses. + * @property {string[]} allFields - An array of all available fields in a lunr.Index. + */ +lunr.Query = function (allFields) { + this.clauses = [] + this.allFields = allFields +} + +/** + * Constants for indicating what kind of automatic wildcard insertion will be used when constructing a query clause. + * + * This allows wildcards to be added to the beginning and end of a term without having to manually do any string + * concatenation. + * + * The wildcard constants can be bitwise combined to select both leading and trailing wildcards. + * + * @constant + * @default + * @property {number} wildcard.NONE - The term will have no wildcards inserted, this is the default behaviour + * @property {number} wildcard.LEADING - Prepend the term with a wildcard, unless a leading wildcard already exists + * @property {number} wildcard.TRAILING - Append a wildcard to the term, unless a trailing wildcard already exists + * @see lunr.Query~Clause + * @see lunr.Query#clause + * @see lunr.Query#term + * @example query term with trailing wildcard + * query.term('foo', { wildcard: lunr.Query.wildcard.TRAILING }) + * @example query term with leading and trailing wildcard + * query.term('foo', { + * wildcard: lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING + * }) + */ + +lunr.Query.wildcard = new String ("*") +lunr.Query.wildcard.NONE = 0 +lunr.Query.wildcard.LEADING = 1 +lunr.Query.wildcard.TRAILING = 2 + +/** + * Constants for indicating what kind of presence a term must have in matching documents. + * + * @constant + * @enum {number} + * @see lunr.Query~Clause + * @see lunr.Query#clause + * @see lunr.Query#term + * @example query term with required presence + * query.term('foo', { presence: lunr.Query.presence.REQUIRED }) + */ +lunr.Query.presence = { + /** + * Term's presence in a document is optional, this is the default value. + */ + OPTIONAL: 1, + + /** + * Term's presence in a document is required, documents that do not contain + * this term will not be returned. + */ + REQUIRED: 2, + + /** + * Term's presence in a document is prohibited, documents that do contain + * this term will not be returned. + */ + PROHIBITED: 3 +} + +/** + * A single clause in a {@link lunr.Query} contains a term and details on how to + * match that term against a {@link lunr.Index}. + * + * @typedef {Object} lunr.Query~Clause + * @property {string[]} fields - The fields in an index this clause should be matched against. + * @property {number} [boost=1] - Any boost that should be applied when matching this clause. + * @property {number} [editDistance] - Whether the term should have fuzzy matching applied, and how fuzzy the match should be. + * @property {boolean} [usePipeline] - Whether the term should be passed through the search pipeline. + * @property {number} [wildcard=lunr.Query.wildcard.NONE] - Whether the term should have wildcards appended or prepended. + * @property {number} [presence=lunr.Query.presence.OPTIONAL] - The terms presence in any matching documents. + */ + +/** + * Adds a {@link lunr.Query~Clause} to this query. + * + * Unless the clause contains the fields to be matched all fields will be matched. In addition + * a default boost of 1 is applied to the clause. + * + * @param {lunr.Query~Clause} clause - The clause to add to this query. + * @see lunr.Query~Clause + * @returns {lunr.Query} + */ +lunr.Query.prototype.clause = function (clause) { + if (!('fields' in clause)) { + clause.fields = this.allFields + } + + if (!('boost' in clause)) { + clause.boost = 1 + } + + if (!('usePipeline' in clause)) { + clause.usePipeline = true + } + + if (!('wildcard' in clause)) { + clause.wildcard = lunr.Query.wildcard.NONE + } + + if ((clause.wildcard & lunr.Query.wildcard.LEADING) && (clause.term.charAt(0) != lunr.Query.wildcard)) { + clause.term = "*" + clause.term + } + + if ((clause.wildcard & lunr.Query.wildcard.TRAILING) && (clause.term.slice(-1) != lunr.Query.wildcard)) { + clause.term = "" + clause.term + "*" + } + + if (!('presence' in clause)) { + clause.presence = lunr.Query.presence.OPTIONAL + } + + this.clauses.push(clause) + + return this +} + +/** + * A negated query is one in which every clause has a presence of + * prohibited. These queries require some special processing to return + * the expected results. + * + * @returns boolean + */ +lunr.Query.prototype.isNegated = function () { + for (var i = 0; i < this.clauses.length; i++) { + if (this.clauses[i].presence != lunr.Query.presence.PROHIBITED) { + return false + } + } + + return true +} + +/** + * Adds a term to the current query, under the covers this will create a {@link lunr.Query~Clause} + * to the list of clauses that make up this query. + * + * The term is used as is, i.e. no tokenization will be performed by this method. Instead conversion + * to a token or token-like string should be done before calling this method. + * + * The term will be converted to a string by calling `toString`. Multiple terms can be passed as an + * array, each term in the array will share the same options. + * + * @param {object|object[]} term - The term(s) to add to the query. + * @param {object} [options] - Any additional properties to add to the query clause. + * @returns {lunr.Query} + * @see lunr.Query#clause + * @see lunr.Query~Clause + * @example adding a single term to a query + * query.term("foo") + * @example adding a single term to a query and specifying search fields, term boost and automatic trailing wildcard + * query.term("foo", { + * fields: ["title"], + * boost: 10, + * wildcard: lunr.Query.wildcard.TRAILING + * }) + * @example using lunr.tokenizer to convert a string to tokens before using them as terms + * query.term(lunr.tokenizer("foo bar")) + */ +lunr.Query.prototype.term = function (term, options) { + if (Array.isArray(term)) { + term.forEach(function (t) { this.term(t, lunr.utils.clone(options)) }, this) + return this + } + + var clause = options || {} + clause.term = term.toString() + + this.clause(clause) + + return this +} +lunr.QueryParseError = function (message, start, end) { + this.name = "QueryParseError" + this.message = message + this.start = start + this.end = end +} + +lunr.QueryParseError.prototype = new Error +lunr.QueryLexer = function (str) { + this.lexemes = [] + this.str = str + this.length = str.length + this.pos = 0 + this.start = 0 + this.escapeCharPositions = [] +} + +lunr.QueryLexer.prototype.run = function () { + var state = lunr.QueryLexer.lexText + + while (state) { + state = state(this) + } +} + +lunr.QueryLexer.prototype.sliceString = function () { + var subSlices = [], + sliceStart = this.start, + sliceEnd = this.pos + + for (var i = 0; i < this.escapeCharPositions.length; i++) { + sliceEnd = this.escapeCharPositions[i] + subSlices.push(this.str.slice(sliceStart, sliceEnd)) + sliceStart = sliceEnd + 1 + } + + subSlices.push(this.str.slice(sliceStart, this.pos)) + this.escapeCharPositions.length = 0 + + return subSlices.join('') +} + +lunr.QueryLexer.prototype.emit = function (type) { + this.lexemes.push({ + type: type, + str: this.sliceString(), + start: this.start, + end: this.pos + }) + + this.start = this.pos +} + +lunr.QueryLexer.prototype.escapeCharacter = function () { + this.escapeCharPositions.push(this.pos - 1) + this.pos += 1 +} + +lunr.QueryLexer.prototype.next = function () { + if (this.pos >= this.length) { + return lunr.QueryLexer.EOS + } + + var char = this.str.charAt(this.pos) + this.pos += 1 + return char +} + +lunr.QueryLexer.prototype.width = function () { + return this.pos - this.start +} + +lunr.QueryLexer.prototype.ignore = function () { + if (this.start == this.pos) { + this.pos += 1 + } + + this.start = this.pos +} + +lunr.QueryLexer.prototype.backup = function () { + this.pos -= 1 +} + +lunr.QueryLexer.prototype.acceptDigitRun = function () { + var char, charCode + + do { + char = this.next() + charCode = char.charCodeAt(0) + } while (charCode > 47 && charCode < 58) + + if (char != lunr.QueryLexer.EOS) { + this.backup() + } +} + +lunr.QueryLexer.prototype.more = function () { + return this.pos < this.length +} + +lunr.QueryLexer.EOS = 'EOS' +lunr.QueryLexer.FIELD = 'FIELD' +lunr.QueryLexer.TERM = 'TERM' +lunr.QueryLexer.EDIT_DISTANCE = 'EDIT_DISTANCE' +lunr.QueryLexer.BOOST = 'BOOST' +lunr.QueryLexer.PRESENCE = 'PRESENCE' + +lunr.QueryLexer.lexField = function (lexer) { + lexer.backup() + lexer.emit(lunr.QueryLexer.FIELD) + lexer.ignore() + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexTerm = function (lexer) { + if (lexer.width() > 1) { + lexer.backup() + lexer.emit(lunr.QueryLexer.TERM) + } + + lexer.ignore() + + if (lexer.more()) { + return lunr.QueryLexer.lexText + } +} + +lunr.QueryLexer.lexEditDistance = function (lexer) { + lexer.ignore() + lexer.acceptDigitRun() + lexer.emit(lunr.QueryLexer.EDIT_DISTANCE) + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexBoost = function (lexer) { + lexer.ignore() + lexer.acceptDigitRun() + lexer.emit(lunr.QueryLexer.BOOST) + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexEOS = function (lexer) { + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } +} + +// This matches the separator used when tokenising fields +// within a document. These should match otherwise it is +// not possible to search for some tokens within a document. +// +// It is possible for the user to change the separator on the +// tokenizer so it _might_ clash with any other of the special +// characters already used within the search string, e.g. :. +// +// This means that it is possible to change the separator in +// such a way that makes some words unsearchable using a search +// string. +lunr.QueryLexer.termSeparator = lunr.tokenizer.separator + +lunr.QueryLexer.lexText = function (lexer) { + while (true) { + var char = lexer.next() + + if (char == lunr.QueryLexer.EOS) { + return lunr.QueryLexer.lexEOS + } + + // Escape character is '\' + if (char.charCodeAt(0) == 92) { + lexer.escapeCharacter() + continue + } + + if (char == ":") { + return lunr.QueryLexer.lexField + } + + if (char == "~") { + lexer.backup() + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } + return lunr.QueryLexer.lexEditDistance + } + + if (char == "^") { + lexer.backup() + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } + return lunr.QueryLexer.lexBoost + } + + // "+" indicates term presence is required + // checking for length to ensure that only + // leading "+" are considered + if (char == "+" && lexer.width() === 1) { + lexer.emit(lunr.QueryLexer.PRESENCE) + return lunr.QueryLexer.lexText + } + + // "-" indicates term presence is prohibited + // checking for length to ensure that only + // leading "-" are considered + if (char == "-" && lexer.width() === 1) { + lexer.emit(lunr.QueryLexer.PRESENCE) + return lunr.QueryLexer.lexText + } + + if (char.match(lunr.QueryLexer.termSeparator)) { + return lunr.QueryLexer.lexTerm + } + } +} + +lunr.QueryParser = function (str, query) { + this.lexer = new lunr.QueryLexer (str) + this.query = query + this.currentClause = {} + this.lexemeIdx = 0 +} + +lunr.QueryParser.prototype.parse = function () { + this.lexer.run() + this.lexemes = this.lexer.lexemes + + var state = lunr.QueryParser.parseClause + + while (state) { + state = state(this) + } + + return this.query +} + +lunr.QueryParser.prototype.peekLexeme = function () { + return this.lexemes[this.lexemeIdx] +} + +lunr.QueryParser.prototype.consumeLexeme = function () { + var lexeme = this.peekLexeme() + this.lexemeIdx += 1 + return lexeme +} + +lunr.QueryParser.prototype.nextClause = function () { + var completedClause = this.currentClause + this.query.clause(completedClause) + this.currentClause = {} +} + +lunr.QueryParser.parseClause = function (parser) { + var lexeme = parser.peekLexeme() + + if (lexeme == undefined) { + return + } + + switch (lexeme.type) { + case lunr.QueryLexer.PRESENCE: + return lunr.QueryParser.parsePresence + case lunr.QueryLexer.FIELD: + return lunr.QueryParser.parseField + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expected either a field or a term, found " + lexeme.type + + if (lexeme.str.length >= 1) { + errorMessage += " with value '" + lexeme.str + "'" + } + + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } +} + +lunr.QueryParser.parsePresence = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + switch (lexeme.str) { + case "-": + parser.currentClause.presence = lunr.Query.presence.PROHIBITED + break + case "+": + parser.currentClause.presence = lunr.Query.presence.REQUIRED + break + default: + var errorMessage = "unrecognised presence operator'" + lexeme.str + "'" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + var errorMessage = "expecting term or field, found nothing" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.FIELD: + return lunr.QueryParser.parseField + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expecting term or field, found '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseField = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + if (parser.query.allFields.indexOf(lexeme.str) == -1) { + var possibleFields = parser.query.allFields.map(function (f) { return "'" + f + "'" }).join(', '), + errorMessage = "unrecognised field '" + lexeme.str + "', possible fields: " + possibleFields + + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.fields = [lexeme.str] + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + var errorMessage = "expecting term, found nothing" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expecting term, found '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseTerm = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + parser.currentClause.term = lexeme.str.toLowerCase() + + if (lexeme.str.indexOf("*") != -1) { + parser.currentClause.usePipeline = false + } + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseEditDistance = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + var editDistance = parseInt(lexeme.str, 10) + + if (isNaN(editDistance)) { + var errorMessage = "edit distance must be numeric" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.editDistance = editDistance + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseBoost = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + var boost = parseInt(lexeme.str, 10) + + if (isNaN(boost)) { + var errorMessage = "boost must be numeric" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.boost = boost + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + + /** + * export the module via AMD, CommonJS or as a browser global + * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js + */ + ;(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(factory) + } else if (typeof exports === 'object') { + /** + * Node. Does not work with strict CommonJS, but + * only CommonJS-like environments that support module.exports, + * like Node. + */ + module.exports = factory() + } else { + // Browser globals (root is window) + root.lunr = factory() + } + }(this, function () { + /** + * Just return a value to define the module export. + * This example returns an object, but the module + * can return a function as the exported value. + */ + return lunr + })) +})(); diff --git a/search/main.js b/search/main.js new file mode 100644 index 00000000..a5e469d7 --- /dev/null +++ b/search/main.js @@ -0,0 +1,109 @@ +function getSearchTermFromLocation() { + var sPageURL = window.location.search.substring(1); + var sURLVariables = sPageURL.split('&'); + for (var i = 0; i < sURLVariables.length; i++) { + var sParameterName = sURLVariables[i].split('='); + if (sParameterName[0] == 'q') { + return decodeURIComponent(sParameterName[1].replace(/\+/g, '%20')); + } + } +} + +function joinUrl (base, path) { + if (path.substring(0, 1) === "/") { + // path starts with `/`. Thus it is absolute. + return path; + } + if (base.substring(base.length-1) === "/") { + // base ends with `/` + return base + path; + } + return base + "/" + path; +} + +function escapeHtml (value) { + return value.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); +} + +function formatResult (location, title, summary) { + return ''; +} + +function displayResults (results) { + var search_results = document.getElementById("mkdocs-search-results"); + while (search_results.firstChild) { + search_results.removeChild(search_results.firstChild); + } + if (results.length > 0){ + for (var i=0; i < results.length; i++){ + var result = results[i]; + var html = formatResult(result.location, result.title, result.summary); + search_results.insertAdjacentHTML('beforeend', html); + } + } else { + var noResultsText = search_results.getAttribute('data-no-results-text'); + if (!noResultsText) { + noResultsText = "No results found"; + } + search_results.insertAdjacentHTML('beforeend', '

' + noResultsText + '

'); + } +} + +function doSearch () { + var query = document.getElementById('mkdocs-search-query').value; + if (query.length > min_search_length) { + if (!window.Worker) { + displayResults(search(query)); + } else { + searchWorker.postMessage({query: query}); + } + } else { + // Clear results for short queries + displayResults([]); + } +} + +function initSearch () { + var search_input = document.getElementById('mkdocs-search-query'); + if (search_input) { + search_input.addEventListener("keyup", doSearch); + } + var term = getSearchTermFromLocation(); + if (term) { + search_input.value = term; + doSearch(); + } +} + +function onWorkerMessage (e) { + if (e.data.allowSearch) { + initSearch(); + } else if (e.data.results) { + var results = e.data.results; + displayResults(results); + } else if (e.data.config) { + min_search_length = e.data.config.min_search_length-1; + } +} + +if (!window.Worker) { + console.log('Web Worker API not supported'); + // load index in main thread + $.getScript(joinUrl(base_url, "search/worker.js")).done(function () { + console.log('Loaded worker'); + init(); + window.postMessage = function (msg) { + onWorkerMessage({data: msg}); + }; + }).fail(function (jqxhr, settings, exception) { + console.error('Could not load worker.js'); + }); +} else { + // Wrap search in a web worker + var searchWorker = new Worker(joinUrl(base_url, "search/worker.js")); + searchWorker.postMessage({init: true}); + searchWorker.onmessage = onWorkerMessage; +} diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 00000000..ebf1d438 --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"The source code for this page can be found on github . This page contains tutorials and generally useful information regarding packages and system administration in Fedora and Enterprise Linux (Rocky Linux, CentOS Stream). Quick Links \u00b6 Here are some quick links to solid documentation: Fedora Quick Docs Fedora Latest Release Docs Notes \u00b6 Due to the number of articles out there about disabling SELinux, we felt this note was important. Disabling SELinux is and almost always will be a terrible idea. See our antipatterns page as well as the Red Hat Enterprise Linux documentation, Fedora Docs, and Rocky Linux documentation.","title":"Linux Guide and Hints"},{"location":"#quick-links","text":"Here are some quick links to solid documentation: Fedora Quick Docs Fedora Latest Release Docs","title":"Quick Links"},{"location":"#notes","text":"Due to the number of articles out there about disabling SELinux, we felt this note was important. Disabling SELinux is and almost always will be a terrible idea. See our antipatterns page as well as the Red Hat Enterprise Linux documentation, Fedora Docs, and Rocky Linux documentation.","title":"Notes"},{"location":"archive/","text":"This section is for archives. Most of these pages were created by one of our late contributors and have not been updated since his passing. As such, we have moved most of them here as clean up.","title":"Archives"},{"location":"el/","text":"This section contains various articles on setups for Enterprise Linux and Fedora systems.","title":"Enterprise Linux"},{"location":"el/builds/","text":"This page goes over various ways that installs can be automated without the use of PXE. Instead, we can use templated scripts with pre-configured commands, boot images, and mirrors for builds. We cover the following here: CentOS Stream 9 Enterprise Linux 8, 9 Fedora openSUSE 15+ Windows Server","title":"Auto-Provisioning"},{"location":"el/freeipa/","text":"This page is a series of notes and information that goes over how to install and configure FreeIPA on Enterprise Linux 8/9 servers with replicas, as well as configuring client machines to connect and utilize FreeIPA resources, policies (eg sudo), and host based access control methods. We will also go over a scenario of configuring a trust with an Active Directory domain. The client setup will work for Fedora users as the packages are the same, just newer versions. Overview \u00b6 FreeIPA is an integrated security information management system combining Linux, a Directory Server (389), Kerberos, NTP, DNS, DogTag. It's a system that can be loosely compared to Active Directory in what it attempts to solve for Linux and UNIX clients and even mixed environments. While it is not an active directory, it is an integrated Identity and Authentication solution for Linux/UNIX environments, which means it does not support Windows clients. One problem that FreeIPA attempts to solve is giving back control to the Linux/UNIX administration teams of access, authentication, and authorization rather than trying to integrate directly into Active Directory, where the controls do not work the same or do not work at all. And because of this, no third party software is required to be installed. Requirements \u00b6 Here are the list of requirements below. Enterprise Linux 8+ or Fedora Linux An active internet connection to install the packages required or available internal mirrors 2 core, 4GB system with at least 10GB+ disk for /var/lib/dirsrv DNS domain delegation (if a DNS appliance or server already exists) Tutorial Preface, Notes, and Recommendations \u00b6 Potential Pitfalls! Leave SELinux enabled at all times. You will not run into SELinux issues FreeIPA runs better when it controls the DNS domain that it is given - It is recommended DNS is delegated or that FreeIPA run DNS entirely FreeIPA does not run DHCP. ISC DHCP can be configured to do dynamic DNS updates to FreeIPA or hosts can be configured to perform dynamic DNS updates Recommended Information Keep selinux set to enforcing DNS - You must be careful when using DNS. Here are recommendations. 1 Recommendation 1: FreeIPA runs your entire DNS for your network - This requires the DHCP servers to set the DNS servers to the IPA servers. This will be useful in the case that your clients will have their SSH keys added as SSHFP records to DNS when enrolled as clients. This also gives you the added benefit of a client updating its own DNS entries (A and PTR records) if the client is DHCP enabled and the IP changes if you so choose. Recommendation 2: FreeIPA is delegated a subdomain of a domain used already in the network - It's not required for hosts to live in the subdomain to be a member of the IPA domain, but you will lose out on kerberos SSO. Do not try to hijack a domain. Consider setting up a trust with Active Directory if you are in a mixed environment, eg Active Directory already exists - winsync is available, but deprecated and not recommended. IPA servers should have static assigned addresses - Configured via nmcli or directly in /etc/sysconfig/network-scripts/ifcfg-* Try to avoid running FreeIPA without DNS - while possible, you are creating higher maintenance Trust Information If you are in a mixed environment (both Windows and Linux/UNIX), it is recommended to setup a trust between FreeIPA and Active Directory. Because of this, they will need to be in different domains (eg, example.com and ipa.example.com, or example.com and example.net). This way, you do not have to create duplicate users if a windows user logs into Linux resources nor use winsync. DNS \u00b6 As noted in the previous section, you must try not to hijack a domain. You can migrate records over to FreeIPA's DNS if you'd like, but care must be taken with that approach. While FreeIPA can do the typical DNS server work such as forward/reverse zones and various types of records, it should not be considered a full solution. It does not support views (eg, you can't have internal and external views, assuming you have domains that are publically facing). In the event you need to have views, that's when you need a different DNS server or service to provide this to you. There are two ways you can have DNS entries updated dynamically: --enable-dns-updates for ipa-client-install and DHCP dynamic DNS updates. Both are sufficient. The latter requires additional work and is outside the scope of this write up. Delegation \u00b6 Throughout this guide, you may find or see examples of domain delegation where there is an AD trust, as it would be a more real world example of bringing in FreeIPA to an environment that is already in place, working, with a DNS hosted by AD or by an appliance. Majority of the examples assume both IPA and AD is delegated (when it's normally IPA that's just delegated while AD hosts the actual parent zone). Using this type of setup, it is not required for clients to have entries in the IPA domain. In fact, they can be in other domains as long as they have A/AAAA/PTR records associated with them. This assumes that there could be dynamic dns associated with DHCP or everything is static and lives in the parent zones. The caveat to this is SSO will fail . You can setup already existing DNS servers to delegate an entire domain or a subdomain for FreeIPA. This way, you don't overlap with a domain that's already in use. So for example, if AD owns example.com, you could have AD delegate ipa.example.com or even forward example.net. If AD is not the DNS provider for the environment, you can have the appliance delegate the domain in the same manner. Below is a bind example of what example.com would look like when delegating the IPA domain: $ORIGIN example.com. @ IN SOA ... ( ) NS np-ad01 NS np-ad02 np-ad01 A 10.200.0.232 np-ad02 A 10.200.0.233 ; Many other records here, pertaining to AD, eg msdcs and SRV records ; IPA records $ORIGIN ipa.example.com. @ NS np-ipa01 NS np-ipa02 np-ipa01 A 10.200.0.230 np-ipa02 A 10.200.0.231 Note that AD can send nsupdates to a DNS server if given the permissions. As of this writing, FreeIPA does not do this, which is why DNS delegation is recommended. Server Setup \u00b6 Required Packages \u00b6 ipa-server ipa-client (required as an IPA server is technically a client of the domain) ipa-server-dns (required for using the internal DNS) sssd/sssd-ipa (pulled in as dependencies) Optional Packages \u00b6 ipa-server-trust-ad if using an AD trust Installation \u00b6 To install the server, make sure the hostname is set to the A records and NS delegations you've put in DNS (which won't respond to a DNS lookup). If these are stand-alone, then you can just keep it at the top level (eg, example.com). You'll also need to modify /etc/hosts, set static IP addresses, and then run the ipa-server-install command. % hostnamectl set-hostname server1.ipa.example.com % nmcli con mod ens192 ipv4.address 10.200.0.230/24 % nmcli con mod ens192 ipv4.gateway 10.200.0.1 % nmcli con mod ens192 ipv4.method manual % nmcli con up ens192 % vi /etc/hosts . . . 10.200.0.230 server1.ipa.example.com 10.200.0.231 server2.ipa.example.com # Fedora % dnf install freeipa-server{,-common,-dns,-trust-ad} -y # Enterprise Linux 8 % dnf module enable idm:DL1/{dns,adtrust,client,server,common} % dnf install ipa-server ipa-server-dns ipa-client sssd sssd-ipa -y # Enterprise Linux 9 (there appears to be no modules) % dnf install ipa-server ipa-server-dns ipa-client sssd sssd-ipa -y # Setup # Enterprise 8 / 9 % firewall-cmd --permanent --add-service={freeipa-4,ntp,dns,freeipa-trust} % firewall-cmd --complete-reload % ipa-server-install \\ --no_hbac_allow \\ <-- If you want to have HBAC allow_all disabled initially --no-ntp \\ <-- If you want to host NTP from IPA, take off --no-ntp --setup-dns \\ --realm IPA.EXAMPLE.COM \\ --domain example.com . . . (show steps here) While not officially recommended, you could have two accounts. One for administration of servers and the domain and one for your workstation, similar to separating domain users and domain administrators in active directory. You don't have to follow this, but at least there's a form of separation. % kinit admin % ipa user-add --first=First --last=Last --cn=\"First Last Admin\" --gecos=\"First Last Admin\" flast2 % ipa group-add-member --users=flast2 admins Replica \u00b6 On the replica, ensure you repeat the same steps as above. % hostnamectl set-hostname server2.ipa.example.com % nmcli con mod ens192 ipv4.address 10.200.0.231/24 % nmcli con mod ens192 ipv4.gateway 10.200.0.1 % nmcli con mod ens192 ipv4.method manual % nmcli con up ens192 % vi /etc/hosts . . . 10.200.0.230 server1.ipa.example.com 10.200.0.231 server2.ipa.example.com % dnf install ipa-server ipa-server-dns ipa-client sssd sssd-ipa -y # Enterprise 8 / 9 % firewall-cmd --permanent --add-service={freeipa-4,ntp,dns,freeipa-trust} % firewall-cmd --complete-reload % ipa-replica-install --no-forwarders --setup-ca --setup-dns --no-ntp --principal admin --admin-password \"ChangePass123\" --domain ipa.example.com . . . (show steps) You should now be able to see your replicas. % ipa-replica-manage list server1.ipa.example.com: master server2.ipa.example.com: master Replica Automation \u00b6 It is possible to automate the replica installation. To automate the replica installation, the following requirements would need to be met: Server must be added as a client (ipa-client-install) with an IP address on the commandline Server must be added to the ipaservers host group ipa-replica-install ran without principal and passwords Once you have a server added as a client and then added to the ipaservers host group, you would run a command like this: % ipa-replica-install --ssh-trust-dns --unattended --setup-ca --mkhomedir --setup-dns --no-forwarders If you have forwarders, use the --forwarders option instead. Server Migration/Upgrade \u00b6 Performing a migration is a multi-step process. Typically you are going from one major version of Enterprise Linux (such as 7 or 8) to another (such as 9). Regardless of which version you are migrating from, the typical beginning steps are: System's time is verified for time synchronization like using ntpstat or equivalent Server roles are verified in the current environment using ipa server-role-find --status enabled --server ipa.example.com New system is installed and enrolled as a client New system is added as a replica with required server roles EL7 to EL9 / Two Major Version Jumps When jumping from EL7 to EL9 or two major versions in general, it is recommended that you have an \"in between\" machine. This means that you need to add the in between version first and then you can add the latest version. See this page for an example. The below is in the case of a single master installation and doesn't take into account of multiple version jumps. Let's say you have two old Enterprise Linux replicas instead. There are two approaches you can take: Install a new Enterprise Linux system, add it, reinstall old system to the new version, add it back. Install two new Enterprise Linux systems, add them as needed, power off old systems. Below is an example, with X being the old version, and Y being the new. Enterprise Linux Y system is installed and enrolled as a client Enterprise Linux Y system is added as a replica Change CRL to Enterprise Linux Y system and adjust settings on Enterprise Linux X CA master and new Enterprise Linux Y replica for pki-tomcatd and httpd Test user is created to ensure DNA range is adjusted Verify DNA range Stop first Enterprise Linux X IPA services, remove replica, uninstall, power off. Second Enterprise Linux Y system is installed and enrolled as a client Second Enterprise Linux Y system is added as a replica Test user is created again to ensure DNA range is adjusted Verify DNA range Stop second Enterprise Linux X IPA services, remove replica, uninstall, power off. EL7 to EL8 \u00b6 # Enterprise Linux 8 % dnf module enable idm:DL1 # Install necessary packages, ie AD trust packages if you need them % dnf install ipa-server ipa-server-dns -y % ipa-client-install --realm EXAMPLE.COM --domain example.com % kinit admin # Add other switches that you feel are necessary, such as forwarders, kra, ntp... % ipa-replica-install --setup-dns --setup-ca --ssh-trust-dns --mkhomedir # Verify all services are in a RUNNING state % ipactl status Directory Service: RUNNING . . . % ipa-csreplica-manage list elX.example.com: master elY.example.com: master % ipa-csreplica-manage list --verbose elY.example.com Directory Manager password: elX.example.com last init status: None last init ended: 1970-01-01 00:00:00+00:00 last update status: Error (0) Replica acquired successfully: Incremental update succeeded last update ended: 2019-11-07 22:46:15+00:00 Change CRL to new Enterprise Linux system and adjust settings on both replicas for pki-tomcatd and httpd # Change CA master to elY % ipa config-mod --ca-renewal-master-server elY.example.com # Shut down all CRL generation on ELX elX% ipa-crlgen-manage status CRL generation: enabled . . . elX% ipa-crlgen-manage disable Stopping pki-tomcatd Editing /var/lib/pki/pki-tomcat/conf/ca/CS.cfg Starting pki-tomcatd Editing /etc/httpd/conf.d/ipa-pki-proxy.conf Restarting httpd CRL generation disabled on the local host. Please make sure to configure CRL generation on another master with ipa-crlgen-manage enable. The ipa-crlgen-manage command was successful # Verify that the /etc/httpd/conf.d/ipa-pki-proxy.conf file's RewriteRule is not commented # If it is, remove the comment and restart httpd. ipa-crlgen-manage should take care of this. % tail -n 1 /etc/httpd/conf.d/ipa-pki-proxy.conf RewriteRule ^/ipa/crl/MasterCRL.bin https://elX.example.com/ca/ee/ca/getCRL?op=getCRL&crlIssuingPoint=MasterCRL [L,R=301,NC] # Turn it on with ELY elY% systemctl stop pki-tomcatd@pki-tomcat.service # The values should be changed from false to true elY% vi /etc/pki/pki-tomcat/ca/CS.cfg ca.crl.MasterCRL.enableCRLCache=true ca.crl.MasterCRL.enableCRLUpdates=true elY% systemctl start pki-tomcatd@pki-tomcat.service # Make sure the rewrite rule has a comment on elY elY% vi /etc/httpd/conf.d/ipa-pki-proxy.conf . . . #RewriteRule ^/ipa/crl/MasterCRL.bin https://elY.example.com/ca/ee/ca/getCRL?op=getCRL&crlIssuingPoint=MasterCRL [L,R=301,NC] elY% systemctl restart httpd Test user is created to ensure DNA range is adjusted and replication is working % ipa user-add --first=testing --last=user testinguser1 # Test on both systems elX% ipa user-find testinguser1 elY% ipa user-find testinguser1 Verify DNA range # There should be ranges for both replicas % ipa-replica-manage dnarange-show elX.example.com: ... elY.example.com: ... Stop old Enterprise Linux IPA services, remove replica, uninstall # Stop all elX services elX% ipactl stop # Delete the elX system from the topology elY% ipa server-del elX.example.com # Uninstall and/or power down system elX% ipa-server-install --uninstall elX% init 0 EL8 to EL9 \u00b6 # Enterprise Linux 9 % dnf install ipa-server ipa-server-dns -y % ipa-client-install --realm EXAMPLE.COM --domain example.com % kinit admin # Add other switches that you feel are necessary, such as forwarders, kra, ntp... % ipa-replica-install --setup-dns --setup-ca --ssh-trust-dns --mkhomedir # Verify all services are in a RUNNING state % ipactl status Directory Service: RUNNING . . . % ipa-csreplica-manage list elX.example.com: master elY.example.com: master % ipa-csreplica-manage list --verbose elY.example.com Directory Manager password: elX.example.com last init status: None last init ended: 1970-01-01 00:00:00+00:00 last update status: Error (0) Replica acquired successfully: Incremental update succeeded last update ended: 2022-08-12 18:11:11+00:00 Set the CA renewal master to the new system and change the CRL settings % ipa config-mod --ca-renewal-master-server elY.example.com # Remove the ca.certStatusUpdateInterval entry or set it to 600 (default) on elY elY% vim /etc/pki/pki-tomcat/ca/CS.cfg # Restart the ipa services elY% ipactl restart # Set the value of ca.certStatusUpdateInterval on elX to 0 elX% vim /etc/pki/pki-tomcat/ca/CS.cfg ca.certStatusUpdateInterval=0 elX% ipactl restart elX% ipa-crlgen-manage status CRL generation: enabled . . . elX% ipa-crlgen-manage disable Stopping pki-tomcatd Editing /var/lib/pki/pki-tomcat/conf/ca/CS.cfg Starting pki-tomcatd Editing /etc/httpd/conf.d/ipa-pki-proxy.conf Restarting httpd CRL generation disabled on the local host. Please make sure to configure CRL generation on another master with ipa-crlgen-manage enable. The ipa-crlgen-manage command was successful elX% ipa-crlgen-manage status CRL generation: disabled Create a test user to ensure DNA range is adjusted and replication is working elY% ipa user-add --first=testing --last=user testinguser1 # Test on both systems elX% ipa user-find testinguser1 elY% ipa user-find testinguser1 Verify DNA range. # There should be ranges for both replicas % ipa-replica-manage dnarange-show elX.example.com: ... elY.example.com: ... Stop old Enterprise Linux IPA services, remove replica, uninstall. # Stop all elX services elX% ipactl stop # Delete the elX system from the topology elY% ipa server-del elX.example.com # Uninstall and/or power down system elX% ipa-server-install --uninstall elX% init 0 See this page for more information. Active Directory Trust \u00b6 To initiate a trust with your active directory domain, ensure the following requirements are met. Requirements Package installed: ipa-server-trust-ad DNS: Properly configured that FreeIPA can resolve the AD servers A and SRV records This can either be forwarders to AD, a subdomain that IPA manages, or delegated subdomain from the master DNS servers in your network. This is completely dependent on your infrastructure. DNS: AD forest has sites and SRV records, including priorities, are set correctly When the following requirements are met, you have two choices before continuning. You can either use POSIX or have the id range generated automatically. POSIX vs Non-POSIX If you decide to use POSIX, your AD users are expected to have uidNumber, gidNumber, loginShell, unixHomeDirectory set. Else, you will need to setup ID overrides if you already have that information for current users (assuming this is not a new setup for the environment, ie you already have UID's for people). If you are not planning a migration from pure AD over to IPA with a trust, it is recommended to note that information so you can setup the ID overrides. Afterwards, any new users will get UID/GID's that you will not have to manage yourself. You will need to prep your master(s) for the trust. We will be enabling compat, adding sids, and adding agents so both masters can provide AD information. % ipa-adtrust-install --add-sids --add-agents --enable-compat This will do what we need. If you do not have legacy clients (Enterprise Linux 5, Solaris, HP-UX, AIX, SLES 11.4, FreeBSD, the list goes on), then you do not need to enable compat mode. Though, it could be useful to have it for certain apps or scenarios. You will now need to open the necessary ports. Do this on all masters. Ports TCP: 135, 138, 139, 389, 445, 1024-1300, 3268 UDP: 138, 139, 389, 445 % firewall-cmd --add-service=freeipa-trust --permanent % firewall-cmd --complete-reload Now you can initiate the trust. The admin account you use should be part of the domain admins group or at least have permissions to initiate a trust. The former is path of least resistance. # If you are using POSIX ID, use ipa-ad-trust-posix. % ipa trust-add --type=ad example.com --range-type=ipa-ad-trust --admin adminaccount --password Once the trust is up, verify it. % ipa trust-show example.com Realm name: example.com Domain NetBIOS name: AD Domain Security Identifier: S-X-X-XX-XXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX Trust direction: Trusting forest Trust type: Active Directory domain UPN suffixes: example.com You should be able to test for the users now. % id aduser1@example.com uid=XXXXX(aduser1@example.com) gid=XXXXX(aduser1@example.com) groups=XXXXX(aduser1@example.com) Disable Anonymous Bind \u00b6 In some cases, it is a requirement to disable all anonymous binds. If this is the case, you will need to modify cn=config on each master as it is not replicated. rootdse Some applications do anonymous binds to the directory server to determine its version and it supported controls. While it is possible to disable anonymous binds completely, it is important to know that if you disable the rootdse binds, applications that do anonymous lookups to get server information will fail. % ldapmodify -xZZ -D \"cn=Directory Manager\" -W -h server.ipa.example.com Enter LDAP Password: dn: cn=config changetype: modify replace: nsslapd-allow-anonymous-access nsslapd-allow-anonymous-access: rootdse modifying entry \"cn=config\" Client Setup \u00b6 Enterprise Linux & Fedora \u00b6 Ensure your /etc/resolv.conf (or other dns settings) are set correctly. Ensure your hostname is also set correctly. % dnf install ipa-client -y % ipa-client-install --realm EXAMPLE.COM --domain example.com --mkhomedir Mac Clients \u00b6 MacOS Clients are an interesting workstation to setup as a FreeIPA client. It takes a little bit of fighting and troubleshooting, but it can work with the right settings. Note that as of Catalina, you may not be able to login to your account nor will creating a mobile account function as you would expect. This may have changed in recent macos releases, so YMMV. Other Guides There are a couple of guides out there that you may have found before (if you looked) that help setup IPA for Mac. There's one for much older (I think Lion) and one for Sierra. This section was made mostly for my own reference because I found some things in both of those guides didn't address issues I ran into one way or another and couldn't find any information on. The FreeIPA users mail list didn't have any archives with people having similar issues. If you are interested in the other guides to compare to, you may see them here (recent) and here (older) AD Users AD Users You cannot login as AD users on a Mac when going through FreeIPA. You can, in theory, point to the cn=compat tree and set the attribute mapping to rfc2307. In my tests, I have never been able to get this to work. This section, I am going to assume you are going to be logging in as a user in IPA. If you are in a mixed environment, add your Mac to your AD domain instead. Anonymous Bind There may be cases where if you have disabled anonymous binds in IPA, this setup may not work, even if you do use a bind account. You will need to experiment with this if you plan on using a bind account and plan on or currently have IPA not allowing anonymous binds. Check your system's hostname. You want to make sure it has a hostname defined for it in the domain the mac sits in, even if it's dynamic via DHCP/DNS. % sudo scutil --set HostName mac.example.com Get the IPA certificate. You'll need to double click it after you get it and import it. % cd ~/Desktop && curl -OL http://server1.ipa.example.com/ipa/config/ca.crt % sudo mkdir /etc/ipa % sudo cp ca.crt /etc/ipa/ca.crt % sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /etc/ipa/ca.crt On the IPA server, you will need to create a host and get the keytab. % ipa host-add mac.example.com --macaddress=\"00:00:00:00:00:00\" % ipa-getkeytab -s server1.ipa.example.com -p host/mac.example.com -k /tmp/krb5.keytab You will need to transfer that keytab to your mac. % cd ~ % scp user@server1.ipa.example.com:/tmp/krb5.keytab . % sudo mv krb5.keytab /etc/krb5.keytab % sudo chmod 600 /etc/krb5.keytab % sudo chown root:wheel /etc/krb5.keytab Configure /etc/krb5.conf [domain_realm] .ipa.example.com = IPA.EXAMPLE.COM ipa.example.com = IPA.EXAMPLE.COM [libdefaults] default_realm = IPA.EXAMPLE.COM allow_weak_crypto = yes dns_lookup_realm = true dns_lookup_kdc = true rdns = false ticket_lifetime = 24h forwardable = yes renewable = true [realms] IPA.EXAMPLE.COM = { # You don't need to set these when your DNS is setup correctly, but it doesn't hurt to have a reference. # In my opinion, you shouldn't hardcode these values. You have to have a good reason to. #kdc = tcp/server1.ipa.example.com #kdc = tcp/server2.ipa.example.com #admin_server = tcp/server1.ipa.example.com #admin_server = tcp/server2.ipa.example.com pkinit_anchors = FILE:/etc/ipa/ca.crt } You'll want to do a kinit to verify. If it works, you should be able to go to the FreeIPA webui and check that the host is \"enrolled\" (Identity -> Hosts). % kinit username@IPA.EXAMPLE.COM You need to modify a couple of pam files. I'll explain why they need to be changed. % sudo vi /etc/pam.d/authorization # authorization: auth account # Putting krb5 here twice ensures that you can login via kerberos and also get a keytab # If \"no_ccache\" is here, keytabs will not be available on login auth optional pam_krb5.so use_first_pass use_kcminit default_principal auth sufficient pam_krb5.so use_first_pass default_principal auth required pam_opendirectory.so use_first_pass nullok account required pam_opendirectory.so % sudo vi /etc/pam.d/screensaver # The krb5 changes do similar to the authorization when on the lock screen after a sleep #auth optional pam_krb5.so use_first_pass use_kcminit auth optional pam_krb5.so use_first_pass use_kcminit default_principal auth sufficient pam_krb5.so use_first_pass default_principal auth required pam_opendirectory.so use_first_pass nullok account required pam_opendirectory.so account sufficient pam_self.so account required pam_group.so no_warn group=admin,wheel fail_safe account required pam_group.so no_warn deny group=admin,wheel ruser fail_safe % sudo vi /etc/pam.d/passwd # Helps with kerberos logins password sufficient pam_krb5.so auth required pam_permit.so account required pam_opendirectory.so password required pam_opendirectory.so session required pam_permit.so After these changes, you'll need to go into make some changes with the directory utility. This depends on your macOS version. Monterey and older \u00b6 Go to system preferences -> users & groups -> login options - Click the 'lock' to make changes Set the following: Automatic login: Off Display login window as: Name and Password Show fast user switching menu as: Full Name Click \"Join\" next to \"Network Account Server\" Enter one of your IPA servers (you can duplicate it later for backup purposes) and click Continue. Ensure \"Allow network users to log in at login window\" is checked - Make sure it's set to all users Click \"edit\" next to the \"Network Account Server\" Click \"Open Directory Utility\" Click the lock, edit LDAPv3 Select your server and click \"edit\" Set the following options: Open/close times out in 5 seconds Query times out in 5 seconds Connection idles out in 1 minute (this can't be changed) Encrypt using SSL (selected) Click \"Search & Mappings\" You may either select \"rfc2307\" from the dropdown or select custom. It will ask your base DN (eg, dc=ipa,dc=example,dc=com) If you select rfc2307, it will ask for your base DN (eg, dc=ipa,dc=example,dc=com) If you select \"custom\", you will need to do this manually for each record type. You're better off using rfc2307 and working from there Click the \"+\" to add a groups record type or scroll and find \"groups\". Select \"groups\", and ensure the following object classes exist. You can click the \"+\" to add them when needed. Record Type ObjectClasses Groups posixGroup ipausergroup groupOfNames\\* Note \"groupOfNames\" is optional here, because it seems that the directory utility doesn't understand this concept. Expand \"groups\" and ensure the following for each record type. You can click the \"+\" to add the attribute types as needed. Attribute Mapping PrimaryGroupID gidNumber RecordName cn Click the \"+\" to add a users record type or scroll and find \"users\". Select \"users\" and ensure the following object classes exist. You can click the \"+\" to add them when needed. Record Type ObjectClasses Users inetOrgPerson posixAccount shadowAccount apple-user Expand \"users\" and ensure the following for each record type. You can click the \"+\" to add the attribute types as needed. Do not set homeDirectory otherwise you will fail to login. Attribute Mapping AuthenticationAuthority uid GeneratedUID GeneratedUID or ipaUniqueID HomeDirectory #/Users/\\$uid\\$ NFSHomeDirectory #/Users/\\$uid\\$ PrimaryGroupID gidNumber RealName cn RecordName uid UniqueID uidNumber UserShell loginShell AltSecurityIdentities #Kerberos:\\$krbPrincipalName\\$ If using custom mapping, click reach record type you created and ensure the base DN is set. Make sure each record type is set to all subtrees. Click \"security\" and set an authentication bind DN if needed Click OK Click OK Click on Search Policy. Double check that \"/LDAPV3/server1.ipa.example.com\" is listed beneath \"/Local/Default\" Close everything until you're back to the users & groups section of preferences Open a terminal. % dscacheutil -flushcache % dscacheutil -q user -a name username You should get a return. If you want to further verify users and groups after the above succeeds, open up the directory utility again. Click \"Directory Editor\", ensure you are searching for \"users\" and check that they appear in a list on the right hand side, optionally doing a search. In a default setup, you shouldn't need an account to do (some) anonymous lookups. If you changed that in any way, you will need to create a readonly system account in cn=sysaccounts,cn=etc. Login to the account for the first time from the login screen. Once the setup has complete, log out and back to a login account. In a terminal, you will need to make a mobile account. 2 % sudo /System/Library/CoreServices/ManagedClient.app/Contents/Resources/createmobileaccount -n username -P # Press enter and put in the password. sudo may not function if you don't do this step. # OPTIONAL: Allow the mobile account to be an administrator % sudo dscl . -append /Groups/admin GroupMembership username Go to system preferences, users & groups and ensure the account is a mobile account. Ventura and likely newer \u00b6 Go to system preferences -> users & groups Set \"automatic login\" to \"off\" Click \"edit\" next to \"Network account server\" Type in one of your IPA servers (you can duplicate it later for backup purposes). Press enter and wait for it to be \"green\". Click \"Open Directory Utility\" Click the \"lock\" to unlock the utility Click \"LDAPv3\" and click the pencil at the bottom left corner Select the \"from server\" portion under LDAP mappings and clck RFC2307. You may also leave it as custom. If you select rfc2307, it will ask for your base DN (eg, dc=ipa,dc=example,dc=com) If you select \"custom\", you will need to do this manually for each record type. You're better off using rfc2307 and working from there Click \"edit\" Click the \"+\" to add a groups record type or scroll and find \"groups\" and select it. Add the following object classes Record Type ObjectClasses Groups posixGroup ipausergroup groupOfNames\\* Note \"groupOfNames\" is optional here, because it seems that the directory utility doesn't understand this concept. Expand \"groups\" and ensure the following for each record type. You can click the \"+\" to add the attribute types as needed. Attribute Mapping PrimaryGroupID gidNumber RecordName cn Click the \"+\" to add a users record type or scroll and find \"users\". Select \"users\" and ensure the following object classes exist. You can click the \"+\" to add them when needed. Record Type ObjectClasses Users inetOrgPerson posixAccount shadowAccount apple-user Expand \"users\" and ensure the following for each record type. You can click the \"+\" to add the attribute types as needed. Do not set homeDirectory otherwise you will fail to login. Attribute Mapping AuthenticationAuthority uid GeneratedUID GeneratedUID or ipaUniqueID NFSHomeDirectory #/Users/\\$uid\\$ PrimaryGroupID gidNumber RealName cn RecordName uid UniqueID uidNumber UserShell loginShell AltSecurityIdentities #Kerberos:\\$krbPrincipalName\\$ If using custom mapping, click reach record type you created and ensure the base DN is set. Make sure each record type is set to all subtrees if needed. Click \"security\" and set an authentication bind DN if needed Click OK. Click Search Policy Double check that \"/LDAPV3/server1.ipa.example.com\" is listed beneath \"/Local/Default\". If it is not, select \"search patch\" and set it to custom and add it. Click Apply after. Close everything until you're back to the users & groups section of preferences Go to Lock Screen. Set \"login window shows\" to \"name and password\" Open a terminal. % dscacheutil -flushcache % dscacheutil -q user -a name username You should get a return. Login to the account for the first time from the login screen. Once the setup has complete, log out and back to a login account. In a terminal, you will need to make a mobile account. 3 % sudo /System/Library/CoreServices/ManagedClient.app/Contents/Resources/createmobileaccount -n username -P # Press enter, enter the user's password. sudo may hang if you don't do this. # OPTIONAL: Allow the mobile account to be an administrator % sudo dscl . -append /Groups/admin GroupMembership username Go to system preferences and ensure the account is a mobile account. General macOS Notes \u00b6 Group Resolution If you want groups from IPA to resolve to the system, you'll need to enable the compat tree when using this setup (RFC2307). Password Notes There are a couple of potential issues with this setup that you should be aware of as it pertains to mobile accounts. If you do a mobile account, changing your password through the FreeIPA gui does not change your passwords on your system. If your account does not have any keytabs (eg, you haven't had your mac on or haven't logged in in over 24 hours), you can login with the new password and it will suceed. The system will cache the new password right away. However, your keychain the first time will ask for the old passwords and this is normal. So you can change them by hand or you can log out and back in and the system will ask you if you want to update the password and it will just update automatically. There have been reports in a github issue that states you can change the password in the system preferences but I've been unable to confirm this. Below is a script that can be adapted for you. It has not been tested on Monterey and up. This assumes that you took one mac and set it up properly and you created a tarball with the proper configuration. You could optionally setup a temporary NFS or samba mount that gets mounted as root and then unmounted at the end, if you so wish. #!/bin/bash serverName=server1.ipa.example.com krb5Conf=/etc/krb5.conf krb5Tab=/etc/krb5.keytab pamDirectory=/etc/pam.d # Add SSL cert to chain mkdir /etc/ipa cd /etc/ipa curl -OL http://$serverName/ipa/config/ca.crt security add-trusted-cert -d -k /Library/Keychains/System.keychain -r trustRoot /etc/ipa/ca.crt # Stop and flushout the Open Directory /usr/sbin/dscacheutil -flushcache launchctl unload /System/Library/LaunchDaemons/com.apple.opendirectoryd.plist # Pull the plist and pam files needed for IPA and deploy them, this assumes you setup one mac and zipped up the configurations # You can try your hand at dsconfigldap before pam, but I could never figure it out, honestly. # Relevant tar: tar czf /tmp/macconfig.tar.gz /Library/Preferences/OpenDirectory/Configurations /etc/pam.d/authorization \\ # /etc/pam.d/screensaver /etc/pam.d/passwd /etc/krb5.conf cd /tmp curl -OL http://$serverName/macconfig.tar.gz cd / tar xzf /tmp/macconfig.tar.gz # Add steps here for your keytab! Where are you getting it from? cp /tmp/mac.keytab /etc/krb5.keytab chown root:wheel /etc/krb5.keytab chmod 600 /etc/krb5.keytab # Start directory launchctl load /System/Library/LaunchDaemons/com.apple.opendirectoryd.plist sleep 30 # Kill the loginwindow killall loginwindow # If the system doesn't reboot here, reboot now. If you want to move your local files, you will need to tread lightly here. I personally believe it's always good to start fresh though. Look into the ditto command. I suppose something like this can work: # make sure you're logged in as a different account away from your local account % sudo su - root# cd /Users root# ditto localfolder networkfolder (or maybe an mv?) root# chown -R user:user folder root# /System/Library/CoreServices/ManagedClient.app/Contents/Resources/createmobileaccount -n username -P Another issue you may run into, if you have been using your Mac with a local account for a while, a lot of directories in /Applications will be owned by localuser:staff or localuser:admin. It's recommended to fix those too. Discovery The directory framework in MacOS has the ability to discover settings for a particular LDAP server that it is being connected to. FreeIPA does not contain the schema, plugins, nor the infrastructure to provide the same things (for example, mDNS/Avahi, among other things). There was a (WIP) plugin created in 2017 by abbra. However, it is unclear if this works at all, nor is it clear if it ever did and will in python3 (abbra noted at the time that it \"installs\" into python 2 directories, which hints to not being tested or working on python 3). Please see the following resources for discussion and information. Pagure freeipa-macosx-support SUSE \u00b6 To setup openSUSE with FreeIPA, we'll need to do some manual work. This applies to SUSE 12 and up where the freeipa-client packages don't exist in the main repositories. freeipa repos There are OpenSUSE repos with the freeipa packages, though they are considered \"experimental\". If they show up in the base, then the below steps will be removed. However, if you are willing to use the repo , a lot of the steps below may not be needed. We have not tested this. # On an IPA server or client with the IPA utilities... % ipa host-add suse.example.com % /usr/sbin/ipa-getkeytab -s ipa.example.com -p host/suse.example.com -k /tmp/suse.keytab % scp /tmp/suse.keytab suse.example.com:/tmp/krb5.keytab # On the IPA client... % cp /tmp/krb5.keytab /etc % chmod 600 /etc/krb5.keytab % mkdir /etc/ipa % curl -o /etc/ipa/ca.crt http://ipa.example.com/ipa/config/ca.crt % curl -o /etc/pki/trust/anchors/ipa.example.com.crt http://ipa.example.com/ipa/config/ca.crt % update-ca-certificates % zypper install sssd sssd-ipa yast2-auth-client krb5-client openldap2-client cyrus-sasl-gssapi # Setup SSSD % vi /etc/sssd/sssd.conf [domain/example.com] cache_credentials = True krb5_store_password_if_offline = True ipa_domain = example.com ipa_hostname = suse.example.com # Client Specific Settings ipa_server = _srv_, ipa.example.com dns_discovery_domain = example.com # If we have a trust with domain resolution order #full_name_format = %1$s id_provider = ipa auth_provider = ipa access_provider = ipa chpass_provider = ipa ldap_tls_cacert = /etc/ipa/ca.crt [sssd] services = nss, sudo, pam, ssh domains = example.com [nss] filter_users = root,ldap,named,avahi,haldaemon,dbus,radiusd,news,nscd,tomcat,postgres homedir_substring = /home [pam] [sudo] [autofs] [ssh] # Setup kerberos % vi /etc/krb5.conf [libdefaults] default_realm = EXAMPLE.COM dns_lookup_realm = true dns_lookup_kdc = true rdns = false dns_canonicalize_hostname = false ticket_lifetime = 24h forwardable = true udp_preference_limit = 0 default_ccache_name = KEYRING:persistent:%{uid} [realms] EXAMPLE.COM = { pkinit_anchors = FILE:/var/lib/ipa-client/pki/kdc-ca-bundle.pem pkinit_pool = FILE:/var/lib/ipa-client/pki/ca-bundle.pem } [domain_realm] .example.com = EXAMPLE.COM example.com = EXAMPLE.COM suse.example.com = EXAMPLE.COM # Setup pam % pam-config -a --sss --mkhomedir --mkhomedir-umask=0077 \\ --pwhistory --pwhistory-remember=5 --localuser --cracklib \\ --cracklib-minlen=14 --cracklib-dcredit=-1 --cracklib-ucredit=-1 \\ --cracklib-lcredit=-1 --cracklib-ocredit=-1 --cracklib-retry=3 --unix-sha512 # Setup nsswitch (you can make it compat sss, but I use files sss) % sed -i.bak 's/compat$/files sss/g' /etc/nsswitch.conf % echo \"sudoers: files sss\" >> /etc/nsswitch.conf % sed -i '/netgroup/ s/nis/sss/g' /etc/nsswitch.conf # Depending on your suse version, you may want to set the nisdomainname # It does not hurt to set this % sed -i.bak '/NETCONFIG_NIS_STATIC_DOMAIN/ s/\"\"/\"example.com\"/g' /etc/sysconfig/network/config % netconfig update -f # Start sssd % systemctl enable sssd --now # Verify % id admin In the case of having an IPA-AD trust, you may need to change a line in your pam configuration. % sed -i 's/use_first_pass/forward_pass/g' /etc/pam.d/common-auth-pc # The affected line should appear like the below auth sufficient pam_sss.so forward_pass HBAC \u00b6 When we first setup our IPA servers, we had an option set to make it so hbac wasn't allowed for everyone. This way we have to create HBAC rules for our systems. I personally do this out of habit when working with IPA. What we need to do though is create an \"admin\" group that can login to all machines. % ipa idrange-show IPA.EXAMPLE.COM_id_range Range name: IPA.EXAMPLE.COM_id_range First Posix ID of the range: 686600000 Number of IDs in the range: 200000 First RID of the corresponding RID range: 1000 First RID of the secondary RID range: 100000000 Range type: local domain range % ipa group-add --gid=686610000 linuxadm % ipa group-add-member --users=flast linuxadm Note for AD Users : In the event that your AD user or group of users will be an admin, you need to create an \"external\" group to map the user or users over. This isn't required if you don't have an AD trust. # Create an external group that the AD user/group goes into % ipa group-add --external linuxadm_external # Add the user (or group) into the external group % ipa group-add-member --users=aduser1@example.com linuxadm_external % ipa group-add-member --users=adgroup1@example.com linuxadm_external # Add the external group as a member of the IPA posix group. # aduser1 and adgroup1 are now effectively members of the linuxadm group in IPA. % ipa group-add-member --groups=linuxadm_external linuxadm Now, let's create an HBAC for our Linux Administrator account for our group. % ipa hbacrule-add --hostcat=all --servicecat=all --desc='linux admins all access' linuxadm % ipa hbacrule-add-user --groups=linuxadm linuxadm % ipa hbactest --rules=All_Systems --user=flast --host=server1.ipa.example.com --service=sshd % ipa hbactest --rules=All_Systems --user=aduser1@example.com --host=server1.ipa.example.com --service=sshd You might want to create an HBAC rule specifically for your IPA admin accounts to have ssh access to the IPA servers too. You can follow something like the above to make it possible. Or you can just add the IPA admins group into the HBAC rule we just made above. Group Types Groups in Active Directory have three types. These three types can actually change the behavior of how SSSD on the IPA domain controllers resolve them or if they'll even be resolvable at all. The three types are 'Domain Local', 'Global', and 'Universal'. If at all possible, avoid groups being 'Global'. Domain Local or Universal is recommended. SUDO \u00b6 Setting up sudo is relatively easy. SSSD (1.16.x and 2.X) supports IPA as a provider for sudo. Based on the last section, let's create a sample rule for our Linux admins that can login to every system, we want to ensure they can run all commands. % ipa sudorule-add --runasusercat=all --hostcat=all --cmdcat=all --desc='linux admins all sudo' all_linux_sudo % ipa sudorule-add-user --groups=linuxadm all_linux_sudo You can make this a little more specific, such as /bin/bash as everyone or otherwise. It's your call here. If you want to create a sudo rule and add some commands to it, you can do something like this. % ipa sudorule-add sudo_rule % ipa sudorule-add-allow-command --sudocmds=\"/usr/bin/less\" sudo_rule Legacy Client Setup \u00b6 This applies to Solaris, Omnios, others based on Illumos. Solaris 10 \u00b6 Setting up Solaris 10 as an IPA client is an interesting feat. However, it comes with security issues. No SSL or TLS Support Note that for Solaris 10 to talk to IPA, you must use clear text communication. Solaris 10 is too old to use new ciphers. However, while LDAP may be clear text, kerberos should still be secure enough for the time being. If you are using an AD trust, the user's passwords will be passed in clear text. Highly suggested that you decommission Solaris 10 from your environment. Solaris 10 will eventually be removed from this page. Create an ldif for your service account (optional) dn: uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com objectclass: account objectclass: simplesecurityobject uid: solaris userPassword: secret123 passwordExpirationTime: 20380119031407Z nsIdleTimeout: 0 The solaris system account is required. So now, add it in. % ldapadd -xWD 'cn=Directory Manager' -f /tmp/solaris.ldif Now, set the nisdomain. % defaultdomain ipa.example.com % echo 'ipa.example.com' > /etc/defaultdomain Configure kerberos. % vi /etc/krb5/krb5.conf [libdefaults] default_realm = IPA.EXAMPLE.COM dns_lookup_kdc = true verify_ap_req_nofail = false [realms] IPA.EXAMPLE.COM = { } [domain_realm] ipa.example.com = IPA.EXAMPLE.COM .ipa.example.com = IPA.EXAMPLE.COM [logging] default = FILE:/var/krb5/kdc.log kdc = FILE:/var/krb5/kdc.log kdc_rotate = { period = 1d version = 10 } [appdefaults] kinit = { renewable = true forwardable= true } Generate a keytab and bring it over. # on the ipa server % ipa host-add solaris10.example.com % ipa-getkeytab -s server1.ipa.example.com -p host/solaris10.example.com -k /tmp/solaris10.keytab # Transfer the keytab % scp /tmp/solaris10.keytab solaris10.example.com:/tmp # On the solaris 10 machine % cp /tmp/solaris10.keytab /etc/krb5/krb5.keytab % chmod 600 /etc/krb5/krb5.keytab % chmod 644 /etc/krb5/krb5.conf % chown root:sys /etc/krb5/* % kinit flast2@IPA.EXAMPLE.COM Create the LDAP configurations, bring the certificate, and create an NSS database. % mkdir /etc/ipa /var/ldap % cd /etc/ipa % wget -O ipa.pem http://server1.ipa.example.com/ipa/config/ca.crt % certutil -A -n \"ca-cert\" -i /etc/ipa/ipa.pem -a -t CT -d . % cp * /var/ldap % vi /etc/ldap.conf base dc=ipa,dc=example,dc=com scope sub TLS_CACERTDIR /var/ldap TLS_CERT /var/ldap/cert8.db TLS_CACERT /var/ldap/ipa.pem tls_checkpeer no ssl off bind_timelimit 120 timelimit 120 uri ldap://server1.ipa.example.com sudoers_base ou=sudoers,dc=ipa,dc=example,dc=com pam_lookup_policy yes Now init the ldap client. No Secure Connection When using this, you are not creating a secure connection. The Solaris 10 SSL libraries are so old that they cannot work with the ciphers that FreeIPA has turned on. AD Trust - Different Trees If using an AD trust, you should use the second example, where it looks at the compat tree for users. No Service Account If you have configured FreeIPA to not allow any anonymous connections, you will need to use a proxy account. We have provided the examples for this configuration. Without an AD Trust # Without AD Trust (no proxy) % ldapclient manual -a authenticationMethod=none \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com \\ -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 # Without AD Trust (proxy) % ldapclient manual -a credentialLevel=proxy \\ -a authenticationMethod=simple \\ -a proxyDN=\"uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com\" \\ -a proxyPassword=\"secret123\" \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com \\ -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 With an AD Trust # With AD Trust (no proxy) % ldapclient manual -a authenticationMethod=none \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com \\ -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 # With AD Trust (proxy) % ldapclient manual -a credentialLevel=proxy \\ -a authenticationMethod=simple \\ -a proxyDN=\"uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com\" \\ -a proxyPassword=\"secret123\" \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com \\ -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 This should succeed. Once it succeeds, you need to configure pam and nsswitch. AD Trust Information In the event you don't have an AD trust, you can change the \"binding\" lines to required, remove the pam_ldap lines, and change pam_krb5 lines to read \"required\" % vi /etc/pam.conf # Console login auth requisite pam_authtok_get.so.1 login auth sufficient pam_krb5.so.1 login auth required pam_unix_cred.so.1 login auth required pam_dial_auth.so.1 login auth sufficient pam_unix_auth.so.1 server_policy login auth sufficient pam_ldap.so.1 rlogin auth sufficient pam_rhosts_auth.so.1 rlogin auth requisite pam_authtok_get.so.1 rlogin auth required pam_dhkeys.so.1 rlogin auth sufficient pam_krb5.so.1 rlogin auth required pam_unix_cred.so.1 rlogin auth sufficient pam_unix_auth.so.1 server_policy rlogin auth sufficient pam_ldap.so.1 # Needed for krb krlogin auth required pam_unix_cred.so.1 krlogin auth sufficient pam_krb5.so.1 # Needed for krb krsh auth required pam_unix_cred.so.1 krsh auth required pam_krb5.so.1 # ? ppp auth requisite pam_authtok_get.so.1 ppp auth required pam_dhkeys.so.1 ppp auth sufficient pam_krb5.so.1 ppp auth required pam_dial_auth.so.1 ppp auth binding pam_unix_auth.so.1 server_policy ppp auth sufficient pam_ldap.so.1 # Other, used by sshd and \"others\" as a fallback other auth requisite pam_authtok_get.so.1 other auth required pam_dhkeys.so.1 other auth sufficient pam_krb5.so.1 other auth required pam_unix_cred.so.1 other auth sufficient pam_unix_auth.so.1 server_policy other auth sufficient pam_ldap.so.1 other account requisite pam_roles.so.1 other account required pam_projects.so.1 other account binding pam_unix_account.so.1 server_policy other account sufficient pam_krb5.so.1 other account sufficient pam_ldap.so.1 other session required pam_unix_session.so.1 other password required pam_dhkeys.so.1 other password requisite pam_authtok_get.so.1 other password requisite pam_authtok_check.so.1 force_check other password required pam_authtok_store.so.1 server_policy # passwd and cron passwd auth binding pam_passwd_auth.so.1 server_policy passwd auth sufficient pam_ldap.so.1 cron account required pam_unix_account.so.1 # SSH Pubkey - Needed for openldap and still probably needed sshd-pubkey account required pam_unix_account.so.1 % vi /etc/nsswitch.conf # Below are just the minimum changes passwd: files ldap [NOTFOUND=return] group: files ldap [NOTFOUND=return] sudoers: files ldap netgroup: ldap # the rest here are just here, up to you if you choose to set them. hosts: files dns ipnodes: files dns ethers: files ldap publickey: files ldap automount: files ldap You can test now if you'd like. bash-3.2# ldaplist -l passwd flast2 dn: uid=flast2,cn=users,cn=compat,dc=ipa,dc=example,dc=com cn: First Last objectClass: posixAccount objectClass: ipaOverrideTarget objectClass: top gidNumber: 1006800001 gecos: First Last uidNumber: 1006800001 ipaAnchorUUID: :IPA:ipa.example.com:8babb9a8-5aaf-11e7-9769-00505690319e loginShell: /bin/bash homeDirectory: /home/first.last2 uid: first.last2 I recommend setting up sudo at least... if you want to use sudo, install the sudo-ldap from sudo.ws for Solaris 10. Solaris 11 \u00b6 Solaris 11 shares similar configuration to Solaris 10. There are a couple of manual things we have to do, but they are trivial. Solaris 11/Omnios will use TLS and sudo should just work. AD Groups In Solaris 10, users who logged in with AD users (with their short name) would appear as their full name ( name@domain ). This allowed their groups to fully resolve. However, in Solaris 11.4, this was not the case. Short name logins will work but your groups will not resolve as the compat tree uses the full name. To avoid running into this problem, you should be on at least SRU 11.4.7.4.0. Note that on a later SRU, you may need to setup an ID view (without overrides) for groups and sudo to work again. Below is for the service account like in the previous section, here as a reference. dn: uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com objectclass: account objectclass: simplesecurityobject uid: solaris userPassword: secret123 passwordExpirationTime: 20380119031407Z nsIdleTimeout: 0 % ldapadd -xWD 'cn=Directory Manager' -f /tmp/solaris.ldif Now, set the nisdomain. % defaultdomain ipa.example.com % echo 'ipa.example.com' > /etc/defaultdomain Configure kerberos. % vi /etc/krb5/krb5.conf [libdefaults] default_realm = IPA.EXAMPLE.COM dns_lookup_kdc = true verify_ap_req_nofail = false [realms] IPA.EXAMPLE.COM = { } [domain_realm] ipa.example.com = IPA.EXAMPLE.COM .ipa.example.com = IPA.EXAMPLE.COM [logging] default = FILE:/var/krb5/kdc.log kdc = FILE:/var/krb5/kdc.log kdc_rotate = { period = 1d version = 10 } [appdefaults] kinit = { renewable = true forwardable= true } Generate a keytab and bring it over. # on the ipa server % ipa host-add solaris11.example.com % ipa-getkeytab -s server1.ipa.example.com -p host/solaris11.example.com -k /tmp/solaris11.keytab # Transfer the keytab % scp /tmp/solaris11.keytab solaris11.example.com:/tmp # On the solaris 11 machine % cp /tmp/solaris11.keytab /etc/krb5/krb5.keytab % chmod 600 /etc/krb5/krb5.keytab % chmod 644 /etc/krb5/krb5.conf % chown root:sys /etc/krb5/* # Check the keytab % klist -ket /etc/krb5/krb5.keytab # Test that you can kinit % kinit flast2@IPA.EXAMPLE.COM Create the LDAP configurations, bring the certificate, and create an NSS database. Solaris 11.3 vs 11.4 Previously we had 11.3 and 11.4 configurations. We have removed 11.3 as we no longer support it. % mkdir /etc/ipa /var/ldap % cd /etc/ipa % wget -O ipa.pem http://server1.ipa.example.com/ipa/config/ca.crt % cp * /var/ldap % vi /etc/ldap.conf base dc=ipa,dc=example,dc=com scope sub bind_timelimit 120 timelimit 120 uri ldap://server1.ipa.example.com sudoers_base ou=sudoers,dc=ipa,dc=example,dc=com pam_lookup_policy yes TLS_CACERTDIR /var/ldap ssl start_tls tls_checkpeer no Now init the ldap client. We actually get to use a secure connection here. Kerberos is hit or miss, could never get sasl/GSSAPI to work. Different Trees - Trust or not? There are multiple examples of how to setup the trees. If using an AD trust, you should use the second example, where it looks at the compat tree for users. However, if you do not have trusts, then it is perfectly possible to still use the AD Trust example. Try both and see which works better for your environment. No Service Account If you have configured FreeIPA to not allow any anonymous connections, you will need to use a proxy account. We have provided the examples for this configuration. Without AD Trust # Without AD Trust (no proxy) % ldapclient manual -a authenticationMethod=tls:simple \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 # Without AD Trust (proxy) % ldapclient manual -a authenticationMethod=tls:simple \\ -a credentialLevel=proxy \\ -a proxyDN=\"uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com\" \\ -a proxyPassword=\"secret123\" \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com \\ -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 # Without AD Trust (Kerberos) - Only works if Solaris is in the same DNS domain as IPA % ldapclient manual -a authenticationMethod=sasl/GSSAPI \\ -a credentialLevel=self \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com \\ -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 With AD Trust # With AD Trust (no proxy) % ldapclient manual -a authenticationMethod=tls:simple \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 # With AD Trust (proxy) % ldapclient manual -a authenticationMethod=tls:simple \\ -a credentialLevel=proxy \\ -a proxyDN=\"uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com\" \\ -a proxyPassword=\"secret123\" \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com \\ -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 # With AD Trust (Kerberos) - Only works if Solaris is in the same DNS domain as IPA % ldapclient manual -a authenticationMethod=sasl/GSSAPI \\ -a credentialLevel=self \\ -a proxyDN=\"uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com\" \\ -a proxyPassword=\"secret123\" \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com \\ -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 This should succeed. Once it succeeds, you need to configure pam and nsswitch. % /usr/sbin/svccfg -s name-service/switch setprop config/sudoer = astring: \"files ldap\" % /usr/sbin/svccfg -s name-service/switch setprop config/password = astring: \"files ldap [NOTFOUND=return]\" % /usr/sbin/svccfg -s name-service/switch setprop config/group = astring: \"files ldap [NOTFOUND=return]\" % /usr/sbin/svcadm refresh svc:/system/name-service/switch % /usr/sbin/svcadm restart svc:/system/name-service/switch % /usr/sbin/svcadm restart ldap/client AD Trust Information In the event you don't have an AD trust, you can change the \"binding\" lines to required and remove the pam_ldap lines. Optionally, you can set pam_krb5 to \"required\", however sufficient should work just fine. Without an AD Trust % vi /etc/pam.d/login auth definitive pam_user_policy.so.1 auth requisite pam_authtok_get.so.1 auth required pam_dhkeys.so.1 auth sufficient pam_krb5.so.1 auth required pam_unix_cred.so.1 auth sufficient pam_unix_auth.so.1 server_policy % vi /etc/pam.d/other auth definitive pam_user_policy.so.1 auth requisite pam_authtok_get.so.1 auth required pam_dhkeys.so.1 auth sufficient pam_krb5.so.1 auth required pam_unix_cred.so.1 auth sufficient pam_unix_auth.so.1 server_policy account requisite pam_roles.so.1 account definitive pam_user_policy.so.1 account required pam_unix_account.so.1 server_policy account sufficient pam_krb5.so.1 session definitive pam_user_policy.so.1 session required pam_unix_session.so.1 password definitive pam_user_policy.so.1 password include pam_authtok_common password sufficient pam_krb5.so.1 password required pam_authtok_store.so.1 server_policy % vi /etc/pam.d/sshd-pubkey account required pam_unix_account.so.1 With an AD Trust % vi /etc/pam.d/login auth definitive pam_user_policy.so.1 auth requisite pam_authtok_get.so.1 auth required pam_dhkeys.so.1 auth sufficient pam_krb5.so.1 auth required pam_unix_cred.so.1 auth sufficient pam_unix_auth.so.1 server_policy auth sufficient pam_ldap.so.1 % vi /etc/pam.d/other auth definitive pam_user_policy.so.1 auth requisite pam_authtok_get.so.1 auth required pam_dhkeys.so.1 auth sufficient pam_krb5.so.1 auth required pam_unix_cred.so.1 auth sufficient pam_unix_auth.so.1 server_policy auth sufficient pam_ldap.so.1 account requisite pam_roles.so.1 account definitive pam_user_policy.so.1 account binding pam_unix_account.so.1 server_policy account sufficient pam_krb5.so.1 account sufficient pam_ldap.so.1 session definitive pam_user_policy.so.1 session required pam_unix_session.so.1 password definitive pam_user_policy.so.1 password include pam_authtok_common password sufficient pam_krb5.so.1 password required pam_authtok_store.so.1 server_policy % vi /etc/pam.d/sshd-pubkey account required pam_unix_account.so.1 You can test now if you'd like. root@solaris11:~# ldaplist -l passwd flast2 dn: uid=flast2,cn=users,cn=compat,dc=ipa,dc=example,dc=com cn: First Last objectClass: posixAccount objectClass: ipaOverrideTarget objectClass: top gidNumber: 1006800001 gecos: First Last uidNumber: 1006800001 ipaAnchorUUID: :IPA:ipa.example.com:8babb9a8-5aaf-11e7-9769-00505690319e loginShell: /bin/bash homeDirectory: /home/first.last2 uid: first.last2 Automated Scripts \u00b6 I at one point built a bunch of scripts to automate Solaris servers talking to IPA here . However, it is likely the scripts no longer work or contain outdated information. AD Trust Double UID \u00b6 Solaris 11 once in a while gets random regressions when it comes to authentication and ID's, among many other things they randomly decide to break. Big shout out to Oracle. In a brief discussion with a user in the #freeipa IRC channel, the user was trying to find a way to chop off the domain name for logins but also have sudo still work as there were some random issues in general. We both discovered that in SRU 11.4.20.4.0, even though both UID's are present from ldaplist -l passwd, sudo was no longer working properly. The first thing we tried was to create an ID view and override a user with a new username. This successfully removed the domain, but did not solve the sudo problem. He instead got \"no account present for that user\". However, I wasn't able to replicate this. However, later, one thing he noticed is after creating an ID view with no overrides and pointing Solaris 11 to the view in the compat tree, Solaris 10-esque authentication ID reporting started to occur. Running ldaplist -l passwd user reported back the double UID as expected, but the FQDN comes first which resolved his group/sudo issues. # Create a view... no id overrides required here % ipa idview-add solaris # On Solaris... # Take EXTREME care with the group and passwd base DN's, they need to point # to the view properly # This example uses kerberos to authenticate. % ldapclient manual -a authenticationMethod=self \\ -a credentialLevel=sasl/GSSAPI \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com \\ -a defaultServerList=\"server1.angelsofclockwork.net server2.angelsofclockwork.net\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=solaris,cn=views,cn=compat,dc=angelsofclockwork,dc=net \\ -a serviceSearchDescriptor=passwd:cn=users,cn=solaris,cn=views,cn=compat,dc=angelsofclockwork,dc=net \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 # Make sure you set your props... % /usr/sbin/svccfg -s name-service/switch setprop config/sudoer = astring: \"files ldap\" % /usr/sbin/svccfg -s name-service/switch setprop config/password = astring: \"files ldap [NOTFOUND=return]\" % /usr/sbin/svccfg -s name-service/switch setprop config/group = astring: \"files ldap [NOTFOUND=return]\" % /usr/sbin/svcadm refresh svc:/system/name-service/switch % /usr/sbin/svcadm restart svc:/system/name-service/switch % /usr/sbin/svcadm restart ldap/client # Verify... % ldaplist -l passwd adusername . . . % id -a adusername . . . Thank you to \"mewho\" on libera for finding this interesting workaround. OmniOS/Illumos \u00b6 Some steps between Solaris 10 and 11 can be followed to make OmniOS work. However, we have been unable to resolve why sudo will not work when using an AD trust. If you are using a standalone FreeIPA and no trust, sudo should work just fine. Legacy HBAC \u00b6 For HBAC to work on Solaris, you will need to compile the pam_hbac module found here . I would clone the current master branch or download the master.zip to your Solaris system. Each OS has their set of instructions for compiling. First, create the following system account. We will need this when we are configuring our legacy clients. dn: uid=hbac,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com objectClass: account objectClass: simplesecurityobject objectClass: top uid: hbac userPassword: password Solaris 10 \u00b6 % /opt/csw/bin/pkgutil -i -y libnet ar binutils gcc4g++ glib2 libglib2_dev gmake % /opt/csw/bin/pkgutil -i -y libnet ar binutils gcc4g++ glib2 libglib2_dev gmake % PATH=$PATH:/opt/csw/bin % export M4=/opt/csw/bin/gm4 % autoconf -o configure % autoreconf -i # Yes, SSL must be disabled for Solaris 10 to work. The libraries are too old. # You may or may not need to set CFLAGS, CXXFLAGS, and LDFLAGS with -m32 % ./configure AR=/opt/csw/bin/gar --with-pammoddir=/usr/lib/security --sysconfdir=/etc/ --disable-ssl --disable-man-pages % make % make install Solaris 11 \u00b6 % pkg install autoconf libtool pkg-config automake gcc docbook % autoreconf -if % ./configure --with-pammoddir=/usr/lib/security --mandir=/usr/share/man --sysconfdir=/etc/ % make % make install Omnios \u00b6 % pkg install developer/build/autoconf developer/build/libtool \\ developer/pkg-config developer/build/automake \\ developer/gcc48 system/header developer/object-file \\ developer/linker % autoreconf -if % ./configure --with-pammoddir=/usr/lib/security --mandir=/usr/share/man --sysconfdir=/etc/ % make % make install pam_hbac.conf \u00b6 % vim /etc/pam_hbac.conf # Replace client with your server's FQDN URI = ldap://server.ipa.example.com BASE = dc=ipa,dc=example,dc=com BIND_DN = uid=hbac,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com BIND_PW = password SSL_PATH = /var/ldap HOST_NAME = client PAM Configuration \u00b6 # Solaris 10 - /etc/pam.conf # Modify the other account section... It should come at the end of the account blocks. . . . other account required pam_hbac.so ignore_unknown_user ignore_authinfo_unavail # Solaris 11 - /etc/pam.d/other # Same here, only modify the account section . . . account required pam_hbac.so ignore_unknown_user ignore_authinfo_unavail In the event you cannot login or things aren't working the way you'd expect, add 'debug' to the end of the pam_hbac line and watch /var/log/authlog for errors. Login with AD Users to Legacy Clients \u00b6 For AD users to be able to login to legacy clients, you have to enable system-auth to the IPA servers. Without it, users will be denied access, regardless of HBAC controls or if you're using the pam_hbac module. % ipa hbacsvc-add system-auth % ipa hbacrule-add legacy_client_auth % ipa hbacrule-add-host --hostgroups=ipaservers legacy_client_auth % ipa hbacrule-mod --usercat=all legacy_client_auth Legacy Active Directory Trust Notes \u00b6 Just a section of notes. Domain Resolution Order Oddness \u00b6 If using domain resolution order, AD users get double uid attributes - but only if they login with their shortname. If they login with fqdn, double uid's do not occur. But shortnames do not work anymore. Have to restart the directory server to make short names work again. Solaris Weirdness \u00b6 If using domain resolution order, Solaris 10 gets the group resolution correct for short named AD users. Solaris 11 does not unless you are on SRU 11.4.7.4.0 or newer. There is a way to chop off the domain name from the uid using views. Domain Options \u00b6 This section goes over \"situational\" scenarios. These scenarios are reflective of the environment in which IPA is installed and not all will fit into your environment. These are more or less common situations that could occur during an IPA deployment or even post-deployment. Remove @realm for AD users \u00b6 A common scenario is that IPA and AD will have a trust, but there will not be any IPA users with the exception of the engineering team for managing IPA itself. The common theme is that because of this, the engineers and customers would rather not login with username@realm . Info The following is only applicable in an IPA-AD trust. An IPA-only scenario would not require any of these steps and most pieces would work natively (no @realm, sudo, hbac). In the event that you are in an IPA-AD scenario, please take note that this can adversely affect legacy clients. This will cause ldapsearches that are done in the compat tree to display multiple uid attributes. In most cases, this is fine and the user can still login without the realm name. The whoami and id commands will show the domain. There's no workaround for this. On the IPA servers, you will need to set the domain resolution order. This was introduced in 4.5.0. % kinit admin % ipa config-mod --domain-resolution-order=\"example.com:ipa.example.com\" After, you will need to clear out your SSSD cache. # sss_cache -E is insufficient for this. % systemctl stop sssd % rm -rf /var/lib/sss/db/* % systemctl start sssd The below is optional. It will remove the @realm off the usernames, like on the prompt or id or whoami commands. Only do this if required. Only do this on the clients. Do not make this change on an IPA replica. # vi /etc/sssd/sssd.conf [domain/ipa.example.com] . . . full_name_format = %1$s This will ensure EL7, EL8, EL9 clients resolve the AD domain first when attempting logins and optionally drop the @realm off the usernames. AD and IPA group names with short names \u00b6 You may notice that your clients have intermittent issues with name resolution when the following are true: Groups (or users) have the same names in both IPA and AD You are using domain resolution order You are shortening names on the clients You may want to actually search for them to identify the errant groups and then correct them. You can correct them either on the AD or IPA side. I would opt for the IPA side. % kinit admin@IPA.EXAMPLE.COM % vi /tmp/dupecheck.sh #!/bin/bash for x in ${ARRAY[*]} ; do ldapsearch -x -b \"DC=example,DC=com\" -h example.com -LLL -w 'PASSWORD' -D 'username@example.com' samaccountname=\"$x\" samaccountname | grep -q $x if [[ $? -eq 0 ]]; then echo \"$x: DUPLICATE\" fi done % bash /tmp/dupecheck.sh If you run into any duplicates, they should show up in a list for you address. sAMAccountName vs CN The \"CN\" and \"sAMAccountName\" attributes are not the same in AD, depending on who made the group or other factors. The sAMAccountName attribute is the value used to determine names from AD, whether you are enrolled with AD or the IPA server SSSD is pulling the information. This is why we are searching for that attribute, and not the CN. Sites and AD DC's \u00b6 By creating a subdomain section in /etc/sssd/sssd.conf on an IPA server, it is possible to set an AD Site or AD server(s) directly in SSSD. By default, sssd tries to do location based discovery. There may be a case where this isn't possible (eg, only a set of AD servers may only be contacted in certain \"air gapped\" networks). [domain/ipa.example.com/example.com] # If you want a site ad_site = Site_Name # If you want a server(s) ad_server = dc1.example.com, dc2.example.com # A backup? ad_backup_server = dc3.example.com, dc4.example.com If you don't have access or a way to find the sites using the Windows tools, you can run an ldapsearch to find it (or an equivalent ldap browsing tool). % ldapsearch -x -h example.com -s one -WD 'CN=username,CN=Users,DC=example,DC=com' \\ -b 'CN=Sites,CN=Configuration,DC=example,DC=com' cn This should report back your sites. If you want to know the servers for those sites (in case you don't want to deal with the sites, but just the DC's themselves), you use ldapsearch but use the base DN of the site name. % ldapsearch -x -h example.com -WD 'CN=username,CN=Users,DC=example,DC=com' \\ -b 'CN=Servers,CN=Site_Name,CN=Sites,CN=Configuration,DC=example,DC=com' dnsHostName Hardcoded DC's If the DC's change at any time and they are harded in your sssd.conf, it is up to you to know when new controllers are being added or removed as to not disrupt the connectivity from IPA to AD when performing user or group lookups. Enterprise Linux 6 SUDO and Default Domain Suffix \u00b6 This issue with the above section is that once you do this, sudo rules will begin failing, they will no longer work for Enterprise Linux 6. This is because sssd was changed to look for cn=sudo rather than ou=sudoers. To enable the compatibility fall back, you will need to install a newer SSSD. Set Default Shell for AD Users \u00b6 By default, after a trust has been established, the shell all AD users get is /bin/sh. To change this, you must change the sssd.conf on the IPA masters. % vi /etc/sssd/sssd.conf [domain/ipa.example.com] . . . default_shell = /bin/bash % systemctl restart sssd Automated Kerberos Principals \u00b6 Once in a great while, we run into situations where we need to have an automated process for creating principals and keytabs. This section takes a look at some of those examples that we've ran into. Hadoop/Cloudera \u00b6 This assumes you are using Cloudera Manager and not Ambari in any form. DNS Information It is highly likely that if you are using AWS, your nodes are getting stupid names like compute.internal. While there is a a way to change this if you don't change it, you will need to rely on something like DNSMASQ to allow the nodes to communicate with FreeIPA. FreeIPA will be upset about the stupid names because it can't do a rDNS lookup. Cloudera Manager Woes \u00b6 It is likely you have Cloudera/Hadoop, it is also very likely you (or another team) are deploying and using Cloudera Manager (or Director?). You may be running into issues that involve direct Active Directory integration. Maybe you're moving away from a standalone LDAP system over to Active Directory or even FreeIPA. Maybe you have FreeIPA in an AD trust but the users or contractors absolutely insist on using AD against their better judgement, despite the problems they're running into. Whatever the scenario is, we feel your pain. Here are some things you should probably know: Cloudera Manager (or Director?) supports Active Directory out of the box and obviously not FreeIPA despite the devs wanting to work something out back in 2015 Ambari has support for FreeIPA, but we are focusing on Cloudera Manager here. Cloudera Manager supports custom keytab retrieval scripts Hostnames that are longer than 15 characters, regardless of the cloud provider or onprem setup, will ultimately fail The NETBIOS limit in AD is 16 characters, which is 15 + $ at the end - This means hosts will enroll on top of themselves and your cluster will be broken FreeIPA does not have the name limitation and using an AD trust, AD users can freely use Hadoop when the cluster is properly setup. Enrolling the cluster nodes into FreeIPA and using a custom retrieval script will solve most (if not all) of the issues you may run into as well when it comes to keytabs, which Hadoop heavily relies on. The custom script is simply because Cloudera by default likes having direct access to the kerberos infrastructure, which is a no-go for FreeIPA. The Solution \u00b6 To summarize, here is our proposed solution: Create an account called cdh Create a role called \"Kerberos Managers\" and apply the following privileges: System: Manage Host Keytab System: Manage Host Keytab Permissions System: Manage Service Keytab System: Manage Service Keytab Permissions System: Manage User Principals (was not actually used, but who knows what we could use the role for later) Apply the role to the cdh account Create a custom script they could use to enroll the servers into FreeIPA (out of scope here) Create a custom script that utilizes the cdh account to create services So let's create the necessary things we need. # Create the account # Note... you may want to make this account non-expiring since it's just a service account % ipa user-add --first=\"Cloudera\" --last=\"Key Manager\" cdh # Create the Kerberos Managers role % ipa role-add \"Kerberos Managers\" # Create the kerberos manager privilege % ipa privilege-add \"Privileges - Kerberos Managers\" % ipa privilege-add-permission \"Privileges - Kerberos Managers\" \\ --privileges=\"System: Manage Host Keytab\" \\ --privileges=\"System: Manage Host Keytab Permissions\" \\ --privileges=\"System: Manage Service Keytab\" \\ --privileges=\"System: Manage Service Keytab Permissions\" \\ --privileges=\"System: Manage User Principals\" # Add the privilege to the role % ipa role-add-privilege \"Kerberos Managers\" \\ --privileges=\"Privileges - Kerberos Managers\" # Add the user to the role % ipa role-add-member --users=cdh \"Kerberos Managers\" # Optionally, we can export the keytab for the user with a password # You will see why in the next script % ipa-getkeytab -p cdh@EXAMPLE.COM -k cdh.keytab -P Now we need our special kerberos keytab retrieval script. #!/bin/bash # Created by: @nazunalika - Louis Abel # Purpose: To retrieve keytabs for Cloudera / Hadoop # https://github.com/nazunalika/useful-scripts # Disclaimer: We do not take responsibilities for breaches or misconfigurations of # software. Use at your own risk # Variables # This can be anywhere, but it SHOULD be secure with at least 600 permissions CDHKT=\"/root/.cdh/cdh.keytab\" CDHUSER=\"cdh\" IPAREALM=\"EXAMPLE.COM\" # This can be any server. You could make an array and have it randomly selected IPASERVER=\"ipa01.example.com\" # Where is this going? DESTINATION=\"$1\" # The full principal for the keytab in question FULLPRINC=\"$2\" # Shortened name PRINC=$(echo $FULLPRINC | sed \"s/\\@$(echo $IPAREALM)//\") 00_kinitUser() { # Pick what suits you best, we prefer using a keytab # Password based kinit, based on the keytab we created prior! # You could also have this in a file somewhere, I guess. Just # has to be secured. echo ThisIsAWeakPassword | kinit $CDHUSER@$IPAREALM # Keytab based kinit, obviously we created it before right? It just needs to be # on the right system, deployed in some secure manner #kinit -kt $CDHKT $CDHUSER@$IPAREALM if [[ $? == \"1\" ]]; then echo FAILED TO KINIT exit fi } 01_createPrinc() { echo \"INFO: Checking for existing principle\" if ipa service-find $FULLPRINC; then echo \"INFO: Principle found\" else echo \"INFO: Not found, creating\" ipa service-add $FULLPRINC fi } 02_createServiceAllows() { # We need to allow the service to create and retrieve keytabs echo \"INFO: Ensuring service allows to create and retrieve keytabs\" ipa service-allow-create-keytab --users=$CDHUSER $FULLPRINC ipa service-allow-retrieve-keytab --users=$CDHUSER $FULLPRINC # Let's retrieve the keytabs if ipa service-show $FULLPRINC | grep 'Keytab' | grep 'False'; then echo \"INFO: Creating keytab for $FULLPRINC to $DESTINATION\" ipa-getkeytab -s $IPASERVER -p $PRINC -k $DESTINATION else echo \"INFO: Retriving keytab for $FULLPRINC to $DESTINATION\" ipa-getkeytab -r -s $IPASERVER -p $PRINC -k $DESTINATION fi } 00_kinitUser 01_createPrinc 02_createServiceAllows kdestroy exit 0 Place the above script in a file that is accessible by the cloudera manager such as /usr/local/bin/getKeytabsCDH.sh and ensure it is owned by cloudera-scm with a permission set of 775. During the kerberos wizard, stop when you are verifying the \"cdh\" user. You will need to set the configuration for \"Custom Kerberos Keytab Retrieval Script\" to /usr/local/bin/getKeytabsCDH.sh and then you're almost there. 4 An important tidbit is currently Enterprise Linux 7+ and higher use memory based keytabs and java doesn't support them. 5 Because of this, the /etc/krb5.conf should be modified. % cat /etc/krb5.conf . . . # Make sure the below is commented # default_ccache_name = KEYRING:persistent:%{uid} . . . DNS Forwarding \u00b6 DNS Forwarding to DoT \u00b6 Presently, FreeIPA does not support DoT (DNS over TLS) nor DoH (DNS over HTTPS) (this appears to be a bind limitation and we can't find documentation that says otherwise). However, it is possible to setup unbound to do the forwarding for you, in which you tell your bind servers (or in this case, the bind DNS servers in your IPA domain) to forward to that unbound server for all forwarding. Keep it Separate It is recommended to keep your unbound service separate from the IPA servers. Spin up another instance in your network that will run unbound or run it on a standalone bind server that you may have on a separate port. To forward to the unbound service, modify the DNS global configuration in IPA: # Replace 10.100.0.224 with the IP of your unbound instance % ipa dnsconfig-mod --forward-policy=only --forwarder='10.100.0.224' # Add 'port xxxx' if you have set unbound to another port % ipa dnsconfig-mod --forward-policy=only --forwarder='10.100.0.224 port 9553' Logging \u00b6 Audit Logs \u00b6 By default, the audit logs in /var/log/dirsrv/slapd-INSTANCE/audit do not get populated. And the access logs don't show much in terms of modifications and what is being changed. There is also /var/log/httpd/* logs, but it may be useful to see ldif style logging for changes against FreeIPA. # Modify the DSE configuration by turning on audit logging [label@ipa01 ~]# ldapmodify -D \"cn=directory manager\" -W -p 389 -h localhost Enter LDAP Password: dn: cn=config changetype: modify replace: nsslapd-auditlog-logging-enabled nsslapd-auditlog-logging-enabled: on # Press CTRL+d here modifying entry \"cn=config\" # To test, I'll add a user to a group [label@ipa01 ~]$ ipa group-add-member --users=jbaskets aocusers Group name: aocusers GID: 686600003 Member users: ..., jbaskets ------------------------- Number of members added 1 ------------------------- # Let's verify the log [label@ipa01 ~]$ sudo su - [sudo] password for label: Last login: Sun Mar 29 16:42:36 MST 2020 on pts/0 [root@ipa01 ~]# cd /var/log/dirsrv/slapd-EXAMPLE-NET/ [root@ipa01 slapd-EXAMPLE-NET]# cat audit time: 20200329223754 dn: cn=config result: 0 changetype: modify replace: nsslapd-auditlog-logging-enabled nsslapd-auditlog-logging-enabled: on - replace: modifiersname modifiersname: cn=directory manager - replace: modifytimestamp modifytimestamp: 20200330053754Z - 389-Directory/1.4.1.3 B2019.323.229 ipa01.example.net:636 (/etc/dirsrv/slapd-EXAMPLE-NET) # Looks like right here the modification happened time: 20200329224007 dn: cn=aocusers,cn=groups,cn=accounts,dc=example,dc=net result: 0 changetype: modify add: member member: uid=jbaskets,cn=users,cn=accounts,dc=example,dc=net - replace: modifiersname modifiersname: uid=label,cn=users,cn=accounts,dc=example,dc=net - replace: modifytimestamp modifytimestamp: 20200330054006Z - replace: entryusn entryusn: 900028 - Certificates \u00b6 These are notes of things I've ran into before while dealing with certificates. Renewed IPA HTTP Certificate Stuck \u00b6 This was something I discovered sort of on accident but never really \"noticed\" - Though I'm sure I would've noticed sometime in 2021 when my certificate expired. I was running ipa-healthcheck --failures-only as I do sometimes, and noticed some weird certmonger things pop up. But it made me look at my certificate list... [root@ipa01 ~]# ipa-getcert list Number of certificates and requests being tracked: 9. Request ID '20191106025922': status: MONITORING stuck: no key pair storage: type=FILE,location='/var/kerberos/krb5kdc/kdc.key' certificate: type=FILE,location='/var/kerberos/krb5kdc/kdc.crt' CA: IPA issuer: CN=Certificate Authority,O=ANGELSOFCLOCKWORK.NET subject: CN=ipa01.angelsofclockwork.net,O=ANGELSOFCLOCKWORK.NET expires: 2021-11-05 19:59:27 MST principal name: krbtgt/ANGELSOFCLOCKWORK.NET@ANGELSOFCLOCKWORK.NET key usage: digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment eku: id-kp-serverAuth,id-pkinit-KPKdc pre-save command: post-save command: /usr/libexec/ipa/certmonger/renew_kdc_cert track: yes auto-renew: yes Request ID '20200123075636': status: MONITORING stuck: no key pair storage: type=NSSDB,location='/etc/dirsrv/slapd-ANGELSOFCLOCKWORK-NET',nickname='Server-Cert',token='NSS Certificate DB',pinfile='/etc/dirsrv/slapd-ANGELSOFCLOCKWORK-NET/pwdfile.txt' certificate: type=NSSDB,location='/etc/dirsrv/slapd-ANGELSOFCLOCKWORK-NET',nickname='Server-Cert',token='NSS Certificate DB' CA: IPA issuer: CN=Certificate Authority,O=ANGELSOFCLOCKWORK.NET subject: CN=ipa01.angelsofclockwork.net,O=ANGELSOFCLOCKWORK.NET expires: 2021-11-05 19:55:33 MST dns: ipa01.angelsofclockwork.net principal name: ldap/ipa01.angelsofclockwork.net@ANGELSOFCLOCKWORK.NET key usage: digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment eku: id-kp-serverAuth,id-kp-clientAuth pre-save command: post-save command: /usr/libexec/ipa/certmonger/restart_dirsrv ANGELSOFCLOCKWORK-NET track: yes auto-renew: yes Request ID '20200123075639': status: NEWLY_ADDED_NEED_KEYINFO_READ_PIN stuck: yes key pair storage: type=FILE,location='/var/lib/ipa/private/httpd.key' certificate: type=FILE,location='/var/lib/ipa/certs/httpd.crt' CA: IPA issuer: CN=Certificate Authority,O=ANGELSOFCLOCKWORK.NET subject: CN=ipa01.angelsofclockwork.net,O=ANGELSOFCLOCKWORK.NET expires: 2021-11-05 19:55:48 MST dns: ipa01.angelsofclockwork.net principal name: HTTP/ipa01.angelsofclockwork.net@ANGELSOFCLOCKWORK.NET key usage: digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment eku: id-kp-serverAuth,id-kp-clientAuth pre-save command: post-save command: /usr/libexec/ipa/certmonger/restart_httpd track: yes auto-renew: yes Interestingly, I wasn't sure what NEWLY_ADDED_NEED_KEYINFO_READ_PIN meant and I couldn't really find much on what would cause this to happen. And I know my certificate isn't expired, according to the output. In fact, I checked with openssl just in case. [root@ipa01 ~]# openssl x509 -text -noout -in /var/lib/ipa/certs/httpd.crt | grep 'Not After' Not After : Nov 6 02:55:48 2021 GMT I'm not sure if this is just a result of migrating from Enterprise Linux 7 to 8 at the time, but it seemed easy enough to remove the tracking and put it back in, which ultimately fixed the monitoring state and now it was no longer \"stuck\". [root@ipa01 ~]# ipa-getcert stop-tracking -i 20200123075639 Request \"20200123075639\" removed. [root@ipa01 ~]# ipa-getcert start-tracking -f /var/lib/ipa/certs/httpd.crt -k /var/lib/ipa/private/httpd.key -p /var/lib/ipa/passwds/ipa01.angelsofclockwork.net-443-RSA -C /usr/libexec/ipa/certmonger/restart_httpd -K HTTP/ipa01.angelsofclockwork.net@ANGELSOFCLOCKWORK.NET New tracking request \"20200504003758\" added. [root@ipa01 ~]# ipa-getcert list -i \"20200504003758\" Number of certificates and requests being tracked: 9. Request ID '20200504003758': status: MONITORING stuck: no key pair storage: type=FILE,location='/var/lib/ipa/private/httpd.key',pinfile='/var/lib/ipa/passwds/ipa01.angelsofclockwork.net-443-RSA' certificate: type=FILE,location='/var/lib/ipa/certs/httpd.crt' CA: IPA issuer: CN=Certificate Authority,O=ANGELSOFCLOCKWORK.NET subject: CN=ipa01.angelsofclockwork.net,O=ANGELSOFCLOCKWORK.NET expires: 2021-11-05 19:55:48 MST dns: ipa01.angelsofclockwork.net principal name: HTTP/ipa01.angelsofclockwork.net@ANGELSOFCLOCKWORK.NET key usage: digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment eku: id-kp-serverAuth,id-kp-clientAuth pre-save command: post-save command: /usr/libexec/ipa/certmonger/restart_httpd track: yes auto-renew: yes CA Related Certificates Stuck \u00b6 Like with the IPA httpd certificates, I noticed at least 4 certificates stuck because a PIN was missing. Turns out that it's actually easy to modify the tracking request and fix the issue entirely. Below is my example doing this on the auditSigningCert. This seems to only occur on Enterprise Linux 8. [root@ipa01 alias]# getcert list -i 20200615180351 Number of certificates and requests being tracked: 9. Request ID '20200615180351': status: NEWLY_ADDED_NEED_KEYINFO_READ_PIN stuck: yes key pair storage: type=NSSDB,location='/etc/pki/pki-tomcat/alias',nickname='auditSigningCert cert-pki-ca' certificate: type=NSSDB,location='/etc/pki/pki-tomcat/alias',nickname='auditSigningCert cert-pki-ca' CA: dogtag-ipa-ca-renew-agent issuer: subject: expires: unknown pre-save command: /usr/libexec/ipa/certmonger/stop_pkicad post-save command: /usr/libexec/ipa/certmonger/renew_ca_cert \"auditSigningCert cert-pki-ca\" track: yes auto-renew: yes [root@ipa01 alias]# getcert start-tracking -i 20200615180351 -p /etc/pki/pki-tomcat/alias/pwdfile.txt Request \"20200615180351\" modified. [root@ipa01 alias]# getcert list -i 20200615180351 Number of certificates and requests being tracked: 9. Request ID '20200615180351': status: MONITORING stuck: no key pair storage: type=NSSDB,location='/etc/pki/pki-tomcat/alias',nickname='auditSigningCert cert-pki-ca',token='NSS Certificate DB',pinfile='/etc/pki/pki-tomcat/alias/pwdfile.txt' certificate: type=NSSDB,location='/etc/pki/pki-tomcat/alias',nickname='auditSigningCert cert-pki-ca',token='NSS Certificate DB' CA: dogtag-ipa-ca-renew-agent issuer: CN=Certificate Authority,O=ANGELSOFCLOCKWORK.NET subject: CN=CA Audit,O=ANGELSOFCLOCKWORK.NET expires: 2021-03-13 23:15:41 MST key usage: digitalSignature,nonRepudiation pre-save command: /usr/libexec/ipa/certmonger/stop_pkicad post-save command: /usr/libexec/ipa/certmonger/renew_ca_cert \"auditSigningCert cert-pki-ca\" track: yes auto-renew: yes Default Certificates with SAN \u00b6 A question that arises now and again is how to setup a load balancer for FreeIPA's LDAP servers whether it's an actual load balancer (layer 4) or some sort of DNS record with multiple A records, or perhaps with some sort of round robin DNS. The issue is that the certificate verification fails, because the certificate being presented is of the IPA server itself with no SAN. To address this, you have to create a host that has the name of the load balancer or DNS record you plan on using and allow the IPA servers to manage the host. CMS Communication Issues (403) \u00b6 This isn't necessarily certificate issue, but more or less an issue as it pertains to the certificate system itself. There may be cases where during upgrades, a configuration in /etc/pki/pki-tomcat/server.xml is not properly reconfigured. In that file, you'll notice Connector lines that have a secret and a requiredSecret parameter and they both have different values. The issue may be that these aren't correct. This generally comes down to IPA and pki-core conflicting on these attributes. To correct this, you will need to find the secret in /etc/httpd/conf.d/ipa-pki-proxy.conf (on the ProxyPass line) and ensure that's the same secret in both fields. ProxyPassMatch ajp://localhost:8009 secret=AAA Make sure they're the same in server.xml After changing, restart the service with systemctl restart pki-tomcat@pki-tomcatd.service. Kerberos \u00b6 This section goes over some stuff about kerberos that we've ran into and might find useful someday. Accounts with OTP Enabled \u00b6 When logging into a machine with a password (first factor) and an OTP token (second factor), this generally works without a problem. You can easily run klist and you'll see that you have a ticket and everything. In the cases where you're calling kinit all by itself, this doesn't work as expected at the time of this writing. % kinit account@REALM kinit: Pre-authentication failed: Invalid argument while getting initial credentials A bugzilla was opened about this issue in 2017, a pagure issue was opened in 2014 about this exact scenario, where IPA is configured for password+OTP and a user has an assigned token. There is currently one workaround, which is using kinit -n to perform anonymous processing. Footnotes \u00b6 For more information on DNS for FreeIPA, please read this page and this page \u21a9 The -P asks for the password of the username in question, that way it is cached right away. The directory service on the system then has credentials to compare to. I have found that sometimes if you don't use -P, even if you're logged in as the account, the password does not get cached and you'll get stuck at a background image the next time you login. Again, this is only sometimes. Your mileage may vary here. \u21a9 The -P asks for the password of the username in question, that way it is cached right away. The directory service on the system then has credentials to compare to. I have found that sometimes if you don't use -P, even if you're logged in as the account, the password does not get cached and you'll get stuck at a background image the next time you login. Again, this is only sometimes. Your mileage may vary here. \u21a9 Please read this page for more information. \u21a9 This may have changed. However it is up to you to test if this is the case. \u21a9","title":"FreeIPA"},{"location":"el/freeipa/#overview","text":"FreeIPA is an integrated security information management system combining Linux, a Directory Server (389), Kerberos, NTP, DNS, DogTag. It's a system that can be loosely compared to Active Directory in what it attempts to solve for Linux and UNIX clients and even mixed environments. While it is not an active directory, it is an integrated Identity and Authentication solution for Linux/UNIX environments, which means it does not support Windows clients. One problem that FreeIPA attempts to solve is giving back control to the Linux/UNIX administration teams of access, authentication, and authorization rather than trying to integrate directly into Active Directory, where the controls do not work the same or do not work at all. And because of this, no third party software is required to be installed.","title":"Overview"},{"location":"el/freeipa/#requirements","text":"Here are the list of requirements below. Enterprise Linux 8+ or Fedora Linux An active internet connection to install the packages required or available internal mirrors 2 core, 4GB system with at least 10GB+ disk for /var/lib/dirsrv DNS domain delegation (if a DNS appliance or server already exists)","title":"Requirements"},{"location":"el/freeipa/#tutorial-preface-notes-and-recommendations","text":"Potential Pitfalls! Leave SELinux enabled at all times. You will not run into SELinux issues FreeIPA runs better when it controls the DNS domain that it is given - It is recommended DNS is delegated or that FreeIPA run DNS entirely FreeIPA does not run DHCP. ISC DHCP can be configured to do dynamic DNS updates to FreeIPA or hosts can be configured to perform dynamic DNS updates Recommended Information Keep selinux set to enforcing DNS - You must be careful when using DNS. Here are recommendations. 1 Recommendation 1: FreeIPA runs your entire DNS for your network - This requires the DHCP servers to set the DNS servers to the IPA servers. This will be useful in the case that your clients will have their SSH keys added as SSHFP records to DNS when enrolled as clients. This also gives you the added benefit of a client updating its own DNS entries (A and PTR records) if the client is DHCP enabled and the IP changes if you so choose. Recommendation 2: FreeIPA is delegated a subdomain of a domain used already in the network - It's not required for hosts to live in the subdomain to be a member of the IPA domain, but you will lose out on kerberos SSO. Do not try to hijack a domain. Consider setting up a trust with Active Directory if you are in a mixed environment, eg Active Directory already exists - winsync is available, but deprecated and not recommended. IPA servers should have static assigned addresses - Configured via nmcli or directly in /etc/sysconfig/network-scripts/ifcfg-* Try to avoid running FreeIPA without DNS - while possible, you are creating higher maintenance Trust Information If you are in a mixed environment (both Windows and Linux/UNIX), it is recommended to setup a trust between FreeIPA and Active Directory. Because of this, they will need to be in different domains (eg, example.com and ipa.example.com, or example.com and example.net). This way, you do not have to create duplicate users if a windows user logs into Linux resources nor use winsync.","title":"Tutorial Preface, Notes, and Recommendations"},{"location":"el/freeipa/#dns","text":"As noted in the previous section, you must try not to hijack a domain. You can migrate records over to FreeIPA's DNS if you'd like, but care must be taken with that approach. While FreeIPA can do the typical DNS server work such as forward/reverse zones and various types of records, it should not be considered a full solution. It does not support views (eg, you can't have internal and external views, assuming you have domains that are publically facing). In the event you need to have views, that's when you need a different DNS server or service to provide this to you. There are two ways you can have DNS entries updated dynamically: --enable-dns-updates for ipa-client-install and DHCP dynamic DNS updates. Both are sufficient. The latter requires additional work and is outside the scope of this write up.","title":"DNS"},{"location":"el/freeipa/#delegation","text":"Throughout this guide, you may find or see examples of domain delegation where there is an AD trust, as it would be a more real world example of bringing in FreeIPA to an environment that is already in place, working, with a DNS hosted by AD or by an appliance. Majority of the examples assume both IPA and AD is delegated (when it's normally IPA that's just delegated while AD hosts the actual parent zone). Using this type of setup, it is not required for clients to have entries in the IPA domain. In fact, they can be in other domains as long as they have A/AAAA/PTR records associated with them. This assumes that there could be dynamic dns associated with DHCP or everything is static and lives in the parent zones. The caveat to this is SSO will fail . You can setup already existing DNS servers to delegate an entire domain or a subdomain for FreeIPA. This way, you don't overlap with a domain that's already in use. So for example, if AD owns example.com, you could have AD delegate ipa.example.com or even forward example.net. If AD is not the DNS provider for the environment, you can have the appliance delegate the domain in the same manner. Below is a bind example of what example.com would look like when delegating the IPA domain: $ORIGIN example.com. @ IN SOA ... ( ) NS np-ad01 NS np-ad02 np-ad01 A 10.200.0.232 np-ad02 A 10.200.0.233 ; Many other records here, pertaining to AD, eg msdcs and SRV records ; IPA records $ORIGIN ipa.example.com. @ NS np-ipa01 NS np-ipa02 np-ipa01 A 10.200.0.230 np-ipa02 A 10.200.0.231 Note that AD can send nsupdates to a DNS server if given the permissions. As of this writing, FreeIPA does not do this, which is why DNS delegation is recommended.","title":"Delegation"},{"location":"el/freeipa/#server-setup","text":"","title":"Server Setup"},{"location":"el/freeipa/#required-packages","text":"ipa-server ipa-client (required as an IPA server is technically a client of the domain) ipa-server-dns (required for using the internal DNS) sssd/sssd-ipa (pulled in as dependencies)","title":"Required Packages"},{"location":"el/freeipa/#optional-packages","text":"ipa-server-trust-ad if using an AD trust","title":"Optional Packages"},{"location":"el/freeipa/#installation","text":"To install the server, make sure the hostname is set to the A records and NS delegations you've put in DNS (which won't respond to a DNS lookup). If these are stand-alone, then you can just keep it at the top level (eg, example.com). You'll also need to modify /etc/hosts, set static IP addresses, and then run the ipa-server-install command. % hostnamectl set-hostname server1.ipa.example.com % nmcli con mod ens192 ipv4.address 10.200.0.230/24 % nmcli con mod ens192 ipv4.gateway 10.200.0.1 % nmcli con mod ens192 ipv4.method manual % nmcli con up ens192 % vi /etc/hosts . . . 10.200.0.230 server1.ipa.example.com 10.200.0.231 server2.ipa.example.com # Fedora % dnf install freeipa-server{,-common,-dns,-trust-ad} -y # Enterprise Linux 8 % dnf module enable idm:DL1/{dns,adtrust,client,server,common} % dnf install ipa-server ipa-server-dns ipa-client sssd sssd-ipa -y # Enterprise Linux 9 (there appears to be no modules) % dnf install ipa-server ipa-server-dns ipa-client sssd sssd-ipa -y # Setup # Enterprise 8 / 9 % firewall-cmd --permanent --add-service={freeipa-4,ntp,dns,freeipa-trust} % firewall-cmd --complete-reload % ipa-server-install \\ --no_hbac_allow \\ <-- If you want to have HBAC allow_all disabled initially --no-ntp \\ <-- If you want to host NTP from IPA, take off --no-ntp --setup-dns \\ --realm IPA.EXAMPLE.COM \\ --domain example.com . . . (show steps here) While not officially recommended, you could have two accounts. One for administration of servers and the domain and one for your workstation, similar to separating domain users and domain administrators in active directory. You don't have to follow this, but at least there's a form of separation. % kinit admin % ipa user-add --first=First --last=Last --cn=\"First Last Admin\" --gecos=\"First Last Admin\" flast2 % ipa group-add-member --users=flast2 admins","title":"Installation"},{"location":"el/freeipa/#replica","text":"On the replica, ensure you repeat the same steps as above. % hostnamectl set-hostname server2.ipa.example.com % nmcli con mod ens192 ipv4.address 10.200.0.231/24 % nmcli con mod ens192 ipv4.gateway 10.200.0.1 % nmcli con mod ens192 ipv4.method manual % nmcli con up ens192 % vi /etc/hosts . . . 10.200.0.230 server1.ipa.example.com 10.200.0.231 server2.ipa.example.com % dnf install ipa-server ipa-server-dns ipa-client sssd sssd-ipa -y # Enterprise 8 / 9 % firewall-cmd --permanent --add-service={freeipa-4,ntp,dns,freeipa-trust} % firewall-cmd --complete-reload % ipa-replica-install --no-forwarders --setup-ca --setup-dns --no-ntp --principal admin --admin-password \"ChangePass123\" --domain ipa.example.com . . . (show steps) You should now be able to see your replicas. % ipa-replica-manage list server1.ipa.example.com: master server2.ipa.example.com: master","title":"Replica"},{"location":"el/freeipa/#replica-automation","text":"It is possible to automate the replica installation. To automate the replica installation, the following requirements would need to be met: Server must be added as a client (ipa-client-install) with an IP address on the commandline Server must be added to the ipaservers host group ipa-replica-install ran without principal and passwords Once you have a server added as a client and then added to the ipaservers host group, you would run a command like this: % ipa-replica-install --ssh-trust-dns --unattended --setup-ca --mkhomedir --setup-dns --no-forwarders If you have forwarders, use the --forwarders option instead.","title":"Replica Automation"},{"location":"el/freeipa/#server-migrationupgrade","text":"Performing a migration is a multi-step process. Typically you are going from one major version of Enterprise Linux (such as 7 or 8) to another (such as 9). Regardless of which version you are migrating from, the typical beginning steps are: System's time is verified for time synchronization like using ntpstat or equivalent Server roles are verified in the current environment using ipa server-role-find --status enabled --server ipa.example.com New system is installed and enrolled as a client New system is added as a replica with required server roles EL7 to EL9 / Two Major Version Jumps When jumping from EL7 to EL9 or two major versions in general, it is recommended that you have an \"in between\" machine. This means that you need to add the in between version first and then you can add the latest version. See this page for an example. The below is in the case of a single master installation and doesn't take into account of multiple version jumps. Let's say you have two old Enterprise Linux replicas instead. There are two approaches you can take: Install a new Enterprise Linux system, add it, reinstall old system to the new version, add it back. Install two new Enterprise Linux systems, add them as needed, power off old systems. Below is an example, with X being the old version, and Y being the new. Enterprise Linux Y system is installed and enrolled as a client Enterprise Linux Y system is added as a replica Change CRL to Enterprise Linux Y system and adjust settings on Enterprise Linux X CA master and new Enterprise Linux Y replica for pki-tomcatd and httpd Test user is created to ensure DNA range is adjusted Verify DNA range Stop first Enterprise Linux X IPA services, remove replica, uninstall, power off. Second Enterprise Linux Y system is installed and enrolled as a client Second Enterprise Linux Y system is added as a replica Test user is created again to ensure DNA range is adjusted Verify DNA range Stop second Enterprise Linux X IPA services, remove replica, uninstall, power off.","title":"Server Migration/Upgrade"},{"location":"el/freeipa/#el7-to-el8","text":"# Enterprise Linux 8 % dnf module enable idm:DL1 # Install necessary packages, ie AD trust packages if you need them % dnf install ipa-server ipa-server-dns -y % ipa-client-install --realm EXAMPLE.COM --domain example.com % kinit admin # Add other switches that you feel are necessary, such as forwarders, kra, ntp... % ipa-replica-install --setup-dns --setup-ca --ssh-trust-dns --mkhomedir # Verify all services are in a RUNNING state % ipactl status Directory Service: RUNNING . . . % ipa-csreplica-manage list elX.example.com: master elY.example.com: master % ipa-csreplica-manage list --verbose elY.example.com Directory Manager password: elX.example.com last init status: None last init ended: 1970-01-01 00:00:00+00:00 last update status: Error (0) Replica acquired successfully: Incremental update succeeded last update ended: 2019-11-07 22:46:15+00:00 Change CRL to new Enterprise Linux system and adjust settings on both replicas for pki-tomcatd and httpd # Change CA master to elY % ipa config-mod --ca-renewal-master-server elY.example.com # Shut down all CRL generation on ELX elX% ipa-crlgen-manage status CRL generation: enabled . . . elX% ipa-crlgen-manage disable Stopping pki-tomcatd Editing /var/lib/pki/pki-tomcat/conf/ca/CS.cfg Starting pki-tomcatd Editing /etc/httpd/conf.d/ipa-pki-proxy.conf Restarting httpd CRL generation disabled on the local host. Please make sure to configure CRL generation on another master with ipa-crlgen-manage enable. The ipa-crlgen-manage command was successful # Verify that the /etc/httpd/conf.d/ipa-pki-proxy.conf file's RewriteRule is not commented # If it is, remove the comment and restart httpd. ipa-crlgen-manage should take care of this. % tail -n 1 /etc/httpd/conf.d/ipa-pki-proxy.conf RewriteRule ^/ipa/crl/MasterCRL.bin https://elX.example.com/ca/ee/ca/getCRL?op=getCRL&crlIssuingPoint=MasterCRL [L,R=301,NC] # Turn it on with ELY elY% systemctl stop pki-tomcatd@pki-tomcat.service # The values should be changed from false to true elY% vi /etc/pki/pki-tomcat/ca/CS.cfg ca.crl.MasterCRL.enableCRLCache=true ca.crl.MasterCRL.enableCRLUpdates=true elY% systemctl start pki-tomcatd@pki-tomcat.service # Make sure the rewrite rule has a comment on elY elY% vi /etc/httpd/conf.d/ipa-pki-proxy.conf . . . #RewriteRule ^/ipa/crl/MasterCRL.bin https://elY.example.com/ca/ee/ca/getCRL?op=getCRL&crlIssuingPoint=MasterCRL [L,R=301,NC] elY% systemctl restart httpd Test user is created to ensure DNA range is adjusted and replication is working % ipa user-add --first=testing --last=user testinguser1 # Test on both systems elX% ipa user-find testinguser1 elY% ipa user-find testinguser1 Verify DNA range # There should be ranges for both replicas % ipa-replica-manage dnarange-show elX.example.com: ... elY.example.com: ... Stop old Enterprise Linux IPA services, remove replica, uninstall # Stop all elX services elX% ipactl stop # Delete the elX system from the topology elY% ipa server-del elX.example.com # Uninstall and/or power down system elX% ipa-server-install --uninstall elX% init 0","title":"EL7 to EL8"},{"location":"el/freeipa/#el8-to-el9","text":"# Enterprise Linux 9 % dnf install ipa-server ipa-server-dns -y % ipa-client-install --realm EXAMPLE.COM --domain example.com % kinit admin # Add other switches that you feel are necessary, such as forwarders, kra, ntp... % ipa-replica-install --setup-dns --setup-ca --ssh-trust-dns --mkhomedir # Verify all services are in a RUNNING state % ipactl status Directory Service: RUNNING . . . % ipa-csreplica-manage list elX.example.com: master elY.example.com: master % ipa-csreplica-manage list --verbose elY.example.com Directory Manager password: elX.example.com last init status: None last init ended: 1970-01-01 00:00:00+00:00 last update status: Error (0) Replica acquired successfully: Incremental update succeeded last update ended: 2022-08-12 18:11:11+00:00 Set the CA renewal master to the new system and change the CRL settings % ipa config-mod --ca-renewal-master-server elY.example.com # Remove the ca.certStatusUpdateInterval entry or set it to 600 (default) on elY elY% vim /etc/pki/pki-tomcat/ca/CS.cfg # Restart the ipa services elY% ipactl restart # Set the value of ca.certStatusUpdateInterval on elX to 0 elX% vim /etc/pki/pki-tomcat/ca/CS.cfg ca.certStatusUpdateInterval=0 elX% ipactl restart elX% ipa-crlgen-manage status CRL generation: enabled . . . elX% ipa-crlgen-manage disable Stopping pki-tomcatd Editing /var/lib/pki/pki-tomcat/conf/ca/CS.cfg Starting pki-tomcatd Editing /etc/httpd/conf.d/ipa-pki-proxy.conf Restarting httpd CRL generation disabled on the local host. Please make sure to configure CRL generation on another master with ipa-crlgen-manage enable. The ipa-crlgen-manage command was successful elX% ipa-crlgen-manage status CRL generation: disabled Create a test user to ensure DNA range is adjusted and replication is working elY% ipa user-add --first=testing --last=user testinguser1 # Test on both systems elX% ipa user-find testinguser1 elY% ipa user-find testinguser1 Verify DNA range. # There should be ranges for both replicas % ipa-replica-manage dnarange-show elX.example.com: ... elY.example.com: ... Stop old Enterprise Linux IPA services, remove replica, uninstall. # Stop all elX services elX% ipactl stop # Delete the elX system from the topology elY% ipa server-del elX.example.com # Uninstall and/or power down system elX% ipa-server-install --uninstall elX% init 0 See this page for more information.","title":"EL8 to EL9"},{"location":"el/freeipa/#active-directory-trust","text":"To initiate a trust with your active directory domain, ensure the following requirements are met. Requirements Package installed: ipa-server-trust-ad DNS: Properly configured that FreeIPA can resolve the AD servers A and SRV records This can either be forwarders to AD, a subdomain that IPA manages, or delegated subdomain from the master DNS servers in your network. This is completely dependent on your infrastructure. DNS: AD forest has sites and SRV records, including priorities, are set correctly When the following requirements are met, you have two choices before continuning. You can either use POSIX or have the id range generated automatically. POSIX vs Non-POSIX If you decide to use POSIX, your AD users are expected to have uidNumber, gidNumber, loginShell, unixHomeDirectory set. Else, you will need to setup ID overrides if you already have that information for current users (assuming this is not a new setup for the environment, ie you already have UID's for people). If you are not planning a migration from pure AD over to IPA with a trust, it is recommended to note that information so you can setup the ID overrides. Afterwards, any new users will get UID/GID's that you will not have to manage yourself. You will need to prep your master(s) for the trust. We will be enabling compat, adding sids, and adding agents so both masters can provide AD information. % ipa-adtrust-install --add-sids --add-agents --enable-compat This will do what we need. If you do not have legacy clients (Enterprise Linux 5, Solaris, HP-UX, AIX, SLES 11.4, FreeBSD, the list goes on), then you do not need to enable compat mode. Though, it could be useful to have it for certain apps or scenarios. You will now need to open the necessary ports. Do this on all masters. Ports TCP: 135, 138, 139, 389, 445, 1024-1300, 3268 UDP: 138, 139, 389, 445 % firewall-cmd --add-service=freeipa-trust --permanent % firewall-cmd --complete-reload Now you can initiate the trust. The admin account you use should be part of the domain admins group or at least have permissions to initiate a trust. The former is path of least resistance. # If you are using POSIX ID, use ipa-ad-trust-posix. % ipa trust-add --type=ad example.com --range-type=ipa-ad-trust --admin adminaccount --password Once the trust is up, verify it. % ipa trust-show example.com Realm name: example.com Domain NetBIOS name: AD Domain Security Identifier: S-X-X-XX-XXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX Trust direction: Trusting forest Trust type: Active Directory domain UPN suffixes: example.com You should be able to test for the users now. % id aduser1@example.com uid=XXXXX(aduser1@example.com) gid=XXXXX(aduser1@example.com) groups=XXXXX(aduser1@example.com)","title":"Active Directory Trust"},{"location":"el/freeipa/#disable-anonymous-bind","text":"In some cases, it is a requirement to disable all anonymous binds. If this is the case, you will need to modify cn=config on each master as it is not replicated. rootdse Some applications do anonymous binds to the directory server to determine its version and it supported controls. While it is possible to disable anonymous binds completely, it is important to know that if you disable the rootdse binds, applications that do anonymous lookups to get server information will fail. % ldapmodify -xZZ -D \"cn=Directory Manager\" -W -h server.ipa.example.com Enter LDAP Password: dn: cn=config changetype: modify replace: nsslapd-allow-anonymous-access nsslapd-allow-anonymous-access: rootdse modifying entry \"cn=config\"","title":"Disable Anonymous Bind"},{"location":"el/freeipa/#client-setup","text":"","title":"Client Setup"},{"location":"el/freeipa/#enterprise-linux-fedora","text":"Ensure your /etc/resolv.conf (or other dns settings) are set correctly. Ensure your hostname is also set correctly. % dnf install ipa-client -y % ipa-client-install --realm EXAMPLE.COM --domain example.com --mkhomedir","title":"Enterprise Linux & Fedora"},{"location":"el/freeipa/#mac-clients","text":"MacOS Clients are an interesting workstation to setup as a FreeIPA client. It takes a little bit of fighting and troubleshooting, but it can work with the right settings. Note that as of Catalina, you may not be able to login to your account nor will creating a mobile account function as you would expect. This may have changed in recent macos releases, so YMMV. Other Guides There are a couple of guides out there that you may have found before (if you looked) that help setup IPA for Mac. There's one for much older (I think Lion) and one for Sierra. This section was made mostly for my own reference because I found some things in both of those guides didn't address issues I ran into one way or another and couldn't find any information on. The FreeIPA users mail list didn't have any archives with people having similar issues. If you are interested in the other guides to compare to, you may see them here (recent) and here (older) AD Users AD Users You cannot login as AD users on a Mac when going through FreeIPA. You can, in theory, point to the cn=compat tree and set the attribute mapping to rfc2307. In my tests, I have never been able to get this to work. This section, I am going to assume you are going to be logging in as a user in IPA. If you are in a mixed environment, add your Mac to your AD domain instead. Anonymous Bind There may be cases where if you have disabled anonymous binds in IPA, this setup may not work, even if you do use a bind account. You will need to experiment with this if you plan on using a bind account and plan on or currently have IPA not allowing anonymous binds. Check your system's hostname. You want to make sure it has a hostname defined for it in the domain the mac sits in, even if it's dynamic via DHCP/DNS. % sudo scutil --set HostName mac.example.com Get the IPA certificate. You'll need to double click it after you get it and import it. % cd ~/Desktop && curl -OL http://server1.ipa.example.com/ipa/config/ca.crt % sudo mkdir /etc/ipa % sudo cp ca.crt /etc/ipa/ca.crt % sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /etc/ipa/ca.crt On the IPA server, you will need to create a host and get the keytab. % ipa host-add mac.example.com --macaddress=\"00:00:00:00:00:00\" % ipa-getkeytab -s server1.ipa.example.com -p host/mac.example.com -k /tmp/krb5.keytab You will need to transfer that keytab to your mac. % cd ~ % scp user@server1.ipa.example.com:/tmp/krb5.keytab . % sudo mv krb5.keytab /etc/krb5.keytab % sudo chmod 600 /etc/krb5.keytab % sudo chown root:wheel /etc/krb5.keytab Configure /etc/krb5.conf [domain_realm] .ipa.example.com = IPA.EXAMPLE.COM ipa.example.com = IPA.EXAMPLE.COM [libdefaults] default_realm = IPA.EXAMPLE.COM allow_weak_crypto = yes dns_lookup_realm = true dns_lookup_kdc = true rdns = false ticket_lifetime = 24h forwardable = yes renewable = true [realms] IPA.EXAMPLE.COM = { # You don't need to set these when your DNS is setup correctly, but it doesn't hurt to have a reference. # In my opinion, you shouldn't hardcode these values. You have to have a good reason to. #kdc = tcp/server1.ipa.example.com #kdc = tcp/server2.ipa.example.com #admin_server = tcp/server1.ipa.example.com #admin_server = tcp/server2.ipa.example.com pkinit_anchors = FILE:/etc/ipa/ca.crt } You'll want to do a kinit to verify. If it works, you should be able to go to the FreeIPA webui and check that the host is \"enrolled\" (Identity -> Hosts). % kinit username@IPA.EXAMPLE.COM You need to modify a couple of pam files. I'll explain why they need to be changed. % sudo vi /etc/pam.d/authorization # authorization: auth account # Putting krb5 here twice ensures that you can login via kerberos and also get a keytab # If \"no_ccache\" is here, keytabs will not be available on login auth optional pam_krb5.so use_first_pass use_kcminit default_principal auth sufficient pam_krb5.so use_first_pass default_principal auth required pam_opendirectory.so use_first_pass nullok account required pam_opendirectory.so % sudo vi /etc/pam.d/screensaver # The krb5 changes do similar to the authorization when on the lock screen after a sleep #auth optional pam_krb5.so use_first_pass use_kcminit auth optional pam_krb5.so use_first_pass use_kcminit default_principal auth sufficient pam_krb5.so use_first_pass default_principal auth required pam_opendirectory.so use_first_pass nullok account required pam_opendirectory.so account sufficient pam_self.so account required pam_group.so no_warn group=admin,wheel fail_safe account required pam_group.so no_warn deny group=admin,wheel ruser fail_safe % sudo vi /etc/pam.d/passwd # Helps with kerberos logins password sufficient pam_krb5.so auth required pam_permit.so account required pam_opendirectory.so password required pam_opendirectory.so session required pam_permit.so After these changes, you'll need to go into make some changes with the directory utility. This depends on your macOS version.","title":"Mac Clients"},{"location":"el/freeipa/#monterey-and-older","text":"Go to system preferences -> users & groups -> login options - Click the 'lock' to make changes Set the following: Automatic login: Off Display login window as: Name and Password Show fast user switching menu as: Full Name Click \"Join\" next to \"Network Account Server\" Enter one of your IPA servers (you can duplicate it later for backup purposes) and click Continue. Ensure \"Allow network users to log in at login window\" is checked - Make sure it's set to all users Click \"edit\" next to the \"Network Account Server\" Click \"Open Directory Utility\" Click the lock, edit LDAPv3 Select your server and click \"edit\" Set the following options: Open/close times out in 5 seconds Query times out in 5 seconds Connection idles out in 1 minute (this can't be changed) Encrypt using SSL (selected) Click \"Search & Mappings\" You may either select \"rfc2307\" from the dropdown or select custom. It will ask your base DN (eg, dc=ipa,dc=example,dc=com) If you select rfc2307, it will ask for your base DN (eg, dc=ipa,dc=example,dc=com) If you select \"custom\", you will need to do this manually for each record type. You're better off using rfc2307 and working from there Click the \"+\" to add a groups record type or scroll and find \"groups\". Select \"groups\", and ensure the following object classes exist. You can click the \"+\" to add them when needed. Record Type ObjectClasses Groups posixGroup ipausergroup groupOfNames\\* Note \"groupOfNames\" is optional here, because it seems that the directory utility doesn't understand this concept. Expand \"groups\" and ensure the following for each record type. You can click the \"+\" to add the attribute types as needed. Attribute Mapping PrimaryGroupID gidNumber RecordName cn Click the \"+\" to add a users record type or scroll and find \"users\". Select \"users\" and ensure the following object classes exist. You can click the \"+\" to add them when needed. Record Type ObjectClasses Users inetOrgPerson posixAccount shadowAccount apple-user Expand \"users\" and ensure the following for each record type. You can click the \"+\" to add the attribute types as needed. Do not set homeDirectory otherwise you will fail to login. Attribute Mapping AuthenticationAuthority uid GeneratedUID GeneratedUID or ipaUniqueID HomeDirectory #/Users/\\$uid\\$ NFSHomeDirectory #/Users/\\$uid\\$ PrimaryGroupID gidNumber RealName cn RecordName uid UniqueID uidNumber UserShell loginShell AltSecurityIdentities #Kerberos:\\$krbPrincipalName\\$ If using custom mapping, click reach record type you created and ensure the base DN is set. Make sure each record type is set to all subtrees. Click \"security\" and set an authentication bind DN if needed Click OK Click OK Click on Search Policy. Double check that \"/LDAPV3/server1.ipa.example.com\" is listed beneath \"/Local/Default\" Close everything until you're back to the users & groups section of preferences Open a terminal. % dscacheutil -flushcache % dscacheutil -q user -a name username You should get a return. If you want to further verify users and groups after the above succeeds, open up the directory utility again. Click \"Directory Editor\", ensure you are searching for \"users\" and check that they appear in a list on the right hand side, optionally doing a search. In a default setup, you shouldn't need an account to do (some) anonymous lookups. If you changed that in any way, you will need to create a readonly system account in cn=sysaccounts,cn=etc. Login to the account for the first time from the login screen. Once the setup has complete, log out and back to a login account. In a terminal, you will need to make a mobile account. 2 % sudo /System/Library/CoreServices/ManagedClient.app/Contents/Resources/createmobileaccount -n username -P # Press enter and put in the password. sudo may not function if you don't do this step. # OPTIONAL: Allow the mobile account to be an administrator % sudo dscl . -append /Groups/admin GroupMembership username Go to system preferences, users & groups and ensure the account is a mobile account.","title":"Monterey and older"},{"location":"el/freeipa/#ventura-and-likely-newer","text":"Go to system preferences -> users & groups Set \"automatic login\" to \"off\" Click \"edit\" next to \"Network account server\" Type in one of your IPA servers (you can duplicate it later for backup purposes). Press enter and wait for it to be \"green\". Click \"Open Directory Utility\" Click the \"lock\" to unlock the utility Click \"LDAPv3\" and click the pencil at the bottom left corner Select the \"from server\" portion under LDAP mappings and clck RFC2307. You may also leave it as custom. If you select rfc2307, it will ask for your base DN (eg, dc=ipa,dc=example,dc=com) If you select \"custom\", you will need to do this manually for each record type. You're better off using rfc2307 and working from there Click \"edit\" Click the \"+\" to add a groups record type or scroll and find \"groups\" and select it. Add the following object classes Record Type ObjectClasses Groups posixGroup ipausergroup groupOfNames\\* Note \"groupOfNames\" is optional here, because it seems that the directory utility doesn't understand this concept. Expand \"groups\" and ensure the following for each record type. You can click the \"+\" to add the attribute types as needed. Attribute Mapping PrimaryGroupID gidNumber RecordName cn Click the \"+\" to add a users record type or scroll and find \"users\". Select \"users\" and ensure the following object classes exist. You can click the \"+\" to add them when needed. Record Type ObjectClasses Users inetOrgPerson posixAccount shadowAccount apple-user Expand \"users\" and ensure the following for each record type. You can click the \"+\" to add the attribute types as needed. Do not set homeDirectory otherwise you will fail to login. Attribute Mapping AuthenticationAuthority uid GeneratedUID GeneratedUID or ipaUniqueID NFSHomeDirectory #/Users/\\$uid\\$ PrimaryGroupID gidNumber RealName cn RecordName uid UniqueID uidNumber UserShell loginShell AltSecurityIdentities #Kerberos:\\$krbPrincipalName\\$ If using custom mapping, click reach record type you created and ensure the base DN is set. Make sure each record type is set to all subtrees if needed. Click \"security\" and set an authentication bind DN if needed Click OK. Click Search Policy Double check that \"/LDAPV3/server1.ipa.example.com\" is listed beneath \"/Local/Default\". If it is not, select \"search patch\" and set it to custom and add it. Click Apply after. Close everything until you're back to the users & groups section of preferences Go to Lock Screen. Set \"login window shows\" to \"name and password\" Open a terminal. % dscacheutil -flushcache % dscacheutil -q user -a name username You should get a return. Login to the account for the first time from the login screen. Once the setup has complete, log out and back to a login account. In a terminal, you will need to make a mobile account. 3 % sudo /System/Library/CoreServices/ManagedClient.app/Contents/Resources/createmobileaccount -n username -P # Press enter, enter the user's password. sudo may hang if you don't do this. # OPTIONAL: Allow the mobile account to be an administrator % sudo dscl . -append /Groups/admin GroupMembership username Go to system preferences and ensure the account is a mobile account.","title":"Ventura and likely newer"},{"location":"el/freeipa/#general-macos-notes","text":"Group Resolution If you want groups from IPA to resolve to the system, you'll need to enable the compat tree when using this setup (RFC2307). Password Notes There are a couple of potential issues with this setup that you should be aware of as it pertains to mobile accounts. If you do a mobile account, changing your password through the FreeIPA gui does not change your passwords on your system. If your account does not have any keytabs (eg, you haven't had your mac on or haven't logged in in over 24 hours), you can login with the new password and it will suceed. The system will cache the new password right away. However, your keychain the first time will ask for the old passwords and this is normal. So you can change them by hand or you can log out and back in and the system will ask you if you want to update the password and it will just update automatically. There have been reports in a github issue that states you can change the password in the system preferences but I've been unable to confirm this. Below is a script that can be adapted for you. It has not been tested on Monterey and up. This assumes that you took one mac and set it up properly and you created a tarball with the proper configuration. You could optionally setup a temporary NFS or samba mount that gets mounted as root and then unmounted at the end, if you so wish. #!/bin/bash serverName=server1.ipa.example.com krb5Conf=/etc/krb5.conf krb5Tab=/etc/krb5.keytab pamDirectory=/etc/pam.d # Add SSL cert to chain mkdir /etc/ipa cd /etc/ipa curl -OL http://$serverName/ipa/config/ca.crt security add-trusted-cert -d -k /Library/Keychains/System.keychain -r trustRoot /etc/ipa/ca.crt # Stop and flushout the Open Directory /usr/sbin/dscacheutil -flushcache launchctl unload /System/Library/LaunchDaemons/com.apple.opendirectoryd.plist # Pull the plist and pam files needed for IPA and deploy them, this assumes you setup one mac and zipped up the configurations # You can try your hand at dsconfigldap before pam, but I could never figure it out, honestly. # Relevant tar: tar czf /tmp/macconfig.tar.gz /Library/Preferences/OpenDirectory/Configurations /etc/pam.d/authorization \\ # /etc/pam.d/screensaver /etc/pam.d/passwd /etc/krb5.conf cd /tmp curl -OL http://$serverName/macconfig.tar.gz cd / tar xzf /tmp/macconfig.tar.gz # Add steps here for your keytab! Where are you getting it from? cp /tmp/mac.keytab /etc/krb5.keytab chown root:wheel /etc/krb5.keytab chmod 600 /etc/krb5.keytab # Start directory launchctl load /System/Library/LaunchDaemons/com.apple.opendirectoryd.plist sleep 30 # Kill the loginwindow killall loginwindow # If the system doesn't reboot here, reboot now. If you want to move your local files, you will need to tread lightly here. I personally believe it's always good to start fresh though. Look into the ditto command. I suppose something like this can work: # make sure you're logged in as a different account away from your local account % sudo su - root# cd /Users root# ditto localfolder networkfolder (or maybe an mv?) root# chown -R user:user folder root# /System/Library/CoreServices/ManagedClient.app/Contents/Resources/createmobileaccount -n username -P Another issue you may run into, if you have been using your Mac with a local account for a while, a lot of directories in /Applications will be owned by localuser:staff or localuser:admin. It's recommended to fix those too. Discovery The directory framework in MacOS has the ability to discover settings for a particular LDAP server that it is being connected to. FreeIPA does not contain the schema, plugins, nor the infrastructure to provide the same things (for example, mDNS/Avahi, among other things). There was a (WIP) plugin created in 2017 by abbra. However, it is unclear if this works at all, nor is it clear if it ever did and will in python3 (abbra noted at the time that it \"installs\" into python 2 directories, which hints to not being tested or working on python 3). Please see the following resources for discussion and information. Pagure freeipa-macosx-support","title":"General macOS Notes"},{"location":"el/freeipa/#suse","text":"To setup openSUSE with FreeIPA, we'll need to do some manual work. This applies to SUSE 12 and up where the freeipa-client packages don't exist in the main repositories. freeipa repos There are OpenSUSE repos with the freeipa packages, though they are considered \"experimental\". If they show up in the base, then the below steps will be removed. However, if you are willing to use the repo , a lot of the steps below may not be needed. We have not tested this. # On an IPA server or client with the IPA utilities... % ipa host-add suse.example.com % /usr/sbin/ipa-getkeytab -s ipa.example.com -p host/suse.example.com -k /tmp/suse.keytab % scp /tmp/suse.keytab suse.example.com:/tmp/krb5.keytab # On the IPA client... % cp /tmp/krb5.keytab /etc % chmod 600 /etc/krb5.keytab % mkdir /etc/ipa % curl -o /etc/ipa/ca.crt http://ipa.example.com/ipa/config/ca.crt % curl -o /etc/pki/trust/anchors/ipa.example.com.crt http://ipa.example.com/ipa/config/ca.crt % update-ca-certificates % zypper install sssd sssd-ipa yast2-auth-client krb5-client openldap2-client cyrus-sasl-gssapi # Setup SSSD % vi /etc/sssd/sssd.conf [domain/example.com] cache_credentials = True krb5_store_password_if_offline = True ipa_domain = example.com ipa_hostname = suse.example.com # Client Specific Settings ipa_server = _srv_, ipa.example.com dns_discovery_domain = example.com # If we have a trust with domain resolution order #full_name_format = %1$s id_provider = ipa auth_provider = ipa access_provider = ipa chpass_provider = ipa ldap_tls_cacert = /etc/ipa/ca.crt [sssd] services = nss, sudo, pam, ssh domains = example.com [nss] filter_users = root,ldap,named,avahi,haldaemon,dbus,radiusd,news,nscd,tomcat,postgres homedir_substring = /home [pam] [sudo] [autofs] [ssh] # Setup kerberos % vi /etc/krb5.conf [libdefaults] default_realm = EXAMPLE.COM dns_lookup_realm = true dns_lookup_kdc = true rdns = false dns_canonicalize_hostname = false ticket_lifetime = 24h forwardable = true udp_preference_limit = 0 default_ccache_name = KEYRING:persistent:%{uid} [realms] EXAMPLE.COM = { pkinit_anchors = FILE:/var/lib/ipa-client/pki/kdc-ca-bundle.pem pkinit_pool = FILE:/var/lib/ipa-client/pki/ca-bundle.pem } [domain_realm] .example.com = EXAMPLE.COM example.com = EXAMPLE.COM suse.example.com = EXAMPLE.COM # Setup pam % pam-config -a --sss --mkhomedir --mkhomedir-umask=0077 \\ --pwhistory --pwhistory-remember=5 --localuser --cracklib \\ --cracklib-minlen=14 --cracklib-dcredit=-1 --cracklib-ucredit=-1 \\ --cracklib-lcredit=-1 --cracklib-ocredit=-1 --cracklib-retry=3 --unix-sha512 # Setup nsswitch (you can make it compat sss, but I use files sss) % sed -i.bak 's/compat$/files sss/g' /etc/nsswitch.conf % echo \"sudoers: files sss\" >> /etc/nsswitch.conf % sed -i '/netgroup/ s/nis/sss/g' /etc/nsswitch.conf # Depending on your suse version, you may want to set the nisdomainname # It does not hurt to set this % sed -i.bak '/NETCONFIG_NIS_STATIC_DOMAIN/ s/\"\"/\"example.com\"/g' /etc/sysconfig/network/config % netconfig update -f # Start sssd % systemctl enable sssd --now # Verify % id admin In the case of having an IPA-AD trust, you may need to change a line in your pam configuration. % sed -i 's/use_first_pass/forward_pass/g' /etc/pam.d/common-auth-pc # The affected line should appear like the below auth sufficient pam_sss.so forward_pass","title":"SUSE"},{"location":"el/freeipa/#hbac","text":"When we first setup our IPA servers, we had an option set to make it so hbac wasn't allowed for everyone. This way we have to create HBAC rules for our systems. I personally do this out of habit when working with IPA. What we need to do though is create an \"admin\" group that can login to all machines. % ipa idrange-show IPA.EXAMPLE.COM_id_range Range name: IPA.EXAMPLE.COM_id_range First Posix ID of the range: 686600000 Number of IDs in the range: 200000 First RID of the corresponding RID range: 1000 First RID of the secondary RID range: 100000000 Range type: local domain range % ipa group-add --gid=686610000 linuxadm % ipa group-add-member --users=flast linuxadm Note for AD Users : In the event that your AD user or group of users will be an admin, you need to create an \"external\" group to map the user or users over. This isn't required if you don't have an AD trust. # Create an external group that the AD user/group goes into % ipa group-add --external linuxadm_external # Add the user (or group) into the external group % ipa group-add-member --users=aduser1@example.com linuxadm_external % ipa group-add-member --users=adgroup1@example.com linuxadm_external # Add the external group as a member of the IPA posix group. # aduser1 and adgroup1 are now effectively members of the linuxadm group in IPA. % ipa group-add-member --groups=linuxadm_external linuxadm Now, let's create an HBAC for our Linux Administrator account for our group. % ipa hbacrule-add --hostcat=all --servicecat=all --desc='linux admins all access' linuxadm % ipa hbacrule-add-user --groups=linuxadm linuxadm % ipa hbactest --rules=All_Systems --user=flast --host=server1.ipa.example.com --service=sshd % ipa hbactest --rules=All_Systems --user=aduser1@example.com --host=server1.ipa.example.com --service=sshd You might want to create an HBAC rule specifically for your IPA admin accounts to have ssh access to the IPA servers too. You can follow something like the above to make it possible. Or you can just add the IPA admins group into the HBAC rule we just made above. Group Types Groups in Active Directory have three types. These three types can actually change the behavior of how SSSD on the IPA domain controllers resolve them or if they'll even be resolvable at all. The three types are 'Domain Local', 'Global', and 'Universal'. If at all possible, avoid groups being 'Global'. Domain Local or Universal is recommended.","title":"HBAC"},{"location":"el/freeipa/#sudo","text":"Setting up sudo is relatively easy. SSSD (1.16.x and 2.X) supports IPA as a provider for sudo. Based on the last section, let's create a sample rule for our Linux admins that can login to every system, we want to ensure they can run all commands. % ipa sudorule-add --runasusercat=all --hostcat=all --cmdcat=all --desc='linux admins all sudo' all_linux_sudo % ipa sudorule-add-user --groups=linuxadm all_linux_sudo You can make this a little more specific, such as /bin/bash as everyone or otherwise. It's your call here. If you want to create a sudo rule and add some commands to it, you can do something like this. % ipa sudorule-add sudo_rule % ipa sudorule-add-allow-command --sudocmds=\"/usr/bin/less\" sudo_rule","title":"SUDO"},{"location":"el/freeipa/#legacy-client-setup","text":"This applies to Solaris, Omnios, others based on Illumos.","title":"Legacy Client Setup"},{"location":"el/freeipa/#solaris-10","text":"Setting up Solaris 10 as an IPA client is an interesting feat. However, it comes with security issues. No SSL or TLS Support Note that for Solaris 10 to talk to IPA, you must use clear text communication. Solaris 10 is too old to use new ciphers. However, while LDAP may be clear text, kerberos should still be secure enough for the time being. If you are using an AD trust, the user's passwords will be passed in clear text. Highly suggested that you decommission Solaris 10 from your environment. Solaris 10 will eventually be removed from this page. Create an ldif for your service account (optional) dn: uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com objectclass: account objectclass: simplesecurityobject uid: solaris userPassword: secret123 passwordExpirationTime: 20380119031407Z nsIdleTimeout: 0 The solaris system account is required. So now, add it in. % ldapadd -xWD 'cn=Directory Manager' -f /tmp/solaris.ldif Now, set the nisdomain. % defaultdomain ipa.example.com % echo 'ipa.example.com' > /etc/defaultdomain Configure kerberos. % vi /etc/krb5/krb5.conf [libdefaults] default_realm = IPA.EXAMPLE.COM dns_lookup_kdc = true verify_ap_req_nofail = false [realms] IPA.EXAMPLE.COM = { } [domain_realm] ipa.example.com = IPA.EXAMPLE.COM .ipa.example.com = IPA.EXAMPLE.COM [logging] default = FILE:/var/krb5/kdc.log kdc = FILE:/var/krb5/kdc.log kdc_rotate = { period = 1d version = 10 } [appdefaults] kinit = { renewable = true forwardable= true } Generate a keytab and bring it over. # on the ipa server % ipa host-add solaris10.example.com % ipa-getkeytab -s server1.ipa.example.com -p host/solaris10.example.com -k /tmp/solaris10.keytab # Transfer the keytab % scp /tmp/solaris10.keytab solaris10.example.com:/tmp # On the solaris 10 machine % cp /tmp/solaris10.keytab /etc/krb5/krb5.keytab % chmod 600 /etc/krb5/krb5.keytab % chmod 644 /etc/krb5/krb5.conf % chown root:sys /etc/krb5/* % kinit flast2@IPA.EXAMPLE.COM Create the LDAP configurations, bring the certificate, and create an NSS database. % mkdir /etc/ipa /var/ldap % cd /etc/ipa % wget -O ipa.pem http://server1.ipa.example.com/ipa/config/ca.crt % certutil -A -n \"ca-cert\" -i /etc/ipa/ipa.pem -a -t CT -d . % cp * /var/ldap % vi /etc/ldap.conf base dc=ipa,dc=example,dc=com scope sub TLS_CACERTDIR /var/ldap TLS_CERT /var/ldap/cert8.db TLS_CACERT /var/ldap/ipa.pem tls_checkpeer no ssl off bind_timelimit 120 timelimit 120 uri ldap://server1.ipa.example.com sudoers_base ou=sudoers,dc=ipa,dc=example,dc=com pam_lookup_policy yes Now init the ldap client. No Secure Connection When using this, you are not creating a secure connection. The Solaris 10 SSL libraries are so old that they cannot work with the ciphers that FreeIPA has turned on. AD Trust - Different Trees If using an AD trust, you should use the second example, where it looks at the compat tree for users. No Service Account If you have configured FreeIPA to not allow any anonymous connections, you will need to use a proxy account. We have provided the examples for this configuration. Without an AD Trust # Without AD Trust (no proxy) % ldapclient manual -a authenticationMethod=none \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com \\ -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 # Without AD Trust (proxy) % ldapclient manual -a credentialLevel=proxy \\ -a authenticationMethod=simple \\ -a proxyDN=\"uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com\" \\ -a proxyPassword=\"secret123\" \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com \\ -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 With an AD Trust # With AD Trust (no proxy) % ldapclient manual -a authenticationMethod=none \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com \\ -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 # With AD Trust (proxy) % ldapclient manual -a credentialLevel=proxy \\ -a authenticationMethod=simple \\ -a proxyDN=\"uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com\" \\ -a proxyPassword=\"secret123\" \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com \\ -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 This should succeed. Once it succeeds, you need to configure pam and nsswitch. AD Trust Information In the event you don't have an AD trust, you can change the \"binding\" lines to required, remove the pam_ldap lines, and change pam_krb5 lines to read \"required\" % vi /etc/pam.conf # Console login auth requisite pam_authtok_get.so.1 login auth sufficient pam_krb5.so.1 login auth required pam_unix_cred.so.1 login auth required pam_dial_auth.so.1 login auth sufficient pam_unix_auth.so.1 server_policy login auth sufficient pam_ldap.so.1 rlogin auth sufficient pam_rhosts_auth.so.1 rlogin auth requisite pam_authtok_get.so.1 rlogin auth required pam_dhkeys.so.1 rlogin auth sufficient pam_krb5.so.1 rlogin auth required pam_unix_cred.so.1 rlogin auth sufficient pam_unix_auth.so.1 server_policy rlogin auth sufficient pam_ldap.so.1 # Needed for krb krlogin auth required pam_unix_cred.so.1 krlogin auth sufficient pam_krb5.so.1 # Needed for krb krsh auth required pam_unix_cred.so.1 krsh auth required pam_krb5.so.1 # ? ppp auth requisite pam_authtok_get.so.1 ppp auth required pam_dhkeys.so.1 ppp auth sufficient pam_krb5.so.1 ppp auth required pam_dial_auth.so.1 ppp auth binding pam_unix_auth.so.1 server_policy ppp auth sufficient pam_ldap.so.1 # Other, used by sshd and \"others\" as a fallback other auth requisite pam_authtok_get.so.1 other auth required pam_dhkeys.so.1 other auth sufficient pam_krb5.so.1 other auth required pam_unix_cred.so.1 other auth sufficient pam_unix_auth.so.1 server_policy other auth sufficient pam_ldap.so.1 other account requisite pam_roles.so.1 other account required pam_projects.so.1 other account binding pam_unix_account.so.1 server_policy other account sufficient pam_krb5.so.1 other account sufficient pam_ldap.so.1 other session required pam_unix_session.so.1 other password required pam_dhkeys.so.1 other password requisite pam_authtok_get.so.1 other password requisite pam_authtok_check.so.1 force_check other password required pam_authtok_store.so.1 server_policy # passwd and cron passwd auth binding pam_passwd_auth.so.1 server_policy passwd auth sufficient pam_ldap.so.1 cron account required pam_unix_account.so.1 # SSH Pubkey - Needed for openldap and still probably needed sshd-pubkey account required pam_unix_account.so.1 % vi /etc/nsswitch.conf # Below are just the minimum changes passwd: files ldap [NOTFOUND=return] group: files ldap [NOTFOUND=return] sudoers: files ldap netgroup: ldap # the rest here are just here, up to you if you choose to set them. hosts: files dns ipnodes: files dns ethers: files ldap publickey: files ldap automount: files ldap You can test now if you'd like. bash-3.2# ldaplist -l passwd flast2 dn: uid=flast2,cn=users,cn=compat,dc=ipa,dc=example,dc=com cn: First Last objectClass: posixAccount objectClass: ipaOverrideTarget objectClass: top gidNumber: 1006800001 gecos: First Last uidNumber: 1006800001 ipaAnchorUUID: :IPA:ipa.example.com:8babb9a8-5aaf-11e7-9769-00505690319e loginShell: /bin/bash homeDirectory: /home/first.last2 uid: first.last2 I recommend setting up sudo at least... if you want to use sudo, install the sudo-ldap from sudo.ws for Solaris 10.","title":"Solaris 10"},{"location":"el/freeipa/#solaris-11","text":"Solaris 11 shares similar configuration to Solaris 10. There are a couple of manual things we have to do, but they are trivial. Solaris 11/Omnios will use TLS and sudo should just work. AD Groups In Solaris 10, users who logged in with AD users (with their short name) would appear as their full name ( name@domain ). This allowed their groups to fully resolve. However, in Solaris 11.4, this was not the case. Short name logins will work but your groups will not resolve as the compat tree uses the full name. To avoid running into this problem, you should be on at least SRU 11.4.7.4.0. Note that on a later SRU, you may need to setup an ID view (without overrides) for groups and sudo to work again. Below is for the service account like in the previous section, here as a reference. dn: uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com objectclass: account objectclass: simplesecurityobject uid: solaris userPassword: secret123 passwordExpirationTime: 20380119031407Z nsIdleTimeout: 0 % ldapadd -xWD 'cn=Directory Manager' -f /tmp/solaris.ldif Now, set the nisdomain. % defaultdomain ipa.example.com % echo 'ipa.example.com' > /etc/defaultdomain Configure kerberos. % vi /etc/krb5/krb5.conf [libdefaults] default_realm = IPA.EXAMPLE.COM dns_lookup_kdc = true verify_ap_req_nofail = false [realms] IPA.EXAMPLE.COM = { } [domain_realm] ipa.example.com = IPA.EXAMPLE.COM .ipa.example.com = IPA.EXAMPLE.COM [logging] default = FILE:/var/krb5/kdc.log kdc = FILE:/var/krb5/kdc.log kdc_rotate = { period = 1d version = 10 } [appdefaults] kinit = { renewable = true forwardable= true } Generate a keytab and bring it over. # on the ipa server % ipa host-add solaris11.example.com % ipa-getkeytab -s server1.ipa.example.com -p host/solaris11.example.com -k /tmp/solaris11.keytab # Transfer the keytab % scp /tmp/solaris11.keytab solaris11.example.com:/tmp # On the solaris 11 machine % cp /tmp/solaris11.keytab /etc/krb5/krb5.keytab % chmod 600 /etc/krb5/krb5.keytab % chmod 644 /etc/krb5/krb5.conf % chown root:sys /etc/krb5/* # Check the keytab % klist -ket /etc/krb5/krb5.keytab # Test that you can kinit % kinit flast2@IPA.EXAMPLE.COM Create the LDAP configurations, bring the certificate, and create an NSS database. Solaris 11.3 vs 11.4 Previously we had 11.3 and 11.4 configurations. We have removed 11.3 as we no longer support it. % mkdir /etc/ipa /var/ldap % cd /etc/ipa % wget -O ipa.pem http://server1.ipa.example.com/ipa/config/ca.crt % cp * /var/ldap % vi /etc/ldap.conf base dc=ipa,dc=example,dc=com scope sub bind_timelimit 120 timelimit 120 uri ldap://server1.ipa.example.com sudoers_base ou=sudoers,dc=ipa,dc=example,dc=com pam_lookup_policy yes TLS_CACERTDIR /var/ldap ssl start_tls tls_checkpeer no Now init the ldap client. We actually get to use a secure connection here. Kerberos is hit or miss, could never get sasl/GSSAPI to work. Different Trees - Trust or not? There are multiple examples of how to setup the trees. If using an AD trust, you should use the second example, where it looks at the compat tree for users. However, if you do not have trusts, then it is perfectly possible to still use the AD Trust example. Try both and see which works better for your environment. No Service Account If you have configured FreeIPA to not allow any anonymous connections, you will need to use a proxy account. We have provided the examples for this configuration. Without AD Trust # Without AD Trust (no proxy) % ldapclient manual -a authenticationMethod=tls:simple \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 # Without AD Trust (proxy) % ldapclient manual -a authenticationMethod=tls:simple \\ -a credentialLevel=proxy \\ -a proxyDN=\"uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com\" \\ -a proxyPassword=\"secret123\" \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com \\ -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 # Without AD Trust (Kerberos) - Only works if Solaris is in the same DNS domain as IPA % ldapclient manual -a authenticationMethod=sasl/GSSAPI \\ -a credentialLevel=self \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com \\ -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 With AD Trust # With AD Trust (no proxy) % ldapclient manual -a authenticationMethod=tls:simple \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 # With AD Trust (proxy) % ldapclient manual -a authenticationMethod=tls:simple \\ -a credentialLevel=proxy \\ -a proxyDN=\"uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com\" \\ -a proxyPassword=\"secret123\" \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com \\ -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 # With AD Trust (Kerberos) - Only works if Solaris is in the same DNS domain as IPA % ldapclient manual -a authenticationMethod=sasl/GSSAPI \\ -a credentialLevel=self \\ -a proxyDN=\"uid=solaris,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com\" \\ -a proxyPassword=\"secret123\" \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com \\ -a defaultServerList=\"server1.ipa.example.com server2.ipa.example.com\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=passwd:cn=users,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 This should succeed. Once it succeeds, you need to configure pam and nsswitch. % /usr/sbin/svccfg -s name-service/switch setprop config/sudoer = astring: \"files ldap\" % /usr/sbin/svccfg -s name-service/switch setprop config/password = astring: \"files ldap [NOTFOUND=return]\" % /usr/sbin/svccfg -s name-service/switch setprop config/group = astring: \"files ldap [NOTFOUND=return]\" % /usr/sbin/svcadm refresh svc:/system/name-service/switch % /usr/sbin/svcadm restart svc:/system/name-service/switch % /usr/sbin/svcadm restart ldap/client AD Trust Information In the event you don't have an AD trust, you can change the \"binding\" lines to required and remove the pam_ldap lines. Optionally, you can set pam_krb5 to \"required\", however sufficient should work just fine. Without an AD Trust % vi /etc/pam.d/login auth definitive pam_user_policy.so.1 auth requisite pam_authtok_get.so.1 auth required pam_dhkeys.so.1 auth sufficient pam_krb5.so.1 auth required pam_unix_cred.so.1 auth sufficient pam_unix_auth.so.1 server_policy % vi /etc/pam.d/other auth definitive pam_user_policy.so.1 auth requisite pam_authtok_get.so.1 auth required pam_dhkeys.so.1 auth sufficient pam_krb5.so.1 auth required pam_unix_cred.so.1 auth sufficient pam_unix_auth.so.1 server_policy account requisite pam_roles.so.1 account definitive pam_user_policy.so.1 account required pam_unix_account.so.1 server_policy account sufficient pam_krb5.so.1 session definitive pam_user_policy.so.1 session required pam_unix_session.so.1 password definitive pam_user_policy.so.1 password include pam_authtok_common password sufficient pam_krb5.so.1 password required pam_authtok_store.so.1 server_policy % vi /etc/pam.d/sshd-pubkey account required pam_unix_account.so.1 With an AD Trust % vi /etc/pam.d/login auth definitive pam_user_policy.so.1 auth requisite pam_authtok_get.so.1 auth required pam_dhkeys.so.1 auth sufficient pam_krb5.so.1 auth required pam_unix_cred.so.1 auth sufficient pam_unix_auth.so.1 server_policy auth sufficient pam_ldap.so.1 % vi /etc/pam.d/other auth definitive pam_user_policy.so.1 auth requisite pam_authtok_get.so.1 auth required pam_dhkeys.so.1 auth sufficient pam_krb5.so.1 auth required pam_unix_cred.so.1 auth sufficient pam_unix_auth.so.1 server_policy auth sufficient pam_ldap.so.1 account requisite pam_roles.so.1 account definitive pam_user_policy.so.1 account binding pam_unix_account.so.1 server_policy account sufficient pam_krb5.so.1 account sufficient pam_ldap.so.1 session definitive pam_user_policy.so.1 session required pam_unix_session.so.1 password definitive pam_user_policy.so.1 password include pam_authtok_common password sufficient pam_krb5.so.1 password required pam_authtok_store.so.1 server_policy % vi /etc/pam.d/sshd-pubkey account required pam_unix_account.so.1 You can test now if you'd like. root@solaris11:~# ldaplist -l passwd flast2 dn: uid=flast2,cn=users,cn=compat,dc=ipa,dc=example,dc=com cn: First Last objectClass: posixAccount objectClass: ipaOverrideTarget objectClass: top gidNumber: 1006800001 gecos: First Last uidNumber: 1006800001 ipaAnchorUUID: :IPA:ipa.example.com:8babb9a8-5aaf-11e7-9769-00505690319e loginShell: /bin/bash homeDirectory: /home/first.last2 uid: first.last2","title":"Solaris 11"},{"location":"el/freeipa/#automated-scripts","text":"I at one point built a bunch of scripts to automate Solaris servers talking to IPA here . However, it is likely the scripts no longer work or contain outdated information.","title":"Automated Scripts"},{"location":"el/freeipa/#ad-trust-double-uid","text":"Solaris 11 once in a while gets random regressions when it comes to authentication and ID's, among many other things they randomly decide to break. Big shout out to Oracle. In a brief discussion with a user in the #freeipa IRC channel, the user was trying to find a way to chop off the domain name for logins but also have sudo still work as there were some random issues in general. We both discovered that in SRU 11.4.20.4.0, even though both UID's are present from ldaplist -l passwd, sudo was no longer working properly. The first thing we tried was to create an ID view and override a user with a new username. This successfully removed the domain, but did not solve the sudo problem. He instead got \"no account present for that user\". However, I wasn't able to replicate this. However, later, one thing he noticed is after creating an ID view with no overrides and pointing Solaris 11 to the view in the compat tree, Solaris 10-esque authentication ID reporting started to occur. Running ldaplist -l passwd user reported back the double UID as expected, but the FQDN comes first which resolved his group/sudo issues. # Create a view... no id overrides required here % ipa idview-add solaris # On Solaris... # Take EXTREME care with the group and passwd base DN's, they need to point # to the view properly # This example uses kerberos to authenticate. % ldapclient manual -a authenticationMethod=self \\ -a credentialLevel=sasl/GSSAPI \\ -a defaultSearchBase=dc=ipa,dc=example,dc=com \\ -a domainName=ipa.example.com \\ -a defaultServerList=\"server1.angelsofclockwork.net server2.angelsofclockwork.net\" \\ -a followReferrals=true \\ -a objectClassMap=shadow:shadowAccount=posixAccount \\ -a objectClassMap=passwd:posixAccount=posixaccount \\ -a objectClassMap=group:posixGroup=posixgroup \\ -a serviceSearchDescriptor=group:cn=groups,cn=solaris,cn=views,cn=compat,dc=angelsofclockwork,dc=net \\ -a serviceSearchDescriptor=passwd:cn=users,cn=solaris,cn=views,cn=compat,dc=angelsofclockwork,dc=net \\ -a serviceSearchDescriptor=netgroup:cn=ng,cn=compat,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=ethers:cn=computers,cn=accounts,dc=ipa,dc=example,dc=com \\ -a serviceSearchDescriptor=sudoers:ou=sudoers,dc=ipa,dc=example,dc=com \\ -a bindTimeLimit=5 # Make sure you set your props... % /usr/sbin/svccfg -s name-service/switch setprop config/sudoer = astring: \"files ldap\" % /usr/sbin/svccfg -s name-service/switch setprop config/password = astring: \"files ldap [NOTFOUND=return]\" % /usr/sbin/svccfg -s name-service/switch setprop config/group = astring: \"files ldap [NOTFOUND=return]\" % /usr/sbin/svcadm refresh svc:/system/name-service/switch % /usr/sbin/svcadm restart svc:/system/name-service/switch % /usr/sbin/svcadm restart ldap/client # Verify... % ldaplist -l passwd adusername . . . % id -a adusername . . . Thank you to \"mewho\" on libera for finding this interesting workaround.","title":"AD Trust Double UID"},{"location":"el/freeipa/#omniosillumos","text":"Some steps between Solaris 10 and 11 can be followed to make OmniOS work. However, we have been unable to resolve why sudo will not work when using an AD trust. If you are using a standalone FreeIPA and no trust, sudo should work just fine.","title":"OmniOS/Illumos"},{"location":"el/freeipa/#legacy-hbac","text":"For HBAC to work on Solaris, you will need to compile the pam_hbac module found here . I would clone the current master branch or download the master.zip to your Solaris system. Each OS has their set of instructions for compiling. First, create the following system account. We will need this when we are configuring our legacy clients. dn: uid=hbac,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com objectClass: account objectClass: simplesecurityobject objectClass: top uid: hbac userPassword: password","title":"Legacy HBAC"},{"location":"el/freeipa/#solaris-10_1","text":"% /opt/csw/bin/pkgutil -i -y libnet ar binutils gcc4g++ glib2 libglib2_dev gmake % /opt/csw/bin/pkgutil -i -y libnet ar binutils gcc4g++ glib2 libglib2_dev gmake % PATH=$PATH:/opt/csw/bin % export M4=/opt/csw/bin/gm4 % autoconf -o configure % autoreconf -i # Yes, SSL must be disabled for Solaris 10 to work. The libraries are too old. # You may or may not need to set CFLAGS, CXXFLAGS, and LDFLAGS with -m32 % ./configure AR=/opt/csw/bin/gar --with-pammoddir=/usr/lib/security --sysconfdir=/etc/ --disable-ssl --disable-man-pages % make % make install","title":"Solaris 10"},{"location":"el/freeipa/#solaris-11_1","text":"% pkg install autoconf libtool pkg-config automake gcc docbook % autoreconf -if % ./configure --with-pammoddir=/usr/lib/security --mandir=/usr/share/man --sysconfdir=/etc/ % make % make install","title":"Solaris 11"},{"location":"el/freeipa/#omnios","text":"% pkg install developer/build/autoconf developer/build/libtool \\ developer/pkg-config developer/build/automake \\ developer/gcc48 system/header developer/object-file \\ developer/linker % autoreconf -if % ./configure --with-pammoddir=/usr/lib/security --mandir=/usr/share/man --sysconfdir=/etc/ % make % make install","title":"Omnios"},{"location":"el/freeipa/#pam_hbacconf","text":"% vim /etc/pam_hbac.conf # Replace client with your server's FQDN URI = ldap://server.ipa.example.com BASE = dc=ipa,dc=example,dc=com BIND_DN = uid=hbac,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com BIND_PW = password SSL_PATH = /var/ldap HOST_NAME = client","title":"pam_hbac.conf"},{"location":"el/freeipa/#pam-configuration","text":"# Solaris 10 - /etc/pam.conf # Modify the other account section... It should come at the end of the account blocks. . . . other account required pam_hbac.so ignore_unknown_user ignore_authinfo_unavail # Solaris 11 - /etc/pam.d/other # Same here, only modify the account section . . . account required pam_hbac.so ignore_unknown_user ignore_authinfo_unavail In the event you cannot login or things aren't working the way you'd expect, add 'debug' to the end of the pam_hbac line and watch /var/log/authlog for errors.","title":"PAM Configuration"},{"location":"el/freeipa/#login-with-ad-users-to-legacy-clients","text":"For AD users to be able to login to legacy clients, you have to enable system-auth to the IPA servers. Without it, users will be denied access, regardless of HBAC controls or if you're using the pam_hbac module. % ipa hbacsvc-add system-auth % ipa hbacrule-add legacy_client_auth % ipa hbacrule-add-host --hostgroups=ipaservers legacy_client_auth % ipa hbacrule-mod --usercat=all legacy_client_auth","title":"Login with AD Users to Legacy Clients"},{"location":"el/freeipa/#legacy-active-directory-trust-notes","text":"Just a section of notes.","title":"Legacy Active Directory Trust Notes"},{"location":"el/freeipa/#domain-resolution-order-oddness","text":"If using domain resolution order, AD users get double uid attributes - but only if they login with their shortname. If they login with fqdn, double uid's do not occur. But shortnames do not work anymore. Have to restart the directory server to make short names work again.","title":"Domain Resolution Order Oddness"},{"location":"el/freeipa/#solaris-weirdness","text":"If using domain resolution order, Solaris 10 gets the group resolution correct for short named AD users. Solaris 11 does not unless you are on SRU 11.4.7.4.0 or newer. There is a way to chop off the domain name from the uid using views.","title":"Solaris Weirdness"},{"location":"el/freeipa/#domain-options","text":"This section goes over \"situational\" scenarios. These scenarios are reflective of the environment in which IPA is installed and not all will fit into your environment. These are more or less common situations that could occur during an IPA deployment or even post-deployment.","title":"Domain Options"},{"location":"el/freeipa/#remove-realm-for-ad-users","text":"A common scenario is that IPA and AD will have a trust, but there will not be any IPA users with the exception of the engineering team for managing IPA itself. The common theme is that because of this, the engineers and customers would rather not login with username@realm . Info The following is only applicable in an IPA-AD trust. An IPA-only scenario would not require any of these steps and most pieces would work natively (no @realm, sudo, hbac). In the event that you are in an IPA-AD scenario, please take note that this can adversely affect legacy clients. This will cause ldapsearches that are done in the compat tree to display multiple uid attributes. In most cases, this is fine and the user can still login without the realm name. The whoami and id commands will show the domain. There's no workaround for this. On the IPA servers, you will need to set the domain resolution order. This was introduced in 4.5.0. % kinit admin % ipa config-mod --domain-resolution-order=\"example.com:ipa.example.com\" After, you will need to clear out your SSSD cache. # sss_cache -E is insufficient for this. % systemctl stop sssd % rm -rf /var/lib/sss/db/* % systemctl start sssd The below is optional. It will remove the @realm off the usernames, like on the prompt or id or whoami commands. Only do this if required. Only do this on the clients. Do not make this change on an IPA replica. # vi /etc/sssd/sssd.conf [domain/ipa.example.com] . . . full_name_format = %1$s This will ensure EL7, EL8, EL9 clients resolve the AD domain first when attempting logins and optionally drop the @realm off the usernames.","title":"Remove @realm for AD users"},{"location":"el/freeipa/#ad-and-ipa-group-names-with-short-names","text":"You may notice that your clients have intermittent issues with name resolution when the following are true: Groups (or users) have the same names in both IPA and AD You are using domain resolution order You are shortening names on the clients You may want to actually search for them to identify the errant groups and then correct them. You can correct them either on the AD or IPA side. I would opt for the IPA side. % kinit admin@IPA.EXAMPLE.COM % vi /tmp/dupecheck.sh #!/bin/bash for x in ${ARRAY[*]} ; do ldapsearch -x -b \"DC=example,DC=com\" -h example.com -LLL -w 'PASSWORD' -D 'username@example.com' samaccountname=\"$x\" samaccountname | grep -q $x if [[ $? -eq 0 ]]; then echo \"$x: DUPLICATE\" fi done % bash /tmp/dupecheck.sh If you run into any duplicates, they should show up in a list for you address. sAMAccountName vs CN The \"CN\" and \"sAMAccountName\" attributes are not the same in AD, depending on who made the group or other factors. The sAMAccountName attribute is the value used to determine names from AD, whether you are enrolled with AD or the IPA server SSSD is pulling the information. This is why we are searching for that attribute, and not the CN.","title":"AD and IPA group names with short names"},{"location":"el/freeipa/#sites-and-ad-dcs","text":"By creating a subdomain section in /etc/sssd/sssd.conf on an IPA server, it is possible to set an AD Site or AD server(s) directly in SSSD. By default, sssd tries to do location based discovery. There may be a case where this isn't possible (eg, only a set of AD servers may only be contacted in certain \"air gapped\" networks). [domain/ipa.example.com/example.com] # If you want a site ad_site = Site_Name # If you want a server(s) ad_server = dc1.example.com, dc2.example.com # A backup? ad_backup_server = dc3.example.com, dc4.example.com If you don't have access or a way to find the sites using the Windows tools, you can run an ldapsearch to find it (or an equivalent ldap browsing tool). % ldapsearch -x -h example.com -s one -WD 'CN=username,CN=Users,DC=example,DC=com' \\ -b 'CN=Sites,CN=Configuration,DC=example,DC=com' cn This should report back your sites. If you want to know the servers for those sites (in case you don't want to deal with the sites, but just the DC's themselves), you use ldapsearch but use the base DN of the site name. % ldapsearch -x -h example.com -WD 'CN=username,CN=Users,DC=example,DC=com' \\ -b 'CN=Servers,CN=Site_Name,CN=Sites,CN=Configuration,DC=example,DC=com' dnsHostName Hardcoded DC's If the DC's change at any time and they are harded in your sssd.conf, it is up to you to know when new controllers are being added or removed as to not disrupt the connectivity from IPA to AD when performing user or group lookups.","title":"Sites and AD DC's"},{"location":"el/freeipa/#enterprise-linux-6-sudo-and-default-domain-suffix","text":"This issue with the above section is that once you do this, sudo rules will begin failing, they will no longer work for Enterprise Linux 6. This is because sssd was changed to look for cn=sudo rather than ou=sudoers. To enable the compatibility fall back, you will need to install a newer SSSD.","title":"Enterprise Linux 6 SUDO and Default Domain Suffix"},{"location":"el/freeipa/#set-default-shell-for-ad-users","text":"By default, after a trust has been established, the shell all AD users get is /bin/sh. To change this, you must change the sssd.conf on the IPA masters. % vi /etc/sssd/sssd.conf [domain/ipa.example.com] . . . default_shell = /bin/bash % systemctl restart sssd","title":"Set Default Shell for AD Users"},{"location":"el/freeipa/#automated-kerberos-principals","text":"Once in a great while, we run into situations where we need to have an automated process for creating principals and keytabs. This section takes a look at some of those examples that we've ran into.","title":"Automated Kerberos Principals"},{"location":"el/freeipa/#hadoopcloudera","text":"This assumes you are using Cloudera Manager and not Ambari in any form. DNS Information It is highly likely that if you are using AWS, your nodes are getting stupid names like compute.internal. While there is a a way to change this if you don't change it, you will need to rely on something like DNSMASQ to allow the nodes to communicate with FreeIPA. FreeIPA will be upset about the stupid names because it can't do a rDNS lookup.","title":"Hadoop/Cloudera"},{"location":"el/freeipa/#cloudera-manager-woes","text":"It is likely you have Cloudera/Hadoop, it is also very likely you (or another team) are deploying and using Cloudera Manager (or Director?). You may be running into issues that involve direct Active Directory integration. Maybe you're moving away from a standalone LDAP system over to Active Directory or even FreeIPA. Maybe you have FreeIPA in an AD trust but the users or contractors absolutely insist on using AD against their better judgement, despite the problems they're running into. Whatever the scenario is, we feel your pain. Here are some things you should probably know: Cloudera Manager (or Director?) supports Active Directory out of the box and obviously not FreeIPA despite the devs wanting to work something out back in 2015 Ambari has support for FreeIPA, but we are focusing on Cloudera Manager here. Cloudera Manager supports custom keytab retrieval scripts Hostnames that are longer than 15 characters, regardless of the cloud provider or onprem setup, will ultimately fail The NETBIOS limit in AD is 16 characters, which is 15 + $ at the end - This means hosts will enroll on top of themselves and your cluster will be broken FreeIPA does not have the name limitation and using an AD trust, AD users can freely use Hadoop when the cluster is properly setup. Enrolling the cluster nodes into FreeIPA and using a custom retrieval script will solve most (if not all) of the issues you may run into as well when it comes to keytabs, which Hadoop heavily relies on. The custom script is simply because Cloudera by default likes having direct access to the kerberos infrastructure, which is a no-go for FreeIPA.","title":"Cloudera Manager Woes"},{"location":"el/freeipa/#the-solution","text":"To summarize, here is our proposed solution: Create an account called cdh Create a role called \"Kerberos Managers\" and apply the following privileges: System: Manage Host Keytab System: Manage Host Keytab Permissions System: Manage Service Keytab System: Manage Service Keytab Permissions System: Manage User Principals (was not actually used, but who knows what we could use the role for later) Apply the role to the cdh account Create a custom script they could use to enroll the servers into FreeIPA (out of scope here) Create a custom script that utilizes the cdh account to create services So let's create the necessary things we need. # Create the account # Note... you may want to make this account non-expiring since it's just a service account % ipa user-add --first=\"Cloudera\" --last=\"Key Manager\" cdh # Create the Kerberos Managers role % ipa role-add \"Kerberos Managers\" # Create the kerberos manager privilege % ipa privilege-add \"Privileges - Kerberos Managers\" % ipa privilege-add-permission \"Privileges - Kerberos Managers\" \\ --privileges=\"System: Manage Host Keytab\" \\ --privileges=\"System: Manage Host Keytab Permissions\" \\ --privileges=\"System: Manage Service Keytab\" \\ --privileges=\"System: Manage Service Keytab Permissions\" \\ --privileges=\"System: Manage User Principals\" # Add the privilege to the role % ipa role-add-privilege \"Kerberos Managers\" \\ --privileges=\"Privileges - Kerberos Managers\" # Add the user to the role % ipa role-add-member --users=cdh \"Kerberos Managers\" # Optionally, we can export the keytab for the user with a password # You will see why in the next script % ipa-getkeytab -p cdh@EXAMPLE.COM -k cdh.keytab -P Now we need our special kerberos keytab retrieval script. #!/bin/bash # Created by: @nazunalika - Louis Abel # Purpose: To retrieve keytabs for Cloudera / Hadoop # https://github.com/nazunalika/useful-scripts # Disclaimer: We do not take responsibilities for breaches or misconfigurations of # software. Use at your own risk # Variables # This can be anywhere, but it SHOULD be secure with at least 600 permissions CDHKT=\"/root/.cdh/cdh.keytab\" CDHUSER=\"cdh\" IPAREALM=\"EXAMPLE.COM\" # This can be any server. You could make an array and have it randomly selected IPASERVER=\"ipa01.example.com\" # Where is this going? DESTINATION=\"$1\" # The full principal for the keytab in question FULLPRINC=\"$2\" # Shortened name PRINC=$(echo $FULLPRINC | sed \"s/\\@$(echo $IPAREALM)//\") 00_kinitUser() { # Pick what suits you best, we prefer using a keytab # Password based kinit, based on the keytab we created prior! # You could also have this in a file somewhere, I guess. Just # has to be secured. echo ThisIsAWeakPassword | kinit $CDHUSER@$IPAREALM # Keytab based kinit, obviously we created it before right? It just needs to be # on the right system, deployed in some secure manner #kinit -kt $CDHKT $CDHUSER@$IPAREALM if [[ $? == \"1\" ]]; then echo FAILED TO KINIT exit fi } 01_createPrinc() { echo \"INFO: Checking for existing principle\" if ipa service-find $FULLPRINC; then echo \"INFO: Principle found\" else echo \"INFO: Not found, creating\" ipa service-add $FULLPRINC fi } 02_createServiceAllows() { # We need to allow the service to create and retrieve keytabs echo \"INFO: Ensuring service allows to create and retrieve keytabs\" ipa service-allow-create-keytab --users=$CDHUSER $FULLPRINC ipa service-allow-retrieve-keytab --users=$CDHUSER $FULLPRINC # Let's retrieve the keytabs if ipa service-show $FULLPRINC | grep 'Keytab' | grep 'False'; then echo \"INFO: Creating keytab for $FULLPRINC to $DESTINATION\" ipa-getkeytab -s $IPASERVER -p $PRINC -k $DESTINATION else echo \"INFO: Retriving keytab for $FULLPRINC to $DESTINATION\" ipa-getkeytab -r -s $IPASERVER -p $PRINC -k $DESTINATION fi } 00_kinitUser 01_createPrinc 02_createServiceAllows kdestroy exit 0 Place the above script in a file that is accessible by the cloudera manager such as /usr/local/bin/getKeytabsCDH.sh and ensure it is owned by cloudera-scm with a permission set of 775. During the kerberos wizard, stop when you are verifying the \"cdh\" user. You will need to set the configuration for \"Custom Kerberos Keytab Retrieval Script\" to /usr/local/bin/getKeytabsCDH.sh and then you're almost there. 4 An important tidbit is currently Enterprise Linux 7+ and higher use memory based keytabs and java doesn't support them. 5 Because of this, the /etc/krb5.conf should be modified. % cat /etc/krb5.conf . . . # Make sure the below is commented # default_ccache_name = KEYRING:persistent:%{uid} . . .","title":"The Solution"},{"location":"el/freeipa/#dns-forwarding","text":"","title":"DNS Forwarding"},{"location":"el/freeipa/#dns-forwarding-to-dot","text":"Presently, FreeIPA does not support DoT (DNS over TLS) nor DoH (DNS over HTTPS) (this appears to be a bind limitation and we can't find documentation that says otherwise). However, it is possible to setup unbound to do the forwarding for you, in which you tell your bind servers (or in this case, the bind DNS servers in your IPA domain) to forward to that unbound server for all forwarding. Keep it Separate It is recommended to keep your unbound service separate from the IPA servers. Spin up another instance in your network that will run unbound or run it on a standalone bind server that you may have on a separate port. To forward to the unbound service, modify the DNS global configuration in IPA: # Replace 10.100.0.224 with the IP of your unbound instance % ipa dnsconfig-mod --forward-policy=only --forwarder='10.100.0.224' # Add 'port xxxx' if you have set unbound to another port % ipa dnsconfig-mod --forward-policy=only --forwarder='10.100.0.224 port 9553'","title":"DNS Forwarding to DoT"},{"location":"el/freeipa/#logging","text":"","title":"Logging"},{"location":"el/freeipa/#audit-logs","text":"By default, the audit logs in /var/log/dirsrv/slapd-INSTANCE/audit do not get populated. And the access logs don't show much in terms of modifications and what is being changed. There is also /var/log/httpd/* logs, but it may be useful to see ldif style logging for changes against FreeIPA. # Modify the DSE configuration by turning on audit logging [label@ipa01 ~]# ldapmodify -D \"cn=directory manager\" -W -p 389 -h localhost Enter LDAP Password: dn: cn=config changetype: modify replace: nsslapd-auditlog-logging-enabled nsslapd-auditlog-logging-enabled: on # Press CTRL+d here modifying entry \"cn=config\" # To test, I'll add a user to a group [label@ipa01 ~]$ ipa group-add-member --users=jbaskets aocusers Group name: aocusers GID: 686600003 Member users: ..., jbaskets ------------------------- Number of members added 1 ------------------------- # Let's verify the log [label@ipa01 ~]$ sudo su - [sudo] password for label: Last login: Sun Mar 29 16:42:36 MST 2020 on pts/0 [root@ipa01 ~]# cd /var/log/dirsrv/slapd-EXAMPLE-NET/ [root@ipa01 slapd-EXAMPLE-NET]# cat audit time: 20200329223754 dn: cn=config result: 0 changetype: modify replace: nsslapd-auditlog-logging-enabled nsslapd-auditlog-logging-enabled: on - replace: modifiersname modifiersname: cn=directory manager - replace: modifytimestamp modifytimestamp: 20200330053754Z - 389-Directory/1.4.1.3 B2019.323.229 ipa01.example.net:636 (/etc/dirsrv/slapd-EXAMPLE-NET) # Looks like right here the modification happened time: 20200329224007 dn: cn=aocusers,cn=groups,cn=accounts,dc=example,dc=net result: 0 changetype: modify add: member member: uid=jbaskets,cn=users,cn=accounts,dc=example,dc=net - replace: modifiersname modifiersname: uid=label,cn=users,cn=accounts,dc=example,dc=net - replace: modifytimestamp modifytimestamp: 20200330054006Z - replace: entryusn entryusn: 900028 -","title":"Audit Logs"},{"location":"el/freeipa/#certificates","text":"These are notes of things I've ran into before while dealing with certificates.","title":"Certificates"},{"location":"el/freeipa/#renewed-ipa-http-certificate-stuck","text":"This was something I discovered sort of on accident but never really \"noticed\" - Though I'm sure I would've noticed sometime in 2021 when my certificate expired. I was running ipa-healthcheck --failures-only as I do sometimes, and noticed some weird certmonger things pop up. But it made me look at my certificate list... [root@ipa01 ~]# ipa-getcert list Number of certificates and requests being tracked: 9. Request ID '20191106025922': status: MONITORING stuck: no key pair storage: type=FILE,location='/var/kerberos/krb5kdc/kdc.key' certificate: type=FILE,location='/var/kerberos/krb5kdc/kdc.crt' CA: IPA issuer: CN=Certificate Authority,O=ANGELSOFCLOCKWORK.NET subject: CN=ipa01.angelsofclockwork.net,O=ANGELSOFCLOCKWORK.NET expires: 2021-11-05 19:59:27 MST principal name: krbtgt/ANGELSOFCLOCKWORK.NET@ANGELSOFCLOCKWORK.NET key usage: digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment eku: id-kp-serverAuth,id-pkinit-KPKdc pre-save command: post-save command: /usr/libexec/ipa/certmonger/renew_kdc_cert track: yes auto-renew: yes Request ID '20200123075636': status: MONITORING stuck: no key pair storage: type=NSSDB,location='/etc/dirsrv/slapd-ANGELSOFCLOCKWORK-NET',nickname='Server-Cert',token='NSS Certificate DB',pinfile='/etc/dirsrv/slapd-ANGELSOFCLOCKWORK-NET/pwdfile.txt' certificate: type=NSSDB,location='/etc/dirsrv/slapd-ANGELSOFCLOCKWORK-NET',nickname='Server-Cert',token='NSS Certificate DB' CA: IPA issuer: CN=Certificate Authority,O=ANGELSOFCLOCKWORK.NET subject: CN=ipa01.angelsofclockwork.net,O=ANGELSOFCLOCKWORK.NET expires: 2021-11-05 19:55:33 MST dns: ipa01.angelsofclockwork.net principal name: ldap/ipa01.angelsofclockwork.net@ANGELSOFCLOCKWORK.NET key usage: digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment eku: id-kp-serverAuth,id-kp-clientAuth pre-save command: post-save command: /usr/libexec/ipa/certmonger/restart_dirsrv ANGELSOFCLOCKWORK-NET track: yes auto-renew: yes Request ID '20200123075639': status: NEWLY_ADDED_NEED_KEYINFO_READ_PIN stuck: yes key pair storage: type=FILE,location='/var/lib/ipa/private/httpd.key' certificate: type=FILE,location='/var/lib/ipa/certs/httpd.crt' CA: IPA issuer: CN=Certificate Authority,O=ANGELSOFCLOCKWORK.NET subject: CN=ipa01.angelsofclockwork.net,O=ANGELSOFCLOCKWORK.NET expires: 2021-11-05 19:55:48 MST dns: ipa01.angelsofclockwork.net principal name: HTTP/ipa01.angelsofclockwork.net@ANGELSOFCLOCKWORK.NET key usage: digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment eku: id-kp-serverAuth,id-kp-clientAuth pre-save command: post-save command: /usr/libexec/ipa/certmonger/restart_httpd track: yes auto-renew: yes Interestingly, I wasn't sure what NEWLY_ADDED_NEED_KEYINFO_READ_PIN meant and I couldn't really find much on what would cause this to happen. And I know my certificate isn't expired, according to the output. In fact, I checked with openssl just in case. [root@ipa01 ~]# openssl x509 -text -noout -in /var/lib/ipa/certs/httpd.crt | grep 'Not After' Not After : Nov 6 02:55:48 2021 GMT I'm not sure if this is just a result of migrating from Enterprise Linux 7 to 8 at the time, but it seemed easy enough to remove the tracking and put it back in, which ultimately fixed the monitoring state and now it was no longer \"stuck\". [root@ipa01 ~]# ipa-getcert stop-tracking -i 20200123075639 Request \"20200123075639\" removed. [root@ipa01 ~]# ipa-getcert start-tracking -f /var/lib/ipa/certs/httpd.crt -k /var/lib/ipa/private/httpd.key -p /var/lib/ipa/passwds/ipa01.angelsofclockwork.net-443-RSA -C /usr/libexec/ipa/certmonger/restart_httpd -K HTTP/ipa01.angelsofclockwork.net@ANGELSOFCLOCKWORK.NET New tracking request \"20200504003758\" added. [root@ipa01 ~]# ipa-getcert list -i \"20200504003758\" Number of certificates and requests being tracked: 9. Request ID '20200504003758': status: MONITORING stuck: no key pair storage: type=FILE,location='/var/lib/ipa/private/httpd.key',pinfile='/var/lib/ipa/passwds/ipa01.angelsofclockwork.net-443-RSA' certificate: type=FILE,location='/var/lib/ipa/certs/httpd.crt' CA: IPA issuer: CN=Certificate Authority,O=ANGELSOFCLOCKWORK.NET subject: CN=ipa01.angelsofclockwork.net,O=ANGELSOFCLOCKWORK.NET expires: 2021-11-05 19:55:48 MST dns: ipa01.angelsofclockwork.net principal name: HTTP/ipa01.angelsofclockwork.net@ANGELSOFCLOCKWORK.NET key usage: digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment eku: id-kp-serverAuth,id-kp-clientAuth pre-save command: post-save command: /usr/libexec/ipa/certmonger/restart_httpd track: yes auto-renew: yes","title":"Renewed IPA HTTP Certificate Stuck"},{"location":"el/freeipa/#ca-related-certificates-stuck","text":"Like with the IPA httpd certificates, I noticed at least 4 certificates stuck because a PIN was missing. Turns out that it's actually easy to modify the tracking request and fix the issue entirely. Below is my example doing this on the auditSigningCert. This seems to only occur on Enterprise Linux 8. [root@ipa01 alias]# getcert list -i 20200615180351 Number of certificates and requests being tracked: 9. Request ID '20200615180351': status: NEWLY_ADDED_NEED_KEYINFO_READ_PIN stuck: yes key pair storage: type=NSSDB,location='/etc/pki/pki-tomcat/alias',nickname='auditSigningCert cert-pki-ca' certificate: type=NSSDB,location='/etc/pki/pki-tomcat/alias',nickname='auditSigningCert cert-pki-ca' CA: dogtag-ipa-ca-renew-agent issuer: subject: expires: unknown pre-save command: /usr/libexec/ipa/certmonger/stop_pkicad post-save command: /usr/libexec/ipa/certmonger/renew_ca_cert \"auditSigningCert cert-pki-ca\" track: yes auto-renew: yes [root@ipa01 alias]# getcert start-tracking -i 20200615180351 -p /etc/pki/pki-tomcat/alias/pwdfile.txt Request \"20200615180351\" modified. [root@ipa01 alias]# getcert list -i 20200615180351 Number of certificates and requests being tracked: 9. Request ID '20200615180351': status: MONITORING stuck: no key pair storage: type=NSSDB,location='/etc/pki/pki-tomcat/alias',nickname='auditSigningCert cert-pki-ca',token='NSS Certificate DB',pinfile='/etc/pki/pki-tomcat/alias/pwdfile.txt' certificate: type=NSSDB,location='/etc/pki/pki-tomcat/alias',nickname='auditSigningCert cert-pki-ca',token='NSS Certificate DB' CA: dogtag-ipa-ca-renew-agent issuer: CN=Certificate Authority,O=ANGELSOFCLOCKWORK.NET subject: CN=CA Audit,O=ANGELSOFCLOCKWORK.NET expires: 2021-03-13 23:15:41 MST key usage: digitalSignature,nonRepudiation pre-save command: /usr/libexec/ipa/certmonger/stop_pkicad post-save command: /usr/libexec/ipa/certmonger/renew_ca_cert \"auditSigningCert cert-pki-ca\" track: yes auto-renew: yes","title":"CA Related Certificates Stuck"},{"location":"el/freeipa/#default-certificates-with-san","text":"A question that arises now and again is how to setup a load balancer for FreeIPA's LDAP servers whether it's an actual load balancer (layer 4) or some sort of DNS record with multiple A records, or perhaps with some sort of round robin DNS. The issue is that the certificate verification fails, because the certificate being presented is of the IPA server itself with no SAN. To address this, you have to create a host that has the name of the load balancer or DNS record you plan on using and allow the IPA servers to manage the host.","title":"Default Certificates with SAN"},{"location":"el/freeipa/#cms-communication-issues-403","text":"This isn't necessarily certificate issue, but more or less an issue as it pertains to the certificate system itself. There may be cases where during upgrades, a configuration in /etc/pki/pki-tomcat/server.xml is not properly reconfigured. In that file, you'll notice Connector lines that have a secret and a requiredSecret parameter and they both have different values. The issue may be that these aren't correct. This generally comes down to IPA and pki-core conflicting on these attributes. To correct this, you will need to find the secret in /etc/httpd/conf.d/ipa-pki-proxy.conf (on the ProxyPass line) and ensure that's the same secret in both fields. ProxyPassMatch ajp://localhost:8009 secret=AAA Make sure they're the same in server.xml After changing, restart the service with systemctl restart pki-tomcat@pki-tomcatd.service.","title":"CMS Communication Issues (403)"},{"location":"el/freeipa/#kerberos","text":"This section goes over some stuff about kerberos that we've ran into and might find useful someday.","title":"Kerberos"},{"location":"el/freeipa/#accounts-with-otp-enabled","text":"When logging into a machine with a password (first factor) and an OTP token (second factor), this generally works without a problem. You can easily run klist and you'll see that you have a ticket and everything. In the cases where you're calling kinit all by itself, this doesn't work as expected at the time of this writing. % kinit account@REALM kinit: Pre-authentication failed: Invalid argument while getting initial credentials A bugzilla was opened about this issue in 2017, a pagure issue was opened in 2014 about this exact scenario, where IPA is configured for password+OTP and a user has an assigned token. There is currently one workaround, which is using kinit -n to perform anonymous processing.","title":"Accounts with OTP Enabled"},{"location":"el/freeipa/#footnotes","text":"For more information on DNS for FreeIPA, please read this page and this page \u21a9 The -P asks for the password of the username in question, that way it is cached right away. The directory service on the system then has credentials to compare to. I have found that sometimes if you don't use -P, even if you're logged in as the account, the password does not get cached and you'll get stuck at a background image the next time you login. Again, this is only sometimes. Your mileage may vary here. \u21a9 The -P asks for the password of the username in question, that way it is cached right away. The directory service on the system then has credentials to compare to. I have found that sometimes if you don't use -P, even if you're logged in as the account, the password does not get cached and you'll get stuck at a background image the next time you login. Again, this is only sometimes. Your mileage may vary here. \u21a9 Please read this page for more information. \u21a9 This may have changed. However it is up to you to test if this is the case. \u21a9","title":"Footnotes"},{"location":"el/nat/","text":"This page goes over setting up a router or a simple NAT service for Enterprise Linux. Requirements \u00b6 Here are the list of requirements below. Enterprise Linux 8, 9 or Fedora An active internet connection to install the packages required or available internal mirrors A system with at least two (2) network interfaces Tutorial \u00b6 Interface Setup \u00b6 To properly setup the system, a few things have to be done. One interface must be the WAN interface, in most cases this is set to DHCP. Another interface must be the LAN interface or a group of interfaces must become a bridge with a static address ip_forward must be turned on - optionally if you have ipv6, turn on that forwarding as well IPv6 and NAT If you have an IPv6 prefix, whether it's from your ISP or it's a brokered prefix from he.net, NAT is generally not needed. Instead of using NAT for IPv6, you can just do simple forwarding. This is covered in a later section. FirewallD \u00b6 When using firewalld, Enterprise Linux 7+ and all Fedora\\'s can setup a simple NAT with masquerade without having to know iptables or nftables syntax. This may be more or less ideal for some users who want to quickly get a NAT and router going. The drawback is that the syntax and knowing how the rules work are hidden behind a frontend. To setup a NAT: # Tell eth0 to be our WAN % nmcli con mod eth0 connection.zone external # Tell eth1 to be our LAN (or a bridge if you have one) % nmcli con mod eth1 connection.zone internal # Doesn't hurt to re-up % nmcli con up eth0 ; nmcli con up eth1 # The external zone already has masquerade on, but just in case % firewall-cmd --zone=external --add-masquerade --permanent % firewall-cmd --complete-reload % firewall-cmd --get-active-zones external interfaces: eth0 internal interfaces: eth1 nftables \u00b6 This is for Enterprise Linux 8/9 or Fedora where nftables is the default. While iptables exists for Enterprise Linux 8 still, it is being superseded by nftables. It is recommended to stick with nftables. The syntax for nftables is a little tricky and quite different from what we may be used to with iptables. This may be an oversimplification and may or may not work. For ideas, you can view the files in /etc/nftables. This is a rough example of what I tried on migration to Enterprise Linux 8. # Disable firewalld, we'll enable nftables later % systemctl disable firewalld --now % systemctl mask firewalld # Flush all rules % nft flush ruleset Rest coming soon. IPv6 Forwarding \u00b6 Coming soon. DHCP \u00b6 Optional. Coming soon","title":"NAT/Router"},{"location":"el/nat/#requirements","text":"Here are the list of requirements below. Enterprise Linux 8, 9 or Fedora An active internet connection to install the packages required or available internal mirrors A system with at least two (2) network interfaces","title":"Requirements"},{"location":"el/nat/#tutorial","text":"","title":"Tutorial"},{"location":"el/nat/#interface-setup","text":"To properly setup the system, a few things have to be done. One interface must be the WAN interface, in most cases this is set to DHCP. Another interface must be the LAN interface or a group of interfaces must become a bridge with a static address ip_forward must be turned on - optionally if you have ipv6, turn on that forwarding as well IPv6 and NAT If you have an IPv6 prefix, whether it's from your ISP or it's a brokered prefix from he.net, NAT is generally not needed. Instead of using NAT for IPv6, you can just do simple forwarding. This is covered in a later section.","title":"Interface Setup"},{"location":"el/nat/#firewalld","text":"When using firewalld, Enterprise Linux 7+ and all Fedora\\'s can setup a simple NAT with masquerade without having to know iptables or nftables syntax. This may be more or less ideal for some users who want to quickly get a NAT and router going. The drawback is that the syntax and knowing how the rules work are hidden behind a frontend. To setup a NAT: # Tell eth0 to be our WAN % nmcli con mod eth0 connection.zone external # Tell eth1 to be our LAN (or a bridge if you have one) % nmcli con mod eth1 connection.zone internal # Doesn't hurt to re-up % nmcli con up eth0 ; nmcli con up eth1 # The external zone already has masquerade on, but just in case % firewall-cmd --zone=external --add-masquerade --permanent % firewall-cmd --complete-reload % firewall-cmd --get-active-zones external interfaces: eth0 internal interfaces: eth1","title":"FirewallD"},{"location":"el/nat/#nftables","text":"This is for Enterprise Linux 8/9 or Fedora where nftables is the default. While iptables exists for Enterprise Linux 8 still, it is being superseded by nftables. It is recommended to stick with nftables. The syntax for nftables is a little tricky and quite different from what we may be used to with iptables. This may be an oversimplification and may or may not work. For ideas, you can view the files in /etc/nftables. This is a rough example of what I tried on migration to Enterprise Linux 8. # Disable firewalld, we'll enable nftables later % systemctl disable firewalld --now % systemctl mask firewalld # Flush all rules % nft flush ruleset Rest coming soon.","title":"nftables"},{"location":"el/nat/#ipv6-forwarding","text":"Coming soon.","title":"IPv6 Forwarding"},{"location":"el/nat/#dhcp","text":"Optional. Coming soon","title":"DHCP"},{"location":"el/openldap/","text":"Deprecation \u00b6 Please note that the OpenLDAP server is considered deprecated in RHEL (and thusly other EL derivatives). This document will stay here as a reference for those who are still using the server software on Enterprise Linux 7, potentially using the OpenLDAP LTB version of the software, or using the openldap-servers package from Rocky Linux 9's plus repository. It may apply to Fedora users in some contexts, but there are some differences they may never be documented here. If you see a need for corrections, please open up an issue on our github. Most of this information should be considered out of date. OpenLDAP 2.6.x makes some changes that the below document may or may not cover. Preface \u00b6 This tutorial goes over how to install OpenLDAP to a Enterprise Linux Server and options on configuring and setting up accounts for host access, etc. This how-to is the method of implementation that I used, and can be modified/changed to any users preferences if preferred. Enterprise Linux and Fedora users will have the ability to use SSSD for SUDO (and it's relatively easy to set it up). Overview \u00b6 Simply put, LDAP is a directory service for authentication across a network. Rather than have local accounts on a bunch of machines, LDAP can be used to have one account across a bunch of machines. LDAP was once an easy setup in RHEL 5 but had changed in RHEL 6 and 7, and here provides the necessary information needed to get a simple LDAP system running with possible SUDO support and various options of how to support your LDAP system. Requirements \u00b6 First and foremost, we have a list of requirements. Keep in mind, if you do not fulfill these requirements, you may run into some issues down the road. Enterprise Linux 8, Enterprise Linux 9 DNS Server (LDAP does NOT appreciate IP addresses for the URI) An active internet connection to install the packages required Tutorial Preface, Notes, and Recommendations \u00b6 Warning Potential Pitfalls! The incorrect configuration in your firewall or other settings can cause login failures Not using certificates (TLS/SSL) will cause you not to be able to login (This is a EL7+ LDAP Client Requirement) SELinux is a pain when using mounted home directories or certificates (primarily in Enterprise Linux 7+, will you have problems with certificates/home directories) If you use /home as an NFS mount, you'll see some nasty side effects if you have local users. Note Recommended Information It's recommended to use colored vim syntax. Root doesn't use vim when vim-enhanced is installed. You can make an alias for vi to run vim. Turn on syntaxing in ~/.vimrc -- syntax on Make the vim colors brighter in ~/.vimrc -- set background=dark Export your EDITOR variable in ~/.bash_profile -- export EDITOR=vim Keep selinux set to enforcing Note Database Information We will be using lmdb, which is recommended over using hdb or bdb. Note EL 9 OpenLDAP Enterprise Linux 9 has fully dropped OpenLDAP where there are no server packages normally built. In some EL distributions, openldap-servers is still built and provided in an extra repository. Rocky Linux 9 has openldap-servers in their plus repository. EPEL also has it available. Installation \u00b6 Below details the process for installing OpenLDAP to our system(s). This includes installing the packages, setting up certificates, and configuring the LDAP server via LDIF files. Packages \u00b6 You will need the following packages. A couple of them may already be installed. If you don't plan on migrating local accounts to LDAP, you can leave out migrationtools. # If you are on Rocky Linux 9, you will need to enable the plus repository dnf install openldap openldap-servers migrationtools nss-tools -y Certificates \u00b6 Enterprise Linux 7 clients and other newer distributions that are non-el require TLS/SSL for authentication when going toward LDAP. because of this, we will need to create certificates, regardless if you are in a lab or not. Note Certificate Information NSS should no longer be required. Anything NSS related has been removed. I have two ways of doing it, we can do it manually or through a script. I prefer using my script to take care of it. First the manual way. mkdir /etc/pki/ldap cd /etc/pki/ldap openssl genrsa -des3 -out ca.key 4096 # Remember the password you put here openssl genrsa -out ldapserver.key 4096 openssl req -new -x509 -key ca.key -out ca.pem -days 3650 Country Name (2 letter code) [XX]:US State or Province Name (full name) []:Arizona Locality Name (eg, city) [Default City]:Phoenix Organization Name (eg, company) [Default Company Ltd]:SSN Studio Organizational Unit Name (eg, section) []:Channel Maintainers Common Name (eg, your name or your server's hostname) []:SSN # If you want to use a server name here, perform this step on another server first Email Address []:youremail@mail.com openssl req -new -key ldapserver.key -out ldapserver.csr Country Name (2 letter code) [XX]:US State or Province Name (full name) []:Arizona Locality Name (eg, city) [Default City]:Phoenix Organization Name (eg, company) [Default Company Ltd]:SSN Studio Organizational Unit Name (eg, section) []:LDAP Server Maintainer Common Name (eg, your name or your server's hostname) []:zera1.angelsofclockwork.net # Set your common name to your server name for this certificate Email Address []:youremail@mail.com openssl x509 -req -in ldapserver.csr -out ldapserver.pem -CA ca.pem -CAkey ca.key -days 3650 -set_serial 01 ln -s ca.pem `openssl x509 -hash -in ca.pem -noout`.0 # Do an ls on the directory and save the hashed name including the .0 somewhere certutil -N -d /etc/pki/ldap # Do not enter any passwords. When asked, just hit enter beyond this point. chown root:ldap * chmod 640 * The scripted way. #!/bin/bash # CA Information CAcountry=\"US\" CAstate=\"Arizona\" CAlocale=\"Phoenix\" CAorganization=\"SSN Studio\" CAorganizationalunit=\"Channel Maintainers\" # If you set the below to a hostname, you\u2019re screwed. Don\u2019t do it! # Only do it if you have an actual hostname you will do CA signing on! CAconicalname=\"SSN\" CAemail=\"tucklesepk@gmail.com\" # LDAP Server information country=\"US\" state=\"Arizona\" locale=\"Phoenix\" organization=\"SSN Studio\" organizationalunit=\"LDAP Server Maintainer\" conicalname=\"zera1.angelsofclockwork.net\" email=\"pc68xl@gmail.com\" certdir=\"/etc/pki/ldap\" mkdir $certdir ; cd $certdir echo \"Enter a password when asked.\" openssl genrsa -des3 -out ca.key 4096 openssl genrsa -out ldapserver.key 4096 # Create the self-signed CA cert openssl req -new -x509 -key ca.key -out ca.pem -days 3650 -subj /C=\"$CAcountry\"/ST=\"$CAstate\"/L=\"$CAlocale\"/O=\"$CAorganization\"/OU=\"$CAorganizationalunit\"/CN=\"$CAconicalname\"/emailAddress=\"$CAemail\"/ # Create the LDAP server cert openssl req -new -key ldapserver.key -out ldapserver.csr -subj /C=\"$country\"/ST=\"$state\"/L=\"$locale\"/O=\"$organization\"/OU=\"$organizationalunit\"/CN=\"$conicalname\"/emailAddress=\"$email\"/ # Sign it openssl x509 -req -in ldapserver.csr -out ldapserver.pem -CA ca.pem -CAkey ca.key -days 3650 -set_serial 01 ln -s ca.pem `openssl x509 -hash -in ca.pem -noout`.0 chown root:ldap * chmod 640 * Make sure to obtain your hash. Your hash will be different from mine. ls -l /etc/pki/ldap | grep '0' 39642ab3.0 LDAP Server Configuration \u00b6 Configurations done in OpenLDAP are done via LDIF. Your passwords should be hashed as well. Before we begin, let's start by generating a password for our root DN. This is required. slappasswd New password: Re-enter new password: {SSHA}CuaKctEx7rl/+ldG0EjktMzJdrxNc46+ Keep this SSHA output for our configuration files. Next, we'll need to make a couple LDIFs. This is our suffix.ldif file. This file helps to create the mdb database for our LDAP structure. It also sets our DIT suffix, root password, etc. You should change the olcSuffix, olcRootDN, and olcRootPW to whatever you plan on using. The olcDbMaxSize is set to 20GB. This is normally sufficient and can be changed. The olcDbEnvFlags can be changed as well. dn: olcDatabase=mdb,cn=config objectClass: olcDatabaseConfig objectClass: olcMdbConfig olcDatabase: mdb olcDbDirectory: /var/lib/ldap olcSuffix: dc=angelsofclockwork,dc=net olcRootDN: cn=manager,dc=angelsofclockwork,dc=net olcRootPW: {SSHA}CuaKctEx7rl/+ldG0EjktMzJdrxNc46+ olcDbIndex: objectClass eq,pres olcDbIndex: ou,cn,mail,surname,givenname eq,pres,sub olcLastMod: TRUE olcDbEnvFlags: nometasync olcDbEnvFlags: writemap olcDbMaxSize: 21474836480 Now, below we have our primary modification ldif. Comments describe what each one does. # Sets our cert path and information # The \"CertificateFile\" has to be set to the hostname of the LDAP server dn: cn=config changetype: modify replace: olcTLSCACertificatePath olcTLSCACertificatePath: /etc/pki/ldap - replace: olcTLSCertificateFile olcTLSCertificateFile: zera1.angelsofclockwork.net - replace: olcTLSCertificateKeyFile olcTLSCertificateKeyFile: /etc/pki/ldap/ldapserver.key # Adding a rootDN for the config. # Note that this isn't fully necessary as you can use -Y EXTERNAL -H ldapi:/// instead # So, treat this as an optional thing. If you do want it, consider a different password. dn: olcDatabase={0}config,cn=config changetype: modify replace: olcRootDN olcRootDN: cn=config - replace: olcRootPW olcRootPW: {SSHA}CuaKctEx7rl/+ldG0EjktMzJdrxNc46+ # Set the password again in the mdb database # This is because sometimes the password set when making the database doesn't 'work' sometimes dn: olcDatabase={2}mdb,cn=config changetype: modify replace: olcRootPW olcRootPW: {SSHA}CuaKctEx7rl/+ldG0EjktMzJdrxNc46+ # Sets the default password hash to SSHA -- Refer to the 'bug' information if this does not work dn: olcDatabase={-1}frontend,cn=config changetype: modify replace: olcPasswordHash olcPasswordHash: {SSHA} # Changes the rootdn information in the monitor database dn: olcDatabase={1}monitor,cn=config changetype: modify replace: olcAccess olcAccess: {0}to * by dn.base=\"gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth\" read by dn.base=\"cn=manager,dc=angelsofclockwork,dc=net\" read by * none Let's make sure we turn on ldaps. It's recommended to use TLS, but some applications insist on SSL. (Very few, but they are out there.) # vi /etc/sysconfig/slapd . . . SLAPD_URLS=\"ldapi:/// ldap:/// ldaps:///\" # slaptest -u Config file testing succeeded # /etc/openldap/ldap.conf . . . TLS_CACERTDIR /etc/pki/ldap Now, we need to add our LDIFs into LDAP. rm -f /etc/openldap/slapd.d/cn=config/olcDatabase=\\{2\\}hdb.ldif chown -R ldap:ldap /var/lib/ldap systemctl enable slapd systemctl start slapd ldapadd -Y EXTERNAL -H ldapi:/// -f suffix.ldif ldapmodify -Y EXTERNAL -H ldapi:/// -f info.ldif You may end up getting a checksum error in your logs. To solve this, you need to do a simple operation against the configuration. ldapmodify -h localhost -xWD \"cn=config\" Enter LDAP Password: dn: olcDatabase={0}config,cn=config changetype: modify replace: olcRootDN olcRootDN: cn=config modifying entry \"olcDatabase={0}config,cn=config\" slaptest -u config file testing succeeded That should do it. You can do a -Y EXTERNAL -H ldapi:/// instead if you wanted to. I did the above to show passwords will work for config. LDAP Structure \u00b6 The next piece is to get our backend structure built. In EL7, core is the only schema that is there. In EL6, it's a good chunk of these. I like to put them in a file so I can loop through them. Note ppolicy schema As of OpenLDAP 2.6.x, the ppolicy schema no longer applies as it is built-in to the slapo-ppolicy module. See the upgrade document for information. /etc/openldap/schema/corba.ldif /etc/openldap/schema/cosine.ldif /etc/openldap/schema/duaconf.ldif /etc/openldap/schema/dyngroup.ldif /etc/openldap/schema/inetorgperson.ldif /etc/openldap/schema/java.ldif /etc/openldap/schema/misc.ldif /etc/openldap/schema/nis.ldif /etc/openldap/schema/openldap.ldif /etc/openldap/schema/collective.ldif Note rfc2307 If you want to be able to combine groupOfNames and posixGroup together (similar to Active Directory, other open source, and commercial offerings), don't use nis. Use the rfc2307bis schema instead. Once you have your list of schema to put in, we can loop through them. for x in $(cat schemaorder) ; do ldapadd -Y EXTERNAL -H ldapi:/// -f $x ; done adding new entry \"cn=corba,cn=schema,cn=config\" adding new entry \"cn=cosine,cn=schema,cn=config\" adding new entry \"cn=duaconf,cn=schema,cn=config\" adding new entry \"cn=dyngroup,cn=schema,cn=config\" adding new entry \"cn=inetorgperson,cn=schema,cn=config\" adding new entry \"cn=java,cn=schema,cn=config\" adding new entry \"cn=misc,cn=schema,cn=config\" adding new entry \"cn=nis,cn=schema,cn=config\" adding new entry \"cn=openldap,cn=schema,cn=config\" adding new entry \"cn=collective,cn=schema,cn=config\" I normally like to keep all LDIFs in a folder by themselves to avoid clutter (non-configuration LDIF). mkdir ldif ; cd ldif Let's get our base created. Make sure to replace my DN with your DN that you chose earlier. Call this base.ldif. dn: dc=angelsofclockwork,dc=net dc: angelsofclockwork objectClass: top objectClass: domain dn: ou=People,dc=angelsofclockwork,dc=net ou: People objectClass: top objectClass: organizationalUnit dn: ou=Group,dc=angelsofclockwork,dc=net ou: Group objectClass: top objectClass: organizationalUnit ldapadd -xWD \"cn=manager,dc=angelsofclockwork,dc=net\" -f base.ldif Enter LDAP Password: adding new entry \"dc=angelsofclockwork,dc=net\" adding new entry \"ou=People,dc=angelsofclockwork,dc=net\" adding new entry \"ou=Group,dc=angelsofclockwork,dc=net\" If this doesn't add, make sure your LDAP server is running, check /var/log/messages, and ensure you've completed all steps before this. ldapsearch -x -LLL -b 'dc=angelsofclockwork,dc=net' dn: dc=angelsofclockwork,dc=net dc: angelsofclockwork objectClass: top objectClass: domain dn: ou=People,dc=angelsofclockwork,dc=net ou: People objectClass: top objectClass: organizationalUnit dn: ou=Group,dc=angelsofclockwork,dc=net ou: Group objectClass: top objectClass: organizationalUnit Add Users via Migration \u00b6 Note But... I don't want to add my users locally You don't have to add your users locally to the system. This just aids in the creation of users. Go to the next section if you want to add users and do permissions by hand. This is the fun part. We'll need to add some users, set some passwords and migrate them into the LDAP system. I'll make three users as an example, give them an ID starting at 10000, home directories in /lhome, set a password, and proceed to migrate them. If you don't want to use /lhome, keep them set to /home and their home directories should get created automatically when logging into another machine. # mkdir /lhome # mkdir ldif/user # semanage fcontext -a -t home_root_t \"/lhome(/.*)?\" # restorecon -v /lhome \\ restorecon reset /lhome context unconfined_u:object_r:default_t:s0->unconfined_u:object_r:home_root_t:s0 # groupadd -g 10000 sokel # groupadd -g 10001 suree # groupadd -g 10002 ranos # useradd -u 10000 -g 10000 -d /lhome/sokel sokel # useradd -u 10001 -g 10001 -d /lhome/suree suree # useradd -u 10002 -g 10002 -d /lhome/ranos ranos # passwd sokel ; passwd suree ; passwd ranos # cat /etc/passwd | grep sokel > ldif/user/passwd.sokel # cat /etc/passwd | grep suree > ldif/user/passwd.suree # cat /etc/passwd | grep ranos > ldif/user/passwd.ranos # cat /etc/group | grep sokel > ldif/user/group.sokel # cat /etc/group | grep suree > ldif/user/group.suree # cat /etc/group | grep ranos > ldif/user/group.ranos We'll set some aliases for our migration scripts too # alias miguser='/usr/share/migrationtools/migrate_passwd.pl' # alias miggroup='/usr/share/migrationtools/migrate_group.pl' Before we continue, we need to modify our migration scripts. This is extremely important, otherwise our LDIFs will come out incorrect. Change them to your DN. # sed -i.bak \"s/padl.com/angelsofclockwork.net/g\" /usr/share/migrationtools/migrate_common.ph # sed -i.bak \"s/padl,dc=com/angelsofclockwork,dc=net/g\" /usr/share/migrationtools/migrate_common.ph Now we can use a loop to convert them. You can do it by hand also, but that's up to you. # for x in sokel suree ranos ; do miguser ldif/user/passwd.$x > ldif/user/$x.ldif ; done # for x in sokel suree ranos ; do miggroup ldif/user/group.$x >> ldif/user/$x.ldif ; done # cd ldif/user/ # cat *.ldif >> /tmp/ourusers.ldif # ldapadd -xWD \"cn=manager,dc=angelsofclockwork,dc=net\" -f /tmp/ourusers.ldif Enter LDAP Password: adding new entry \"uid=ranos,ou=People,dc=angelsofclockwork,dc=net\" adding new entry \"cn=ranos,ou=Group,dc=angelsofclockwork,dc=net\" adding new entry \"uid=sokel,ou=People,dc=angelsofclockwork,dc=net\" adding new entry \"cn=sokel,ou=Group,dc=angelsofclockwork,dc=net\" adding new entry \"uid=suree,ou=People,dc=angelsofclockwork,dc=net\" adding new entry \"cn=suree,ou=Group,dc=angelsofclockwork,dc=net\" The manual way. # /usr/share/migrationtools/migrate_passwd.pl ldif/user/passwd.sokel > ldif/user/sokel.ldif # /usr/share/migrationtools/migrate_group.pl ldif/user/group.sokel >> ldif/user/sokel.ldif # /usr/share/migrationtools/migrate_passwd.pl ldif/user/passwd.suree > ldif/user/suree.ldif # /usr/share/migrationtools/migrate_group.pl ldif/user/group.suree >> ldif/user/suree.ldif # /usr/share/migrationtools/migrate_passwd.pl ldif/user/passwd.ranos > ldif/user/ranos.ldif # /usr/share/migrationtools/migrate_group.pl ldif/user/group.ranos >> ldif/user/ranos.ldif # cd ldif/user/ # ls group.ranos group.suree passwd.sokel ranos.ldif suree.ldif group.sokel passwd.ranos passwd.suree sokel.ldif # ldapadd -xWD \"cn=manager,dc=angelsofclockwork,dc=net\" -f sokel.ldif Enter LDAP Password: adding new entry \"uid=sokel,ou=People,dc=angelsofclockwork,dc=net\" adding new entry \"cn=sokel,ou=Group,dc=angelsofclockwork,dc=net\" # ldapadd -xWD \"cn=manager,dc=angelsofclockwork,dc=net\" -f suree.ldif Enter LDAP Password: adding new entry \"uid=suree,ou=People,dc=angelsofclockwork,dc=net\" adding new entry \"cn=suree,ou=Group,dc=angelsofclockwork,dc=net\" # ldapadd -xWD \"cn=manager,dc=angelsofclockwork,dc=net\" -f ranos.ldif Enter LDAP Password: adding new entry \"uid=ranos,ou=People,dc=angelsofclockwork,dc=net\" adding new entry \"cn=ranos,ou=Group,dc=angelsofclockwork,dc=net\" Add Users via LDIF \u00b6 This is for those who don't want to create the account locally. For each user, you need to create an LDIF that satisfies their account information such as UID, GID and their group information. If you plan on having NFS exports to /lhome, make sure homeDirectory is correctly pointing as such. Otherwise, keep it as /home/username. dn: uid=zera,ou=People,dc=angelsofclockwork,dc=net objectClass: posixAccount objectClass: top objectClass: shadowAccount objectClass: inetOrgPerson cn: Zera Nalika gidNumber: 11000 sn: Nalika uidNumber: 11000 givenName: Zera uid: zera loginShell: /bin/bash homeDirectory: /home/zera displayName: Zera Nalika userPassword: changeme2 dn: cn=zera,ou=Group,dc=angelsofclockwork,dc=net objectClass: posixGroup objectClass: top cn: zera gidNumber: 11000 That's about it for that. You create these for each user as needed and then add them into ldap. # ldapadd -xWD \"cn=manager,dc=angelsofclockwork,dc=net\" -f zera.ldif adding new entry \"uid=zera,ou=People,dc=angelsofclockwork,dc=net\" adding new entry \"cn=zera,ou=Group,dc=angelsofclockwork,dc=net\" For users who are doing the /lhome thing, make their directories. When you are changing ownership, do it by UID and GID number. # mkdir /lhome # semanage fcontext -a -t home_root_t \"/lhome(/.*)?\" # mkdir /lhome/zera # cp /etc/skel/.* /lhome/zera # chown -R 11000:11000 /lhome/zera # restorecon -Rv /lhome NFS Export Home Directories \u00b6 Warning /home vs /lhome If you used /lhome and you want to use NFS mounts, you may continue here. Otherwise, skip this section entirely. If you use /home and still want to do NFS, you will need to do persistent NFS to say /export/home, and then setup AutoFS to use /export/home as a way to automount into /home. Warning Potential Pitfall Do NOT use NFSv3. The steps below show how to prevent user squashing to allow the user to have access to their home directories. Typically, in an NFSv4 fashion, it tends to mount it with permissions set to nobody. Other solutions have been to force NFSv3. This is NOT recommended. YOU HAVE BEEN WARNED. First, we'll need to install nfs-utils, set up our exports, and modify our id map file. # dnf install nfs-utils libnfsidmap -y # vi /etc/exports /lhome *(rw,sync,root_squash,no_all_squash) # vi /etc/idmapd.conf # Comment out the first Domain line and make your own Domain = zera1.angelsofclockwork.net # systemctl start nfs-server # systemctl enable nfs-server Sometimes you'll still run into the nobody problem. Sometimes this helps. # vi /etc/sysconfig/nfs NEED_IDMAPD=yes NFSMAPID_DOMAIN=library.angelsofclockwork.net Firewall \u00b6 Warning Keep your firewall on It is bad practice to turn your firewall off. Don't do it. We need to open up our firewall. Note Port Reference LDAP Ports: 389/tcp 636/tcp NFS Ports: 111/tcp 111/udp 2049/tcp If using firewalld, you can add these ports by service. # firewall-cmd --add-service=ldap --zone=public --permanent # firewall-cmd --add-service=ldaps --zone=public --permanent # firewall-cmd --add-service=nfs --zone=public --permanent # firewall-cmd --reload Client \u00b6 Setting up the client can be straight-forward or troubling, depending on the distribution you're using. We'll be going over EL7+. Fedora also works here as well. Warning Third-party Repositories If you use third-party repositories, you may want to disable them, at least temporarily. Depending on the repository, there may be conflicts when installing the appropriate packages. You may want to consider on setting up priorities, and ensure your base and updates are higher than the rest. Enterprise Linux/Current Fedora Releases \u00b6 We'll be using SSSD for this. We need to install some key packages first. Some of these packages may not install because they were either superceded or obsoleted. # yum install pki-{ca,common,silent} openldap-clients nss-pam-ldapd policycoreutils-python sssd sssd-common sssd-client sssd-ldap Use authselect to configure pam and nss. You'll need to configure /etc/sssd/sssd.conf by hand after. # authselect select sssd with-mkhomedir with-sudo # vi /etc/sssd/sssd.conf [domain/default] cache_credentials = True krb5_realm = # ldap_search_base = dc=angelsofclockwork,dc=net id_provider = ldap auth_provider = ldap chpass_provider = ldap sudo_provider = ldap ldap_uri = ldap://zera1.angelsofclockwork.net ldap_id_use_start_tls = True ldap_tls_cacertdir = /etc/openldap/certs ldap_tls_cacert = /etc/openldap/certs/ca.pem # Add the below ldap_sudo_search_base = ou=SUDOers,dc=angelsofclockwork,dc=net [sssd] # Modify this line and add sudo to the list services = nss, pam, autofs, sudo [sudo] Now, let's get our CA cert that we made way long ago and download it. If you used a real CA to sign your certificate, obtain their certificate. Note Hash Remember your hash from when you were making your certificates? You need to obtain it. In both examples, we created it while using a symbolic link. # scp zera1.angelsofclockwork.net:/etc/pki/ldap/ca.pem /etc/openldap/certs/ca.pem # cd /etc/openldap/certs # ln -s ca.pem 39642ab3.0 Now, modify /etc/openldap/ldap.conf and add the following to the bottom, ensuring your BASE is set correctly. URI ldap://library.angelsofclockwork.net BASE dc=angelsofclockwork,dc=net ssl start_tls You can attempt an ldapsearch and it should work. Search for one of your users. # ldapsearch -x -LLL uid=zera dn: uid=zera,ou=People,dc=angelsofclockwork,dc=net cn: Zera Nalika gidNumber: 11000 uidNumber: 11000 givenName: Zera objectClass: posixAccount objectClass: top objectClass: shadowAccount objectClass: hostObject objectClass: radiusprofile objectClass: inetOrgPerson objectClass: ldapPublicKey uid: zera loginShell: /bin/bash homeDirectory: /lhome/zera displayName: Zera Nalika Automounting Home Directories \u00b6 If you chose to do /lhome NFS mounting, proceed here. # mkdir /lhome # semanage fcontext -a -t autofs_t \"/lhome(/.*)?\" # restorecon -v /lhome # setsebool use_nfs_home_dirs 1 Now, let's get our automounting setup. # vi /etc/auto.master . . . /lhome /etc/auto.lhome # Add this under the /misc line Let's copy the misc template and make a change to it. # cp /etc/auto.misc /etc/auto.lhome # vi /etc/auto.lhome # Comment the cd line, and add our mount under it. #cd -fstype=iso9660,ro,nosuid,nodev :/dev/cdrom * -rw,soft,intr zera1.angelsofclockwork.net:/lhome/& # restorecon -v /etc/auto.lhome # systemctl enable autofs # systemctl start autofs Let's make our change to the idmapd configuration. # vi /etc/idmapd.conf #Domain = local.domain.edu Domain = zera1.angelsofclockwork.net # systemctl restart sssd autofs LDAP Structure Add-ons \u00b6 Here you'll find my value-added portions of getting LDAP going further than what the above presented. SUDO \u00b6 Getting SUDO to work in LDAP can be a real pain. It doesn't have to be. The default sudo schema provided by the LDAP packages, which I have taken and converted into the proper olc format. dn: cn=sudo,cn=schema,cn=config objectClass: olcSchemaConfig cn: sudo olcAttributeTypes: {0}( 1.3.6.1.4.1.15953.9.1.1 NAME 'sudoUser' DESC 'User(s) who may run sudo' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMa tch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) olcAttributeTypes: {1}( 1.3.6.1.4.1.15953.9.1.2 NAME 'sudoHost' DESC 'Host(s) who may run sudo' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMat ch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) olcAttributeTypes: {2}( 1.3.6.1.4.1.15953.9.1.3 NAME 'sudoCommand' DESC 'Comma nd(s) to be executed by sudo' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1 466.115.121.1.26 ) olcAttributeTypes: {3}( 1.3.6.1.4.1.15953.9.1.4 NAME 'sudoRunAs' DESC 'User(s) impersonated by sudo (deprecated)' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1 .4.1.1466.115.121.1.26 ) olcAttributeTypes: {4}( 1.3.6.1.4.1.15953.9.1.5 NAME 'sudoOption' DESC 'Option s(s) followed by sudo' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115 .121.1.26 ) olcAttributeTypes: {5}( 1.3.6.1.4.1.15953.9.1.6 NAME 'sudoRunAsUser' DESC 'Use r(s) impersonated by sudo' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466 .115.121.1.26 ) olcAttributeTypes: {6}( 1.3.6.1.4.1.15953.9.1.7 NAME 'sudoRunAsGroup' DESC 'Gr oup(s) impersonated by sudo' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.14 66.115.121.1.26 ) olcAttributeTypes: {7}( 1.3.6.1.4.1.15953.9.1.8 NAME 'sudoNotBefore' DESC 'Sta rt of time interval for which the entry is valid' EQUALITY generalizedTimeMat ch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 ) olcAttributeTypes: {8}( 1.3.6.1.4.1.15953.9.1.9 NAME 'sudoNotAfter' DESC 'End of time interval for which the entry is valid' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 ) olcAttributeTypes: {9}( 1.3.6.1.4.1.15953.9.1.10 NAME 'sudoOrder' DESC 'an int eger to order the sudoRole entries' EQUALITY integerMatch ORDERING integerOrd eringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 ) olcObjectClasses: {0}( 1.3.6.1.4.1.15953.9.2.1 NAME 'sudoRole' DESC 'Sudoer En tries' SUP top STRUCTURAL MUST cn MAY ( sudoUser $ sudoHost $ sudoCommand $ s udoRunAs $ sudoRunAsUser $ sudoRunAsGroup $ sudoOption $ sudoOrder $ sudoNotB efore $ sudoNotAfter $ description ) ) Save this as sudoschema.ldif and add it in. # ldapadd -Y EXTERNAL -H ldapi:/// -f sudoschema.ldif Let's create our defaults. This will start our sudo OU and give it some defaults. You may change these if you so desire. # vi sudo.ldif dn: ou=SUDOers,dc=angelsofclockwork,dc=net objectClass: top objectClass: organizationalUnit ou: SUDOers dn: cn=defaults,ou=SUDOers,dc=angelsofclockwork,dc=net objectClass: top objectClass: sudoRole cn: defaults description: SUDOers Default values sudoOption: requiretty sudoOption: env_reset sudoOption: env_keep = \"COLORS DISPLAY HOSTNAME HISTSIZE INPUTRC KDEDIR LS_COLORS\" sudoOption: env_keep += \"MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE\" sudoOption: env_keep += \"LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES\" sudoOption: env_keep += \"LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE\" sudoOption: env_keep += \"LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY\" # ldapadd -xWD \"cn=manager,dc=angelsofclockwork,dc=net\" -f sudo.ldif Now, let's create our first SUDO container. It will be for our \\\"admins\\\". We could specify \\\"sudoHost: ALL\\\" if we wanted. But for the example, I chose a couple of hosts. # vi admins.ldif dn: cn=ADMINS,ou=SUDOers,dc=angelsofclockwork,dc=net objectClass: sudoRole cn: ADMINS description: Administration Role sudoCommand: ALL sudoHost: zera2.angelsofclockwork.net sudoHost: zera3.angelsofclockwork.net sudoRunAs: ALL sudoRunAsGroup: ALL sudoRunAsUser: ALL sudoUser: zera # ldapadd -xWD \"cn=manager,dc=angelsofclockwork,dc=net\" -f admins.ldif If you used authselect with the with-sudo option, this should have turned on sss for sudoers. You may want to verify /etc/nsswitch.conf just to be sure. Note SSSD Cache Sometimes SSSD likes to cache things or never update things for whatever reason or another. To get around this, stop sssd, delete everything under /var/lib/sss/db/ and then start sssd again. Now, let's test. [root@zera3 ~]# su - zera [zera@zera3 ~]$ sudo -l [sudo] password for zera: Matching Defaults entries for zera on this host: requiretty, env_reset, env_keep=\"COLORS DISPLAY HOSTNAME HISTSIZE INPUTRC KDEDIR LS_COLORS\", env_keep+=\"MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE\", env_keep+=\"LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES\", env_keep+=\"LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE\", env_keep+=\"LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY\", secure_path=/sbin\\:/bin\\:/usr/sbin\\:/usr/bin, env_reset, requiretty User sokel may run the following commands on this host: (ALL : ALL) ALL Member Groups \u00b6 Member groups are extremely useful, especially for when you're granting permissions to external applications (and SSSD if you wish). # vi modules.ldif dn: cn=module,cn=config objectClass: olcModuleList cn: module olcModulePath: /usr/lib64/openldap olcModuleLoad: memberof.la # vi memberof.ldif dn: olcOverlay=memberof,olcDatabase={2}mdb,cn=config objectClass: olcMemberOf objectClass: olcOverlayConfig objectClass: olcConfig objectClass: top olcOverlay: memberof olcMemberOfDangling: ignore olcMemberOfRefInt: TRUE olcMemberOfGroupOC: groupOfNames olcMemberOfMemberAD: member olcMemberOfMemberOfAD: memberOf # ldapadd -Y EXTERNAL -H ldapi:/// -f modules.ldif # ldapadd -Y EXTERNAL -H ldapI:/// -f memberof.ldif After that, we can now create our groups. Example. dn: cn=Admins,ou=Group,dc=angelsofclockwork,dc=net objectClass: groupOfNames cn: Admins member: uid=chris,ou=People,dc=angelsofclockwork,dc=net member: uid=zera,ou=People,dc=angelsofclockwork,dc=net member: uid=sithlord,ou=People,dc=angelsofclockwork,dc=net In SSSD, we can make some minor changes. ldap_search_base = dc=angelsofclockwork,dc=net?sub?|(memberOf=cn=Admins,ou=Group,dc=angelsofclockwork,dc=net) ldap_access_filter = (|(memberOf=cn=Admins,ou=Group,dc=angelsofclockwork,dc=net)) # Change this to rfc2307 if you are using nis ldap_schema = rfc2307bis enumerate = True # systemctl stop sssd ; rm -rf /var/lib/sss/db/* ; systemctl start sssd If we were to do an ldapsearch, we can see the groups show up. # ldapsearch -x -LLL uid=zera memberOf dn: uid=zera,ou=People,dc=angelsofclockwork,dc=net memberOf: cn=Admins,ou=Group,dc=angelsofclockwork,dc=net Make sure you turn on referential integrity! Referential Integrity \u00b6 Having referential integrity is absolutely important. It basically means that if a user gets deleted, their group membership disappears also. This prevents you from having to clean up manually. # vi module.ldif dn: cn=module,cn=config changetype: modify replace: olcModuleLoad olcModuleLoad: refint.la olcModuleLoad: memberof.la # ldapmodify -Y EXTERNAL -H ldapi:/// -f module.ldif You also need the overlay. An overlay allows certain plugins to work on a DIT. # vi overlay.ldif dn: olcOverlay=refint,olcDatabase={2}mdb,cn=config objectClass: olcOverlayConfig objectClass: olcConfig objectClass: olcRefintConfig objectClass: top olcOverlay: refint olcRefintAttribute: memberOf member manager # ldapmodify -Y EXTERNAL -H ldapi:/// -f overlay.ldif ACL \u00b6 An ACL (Access Control List) allows permissions to be given to those in the LDAP tree. The problem with a default LDAP setup is that, attributes like userPassword show up in an ldapsearch. This gives little protection. So, to get around this issue, we have to create ACLs. Note The Manager's Rights The manager has all rights to the DIT. In previous implementations, I have put him in access controls as a reference and would put \\\"write\\\" as his access. This isn't needed, but it doesn't hurt to have it. This ldif creates an ACL that allows the Admins group to do anything they want on the DIT (similar to manager). This also prevents anonymous searches from pulling up a user's password. # vi acl.ldif dn: olcDatabase={2}mdb,cn=config changetype: modify replace: olcAccess olcAccess: {0}to attrs=userPassword,shadowLastChange by group.exact=\"cn=Admins,ou=Group,dc=angelsofclockwork,dc=net\" write by anonymous auth by self write by * none break olcAccess: {2}to * by group.exact=\"cn=Admins,ou=Group,dc=angelsofclockwork,dc=net\" write by * read olcAccess: {3}to dn.base=\"\" by * read # ldapmodify -Y EXTERNAL -H ldapi:/// -f acl.ldif It's highly recommended, however, to disable anonymous searching, especially if you go production with LDAP. A lot of LDAP implementations disallow anonymous searching by default. You can do this with ACLs, but it's not recommended. We cover this in the search. Disable Anonymous Binding \u00b6 It's recommended to disable anonymous searching. This can be handled by making a modification to the global configuration and the DIT configuration. dn: cn=config changetype: modify add: olcDisallows olcDisallows: bind_anon dn: olcDatabase={2}mdb,cn=config changetype: modify add: olcRequires olcRequires: authc Once you add this in, all anonymous searching will cease. # ldapsearch -x -LLL uid=zera ldap_bind: Inappropriate authentication (48) additional info: anonymous bind disallowed LDAP Logging \u00b6 Logging is of course, very important for an LDAP server. There are a few types of logs we can do. There are the standard logs and then there are also audit logs. Audit logs allow an administrator to view changes being done to LDAP in an LDIF form. We can setup both. Let's create our modification LDIF. This will turn on standard logging and enable the audit module. Run an ldapmodify against this LDIF. dn: cn=config changetype: modify replace: olcLogFile olcLogFile: /var/log/ldap-standard.log - replace: olcLogLevel olcLogLevel: 256 # Keep in mind, if you have other modules being loaded, # add them to the list dn: cn=module,cn=config changetype: modify replace: olcModuleLoad olcModuleLoad: refint.la olcModuleLoad: memberof.la olcModuleLoad: auditlog.la Now, we need to make sure audit logging is done on our database. dn: olcOverlay=auditlog,olcDatabase={2}mdb,cn=config objectClass: olcAuditlogConfig objectClass: olcOverlayConfig olcOverlay: auditlog olcAuditlogFile: /var/log/ldap-audit.log It's recommended to have logrotate working for our logs. Here is a file I've dropped into /etc/logrotate.d. Experiment with these options. Since I work in an environment that has tons of transactions going all the time, and thus, my rotations are at 100M and 250M respectively. /var/log/ldap-standard.log { missingok compress notifempty daily rotate 10 size=100M } /var/log/ldap-audit.log { missingok compress notifempty daily rotate 10 size=250M } In /etc/rsyslog.conf, optionally, you can create this. If you find that logs are not appearing after the changes above, use this. local4.* /var/log/ldap.log Password Policy \u00b6 Password policies are a great asset, especially when working in an environment that have or require security policies. First, let's load our module and then add our overlay. This LDIF will do both. You may want to remove the comments before adding. dn: cn=module,cn=config changetype: modify replace: olcModuleLoad olcModuleLoad: refint.la olcModuleLoad: memberof.la olcModuleLoad: auditlog.la olcModuleLoad: ppolicy.la dn: olcOverlay=ppolicy,olcDatabase={2}mdb,cn=config objectClass: olcOverlayConfig objectClass: olcPPolicyConfig olcOverlay: ppolicy olcPPolicyDefault: cn=default,ou=policies,dc=angelsofclockwork,dc=net # Set the below to TRUE if you want users to get locked out after failed attempted olcPPolicyUseLockout: TRUE # Set the below to TRUE if you want passwords to be hashed. # HIGHLY RECOMMENDED YOU SET THIS TO TRUE olcPPolicyHashCleartext: TRUE Now, we need an LDIF to create our standard password policy. It's important to have a default password policy and then create separate ones as needed. Make sure to read the comments. You may want to remove the comments before adding. dn: cn=default,ou=policies,dc=angelsofclockwork,dc=net objectClass: pwdPolicy objectClass: person objectClass: top cn: passwordDefault sn: passwordDefault pwdAttribute: userPassword # If set to 0, quality is not checked. # If set to 1, quality is checked by an internal module which you setup. # If set to 2, the system used to change the password must have a checking mechanism. # Pick your poison. pwdCheckQuality: 0 # Password lives for 84 days pwdMinAge: 0 pwdMaxAge: 7257600 # Minimum length is 7 pwdMinLength: 7 # Password history of 10, cannot use a password that's in history pwdInHistory: 10 # 5 Failures till a lockout, 10 minutes for it to reset, 30 minute lockout. pwdMaxFailure: 5 pwdFailureCountInterval: 600 pwdLockout: TRUE pwdLockoutDuration: 1800 # A user can change their own password. pwdAllowUserChange: TRUE # Systems that authenticate to LDAP can warn 14 days before an expiration pwdExpireWarning: 1209600 # Allowed binds on an expired password. pwdGraceAuthNLimit: 5 pwdMustChange: TRUE pwdSafeModify: FALSE In the instance you want to use the built-in module for password checking, your LDIF would have these lines. pwdCheckQuality: 1 pwdCheckModule: check_password.so","title":"OpenLDAP"},{"location":"el/openldap/#deprecation","text":"Please note that the OpenLDAP server is considered deprecated in RHEL (and thusly other EL derivatives). This document will stay here as a reference for those who are still using the server software on Enterprise Linux 7, potentially using the OpenLDAP LTB version of the software, or using the openldap-servers package from Rocky Linux 9's plus repository. It may apply to Fedora users in some contexts, but there are some differences they may never be documented here. If you see a need for corrections, please open up an issue on our github. Most of this information should be considered out of date. OpenLDAP 2.6.x makes some changes that the below document may or may not cover.","title":"Deprecation"},{"location":"el/openldap/#preface","text":"This tutorial goes over how to install OpenLDAP to a Enterprise Linux Server and options on configuring and setting up accounts for host access, etc. This how-to is the method of implementation that I used, and can be modified/changed to any users preferences if preferred. Enterprise Linux and Fedora users will have the ability to use SSSD for SUDO (and it's relatively easy to set it up).","title":"Preface"},{"location":"el/openldap/#overview","text":"Simply put, LDAP is a directory service for authentication across a network. Rather than have local accounts on a bunch of machines, LDAP can be used to have one account across a bunch of machines. LDAP was once an easy setup in RHEL 5 but had changed in RHEL 6 and 7, and here provides the necessary information needed to get a simple LDAP system running with possible SUDO support and various options of how to support your LDAP system.","title":"Overview"},{"location":"el/openldap/#requirements","text":"First and foremost, we have a list of requirements. Keep in mind, if you do not fulfill these requirements, you may run into some issues down the road. Enterprise Linux 8, Enterprise Linux 9 DNS Server (LDAP does NOT appreciate IP addresses for the URI) An active internet connection to install the packages required","title":"Requirements"},{"location":"el/openldap/#tutorial-preface-notes-and-recommendations","text":"Warning Potential Pitfalls! The incorrect configuration in your firewall or other settings can cause login failures Not using certificates (TLS/SSL) will cause you not to be able to login (This is a EL7+ LDAP Client Requirement) SELinux is a pain when using mounted home directories or certificates (primarily in Enterprise Linux 7+, will you have problems with certificates/home directories) If you use /home as an NFS mount, you'll see some nasty side effects if you have local users. Note Recommended Information It's recommended to use colored vim syntax. Root doesn't use vim when vim-enhanced is installed. You can make an alias for vi to run vim. Turn on syntaxing in ~/.vimrc -- syntax on Make the vim colors brighter in ~/.vimrc -- set background=dark Export your EDITOR variable in ~/.bash_profile -- export EDITOR=vim Keep selinux set to enforcing Note Database Information We will be using lmdb, which is recommended over using hdb or bdb. Note EL 9 OpenLDAP Enterprise Linux 9 has fully dropped OpenLDAP where there are no server packages normally built. In some EL distributions, openldap-servers is still built and provided in an extra repository. Rocky Linux 9 has openldap-servers in their plus repository. EPEL also has it available.","title":"Tutorial Preface, Notes, and Recommendations"},{"location":"el/openldap/#installation","text":"Below details the process for installing OpenLDAP to our system(s). This includes installing the packages, setting up certificates, and configuring the LDAP server via LDIF files.","title":"Installation"},{"location":"el/openldap/#packages","text":"You will need the following packages. A couple of them may already be installed. If you don't plan on migrating local accounts to LDAP, you can leave out migrationtools. # If you are on Rocky Linux 9, you will need to enable the plus repository dnf install openldap openldap-servers migrationtools nss-tools -y","title":"Packages"},{"location":"el/openldap/#certificates","text":"Enterprise Linux 7 clients and other newer distributions that are non-el require TLS/SSL for authentication when going toward LDAP. because of this, we will need to create certificates, regardless if you are in a lab or not. Note Certificate Information NSS should no longer be required. Anything NSS related has been removed. I have two ways of doing it, we can do it manually or through a script. I prefer using my script to take care of it. First the manual way. mkdir /etc/pki/ldap cd /etc/pki/ldap openssl genrsa -des3 -out ca.key 4096 # Remember the password you put here openssl genrsa -out ldapserver.key 4096 openssl req -new -x509 -key ca.key -out ca.pem -days 3650 Country Name (2 letter code) [XX]:US State or Province Name (full name) []:Arizona Locality Name (eg, city) [Default City]:Phoenix Organization Name (eg, company) [Default Company Ltd]:SSN Studio Organizational Unit Name (eg, section) []:Channel Maintainers Common Name (eg, your name or your server's hostname) []:SSN # If you want to use a server name here, perform this step on another server first Email Address []:youremail@mail.com openssl req -new -key ldapserver.key -out ldapserver.csr Country Name (2 letter code) [XX]:US State or Province Name (full name) []:Arizona Locality Name (eg, city) [Default City]:Phoenix Organization Name (eg, company) [Default Company Ltd]:SSN Studio Organizational Unit Name (eg, section) []:LDAP Server Maintainer Common Name (eg, your name or your server's hostname) []:zera1.angelsofclockwork.net # Set your common name to your server name for this certificate Email Address []:youremail@mail.com openssl x509 -req -in ldapserver.csr -out ldapserver.pem -CA ca.pem -CAkey ca.key -days 3650 -set_serial 01 ln -s ca.pem `openssl x509 -hash -in ca.pem -noout`.0 # Do an ls on the directory and save the hashed name including the .0 somewhere certutil -N -d /etc/pki/ldap # Do not enter any passwords. When asked, just hit enter beyond this point. chown root:ldap * chmod 640 * The scripted way. #!/bin/bash # CA Information CAcountry=\"US\" CAstate=\"Arizona\" CAlocale=\"Phoenix\" CAorganization=\"SSN Studio\" CAorganizationalunit=\"Channel Maintainers\" # If you set the below to a hostname, you\u2019re screwed. Don\u2019t do it! # Only do it if you have an actual hostname you will do CA signing on! CAconicalname=\"SSN\" CAemail=\"tucklesepk@gmail.com\" # LDAP Server information country=\"US\" state=\"Arizona\" locale=\"Phoenix\" organization=\"SSN Studio\" organizationalunit=\"LDAP Server Maintainer\" conicalname=\"zera1.angelsofclockwork.net\" email=\"pc68xl@gmail.com\" certdir=\"/etc/pki/ldap\" mkdir $certdir ; cd $certdir echo \"Enter a password when asked.\" openssl genrsa -des3 -out ca.key 4096 openssl genrsa -out ldapserver.key 4096 # Create the self-signed CA cert openssl req -new -x509 -key ca.key -out ca.pem -days 3650 -subj /C=\"$CAcountry\"/ST=\"$CAstate\"/L=\"$CAlocale\"/O=\"$CAorganization\"/OU=\"$CAorganizationalunit\"/CN=\"$CAconicalname\"/emailAddress=\"$CAemail\"/ # Create the LDAP server cert openssl req -new -key ldapserver.key -out ldapserver.csr -subj /C=\"$country\"/ST=\"$state\"/L=\"$locale\"/O=\"$organization\"/OU=\"$organizationalunit\"/CN=\"$conicalname\"/emailAddress=\"$email\"/ # Sign it openssl x509 -req -in ldapserver.csr -out ldapserver.pem -CA ca.pem -CAkey ca.key -days 3650 -set_serial 01 ln -s ca.pem `openssl x509 -hash -in ca.pem -noout`.0 chown root:ldap * chmod 640 * Make sure to obtain your hash. Your hash will be different from mine. ls -l /etc/pki/ldap | grep '0' 39642ab3.0","title":"Certificates"},{"location":"el/openldap/#ldap-server-configuration","text":"Configurations done in OpenLDAP are done via LDIF. Your passwords should be hashed as well. Before we begin, let's start by generating a password for our root DN. This is required. slappasswd New password: Re-enter new password: {SSHA}CuaKctEx7rl/+ldG0EjktMzJdrxNc46+ Keep this SSHA output for our configuration files. Next, we'll need to make a couple LDIFs. This is our suffix.ldif file. This file helps to create the mdb database for our LDAP structure. It also sets our DIT suffix, root password, etc. You should change the olcSuffix, olcRootDN, and olcRootPW to whatever you plan on using. The olcDbMaxSize is set to 20GB. This is normally sufficient and can be changed. The olcDbEnvFlags can be changed as well. dn: olcDatabase=mdb,cn=config objectClass: olcDatabaseConfig objectClass: olcMdbConfig olcDatabase: mdb olcDbDirectory: /var/lib/ldap olcSuffix: dc=angelsofclockwork,dc=net olcRootDN: cn=manager,dc=angelsofclockwork,dc=net olcRootPW: {SSHA}CuaKctEx7rl/+ldG0EjktMzJdrxNc46+ olcDbIndex: objectClass eq,pres olcDbIndex: ou,cn,mail,surname,givenname eq,pres,sub olcLastMod: TRUE olcDbEnvFlags: nometasync olcDbEnvFlags: writemap olcDbMaxSize: 21474836480 Now, below we have our primary modification ldif. Comments describe what each one does. # Sets our cert path and information # The \"CertificateFile\" has to be set to the hostname of the LDAP server dn: cn=config changetype: modify replace: olcTLSCACertificatePath olcTLSCACertificatePath: /etc/pki/ldap - replace: olcTLSCertificateFile olcTLSCertificateFile: zera1.angelsofclockwork.net - replace: olcTLSCertificateKeyFile olcTLSCertificateKeyFile: /etc/pki/ldap/ldapserver.key # Adding a rootDN for the config. # Note that this isn't fully necessary as you can use -Y EXTERNAL -H ldapi:/// instead # So, treat this as an optional thing. If you do want it, consider a different password. dn: olcDatabase={0}config,cn=config changetype: modify replace: olcRootDN olcRootDN: cn=config - replace: olcRootPW olcRootPW: {SSHA}CuaKctEx7rl/+ldG0EjktMzJdrxNc46+ # Set the password again in the mdb database # This is because sometimes the password set when making the database doesn't 'work' sometimes dn: olcDatabase={2}mdb,cn=config changetype: modify replace: olcRootPW olcRootPW: {SSHA}CuaKctEx7rl/+ldG0EjktMzJdrxNc46+ # Sets the default password hash to SSHA -- Refer to the 'bug' information if this does not work dn: olcDatabase={-1}frontend,cn=config changetype: modify replace: olcPasswordHash olcPasswordHash: {SSHA} # Changes the rootdn information in the monitor database dn: olcDatabase={1}monitor,cn=config changetype: modify replace: olcAccess olcAccess: {0}to * by dn.base=\"gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth\" read by dn.base=\"cn=manager,dc=angelsofclockwork,dc=net\" read by * none Let's make sure we turn on ldaps. It's recommended to use TLS, but some applications insist on SSL. (Very few, but they are out there.) # vi /etc/sysconfig/slapd . . . SLAPD_URLS=\"ldapi:/// ldap:/// ldaps:///\" # slaptest -u Config file testing succeeded # /etc/openldap/ldap.conf . . . TLS_CACERTDIR /etc/pki/ldap Now, we need to add our LDIFs into LDAP. rm -f /etc/openldap/slapd.d/cn=config/olcDatabase=\\{2\\}hdb.ldif chown -R ldap:ldap /var/lib/ldap systemctl enable slapd systemctl start slapd ldapadd -Y EXTERNAL -H ldapi:/// -f suffix.ldif ldapmodify -Y EXTERNAL -H ldapi:/// -f info.ldif You may end up getting a checksum error in your logs. To solve this, you need to do a simple operation against the configuration. ldapmodify -h localhost -xWD \"cn=config\" Enter LDAP Password: dn: olcDatabase={0}config,cn=config changetype: modify replace: olcRootDN olcRootDN: cn=config modifying entry \"olcDatabase={0}config,cn=config\" slaptest -u config file testing succeeded That should do it. You can do a -Y EXTERNAL -H ldapi:/// instead if you wanted to. I did the above to show passwords will work for config.","title":"LDAP Server Configuration"},{"location":"el/openldap/#ldap-structure","text":"The next piece is to get our backend structure built. In EL7, core is the only schema that is there. In EL6, it's a good chunk of these. I like to put them in a file so I can loop through them. Note ppolicy schema As of OpenLDAP 2.6.x, the ppolicy schema no longer applies as it is built-in to the slapo-ppolicy module. See the upgrade document for information. /etc/openldap/schema/corba.ldif /etc/openldap/schema/cosine.ldif /etc/openldap/schema/duaconf.ldif /etc/openldap/schema/dyngroup.ldif /etc/openldap/schema/inetorgperson.ldif /etc/openldap/schema/java.ldif /etc/openldap/schema/misc.ldif /etc/openldap/schema/nis.ldif /etc/openldap/schema/openldap.ldif /etc/openldap/schema/collective.ldif Note rfc2307 If you want to be able to combine groupOfNames and posixGroup together (similar to Active Directory, other open source, and commercial offerings), don't use nis. Use the rfc2307bis schema instead. Once you have your list of schema to put in, we can loop through them. for x in $(cat schemaorder) ; do ldapadd -Y EXTERNAL -H ldapi:/// -f $x ; done adding new entry \"cn=corba,cn=schema,cn=config\" adding new entry \"cn=cosine,cn=schema,cn=config\" adding new entry \"cn=duaconf,cn=schema,cn=config\" adding new entry \"cn=dyngroup,cn=schema,cn=config\" adding new entry \"cn=inetorgperson,cn=schema,cn=config\" adding new entry \"cn=java,cn=schema,cn=config\" adding new entry \"cn=misc,cn=schema,cn=config\" adding new entry \"cn=nis,cn=schema,cn=config\" adding new entry \"cn=openldap,cn=schema,cn=config\" adding new entry \"cn=collective,cn=schema,cn=config\" I normally like to keep all LDIFs in a folder by themselves to avoid clutter (non-configuration LDIF). mkdir ldif ; cd ldif Let's get our base created. Make sure to replace my DN with your DN that you chose earlier. Call this base.ldif. dn: dc=angelsofclockwork,dc=net dc: angelsofclockwork objectClass: top objectClass: domain dn: ou=People,dc=angelsofclockwork,dc=net ou: People objectClass: top objectClass: organizationalUnit dn: ou=Group,dc=angelsofclockwork,dc=net ou: Group objectClass: top objectClass: organizationalUnit ldapadd -xWD \"cn=manager,dc=angelsofclockwork,dc=net\" -f base.ldif Enter LDAP Password: adding new entry \"dc=angelsofclockwork,dc=net\" adding new entry \"ou=People,dc=angelsofclockwork,dc=net\" adding new entry \"ou=Group,dc=angelsofclockwork,dc=net\" If this doesn't add, make sure your LDAP server is running, check /var/log/messages, and ensure you've completed all steps before this. ldapsearch -x -LLL -b 'dc=angelsofclockwork,dc=net' dn: dc=angelsofclockwork,dc=net dc: angelsofclockwork objectClass: top objectClass: domain dn: ou=People,dc=angelsofclockwork,dc=net ou: People objectClass: top objectClass: organizationalUnit dn: ou=Group,dc=angelsofclockwork,dc=net ou: Group objectClass: top objectClass: organizationalUnit","title":"LDAP Structure"},{"location":"el/openldap/#add-users-via-migration","text":"Note But... I don't want to add my users locally You don't have to add your users locally to the system. This just aids in the creation of users. Go to the next section if you want to add users and do permissions by hand. This is the fun part. We'll need to add some users, set some passwords and migrate them into the LDAP system. I'll make three users as an example, give them an ID starting at 10000, home directories in /lhome, set a password, and proceed to migrate them. If you don't want to use /lhome, keep them set to /home and their home directories should get created automatically when logging into another machine. # mkdir /lhome # mkdir ldif/user # semanage fcontext -a -t home_root_t \"/lhome(/.*)?\" # restorecon -v /lhome \\ restorecon reset /lhome context unconfined_u:object_r:default_t:s0->unconfined_u:object_r:home_root_t:s0 # groupadd -g 10000 sokel # groupadd -g 10001 suree # groupadd -g 10002 ranos # useradd -u 10000 -g 10000 -d /lhome/sokel sokel # useradd -u 10001 -g 10001 -d /lhome/suree suree # useradd -u 10002 -g 10002 -d /lhome/ranos ranos # passwd sokel ; passwd suree ; passwd ranos # cat /etc/passwd | grep sokel > ldif/user/passwd.sokel # cat /etc/passwd | grep suree > ldif/user/passwd.suree # cat /etc/passwd | grep ranos > ldif/user/passwd.ranos # cat /etc/group | grep sokel > ldif/user/group.sokel # cat /etc/group | grep suree > ldif/user/group.suree # cat /etc/group | grep ranos > ldif/user/group.ranos We'll set some aliases for our migration scripts too # alias miguser='/usr/share/migrationtools/migrate_passwd.pl' # alias miggroup='/usr/share/migrationtools/migrate_group.pl' Before we continue, we need to modify our migration scripts. This is extremely important, otherwise our LDIFs will come out incorrect. Change them to your DN. # sed -i.bak \"s/padl.com/angelsofclockwork.net/g\" /usr/share/migrationtools/migrate_common.ph # sed -i.bak \"s/padl,dc=com/angelsofclockwork,dc=net/g\" /usr/share/migrationtools/migrate_common.ph Now we can use a loop to convert them. You can do it by hand also, but that's up to you. # for x in sokel suree ranos ; do miguser ldif/user/passwd.$x > ldif/user/$x.ldif ; done # for x in sokel suree ranos ; do miggroup ldif/user/group.$x >> ldif/user/$x.ldif ; done # cd ldif/user/ # cat *.ldif >> /tmp/ourusers.ldif # ldapadd -xWD \"cn=manager,dc=angelsofclockwork,dc=net\" -f /tmp/ourusers.ldif Enter LDAP Password: adding new entry \"uid=ranos,ou=People,dc=angelsofclockwork,dc=net\" adding new entry \"cn=ranos,ou=Group,dc=angelsofclockwork,dc=net\" adding new entry \"uid=sokel,ou=People,dc=angelsofclockwork,dc=net\" adding new entry \"cn=sokel,ou=Group,dc=angelsofclockwork,dc=net\" adding new entry \"uid=suree,ou=People,dc=angelsofclockwork,dc=net\" adding new entry \"cn=suree,ou=Group,dc=angelsofclockwork,dc=net\" The manual way. # /usr/share/migrationtools/migrate_passwd.pl ldif/user/passwd.sokel > ldif/user/sokel.ldif # /usr/share/migrationtools/migrate_group.pl ldif/user/group.sokel >> ldif/user/sokel.ldif # /usr/share/migrationtools/migrate_passwd.pl ldif/user/passwd.suree > ldif/user/suree.ldif # /usr/share/migrationtools/migrate_group.pl ldif/user/group.suree >> ldif/user/suree.ldif # /usr/share/migrationtools/migrate_passwd.pl ldif/user/passwd.ranos > ldif/user/ranos.ldif # /usr/share/migrationtools/migrate_group.pl ldif/user/group.ranos >> ldif/user/ranos.ldif # cd ldif/user/ # ls group.ranos group.suree passwd.sokel ranos.ldif suree.ldif group.sokel passwd.ranos passwd.suree sokel.ldif # ldapadd -xWD \"cn=manager,dc=angelsofclockwork,dc=net\" -f sokel.ldif Enter LDAP Password: adding new entry \"uid=sokel,ou=People,dc=angelsofclockwork,dc=net\" adding new entry \"cn=sokel,ou=Group,dc=angelsofclockwork,dc=net\" # ldapadd -xWD \"cn=manager,dc=angelsofclockwork,dc=net\" -f suree.ldif Enter LDAP Password: adding new entry \"uid=suree,ou=People,dc=angelsofclockwork,dc=net\" adding new entry \"cn=suree,ou=Group,dc=angelsofclockwork,dc=net\" # ldapadd -xWD \"cn=manager,dc=angelsofclockwork,dc=net\" -f ranos.ldif Enter LDAP Password: adding new entry \"uid=ranos,ou=People,dc=angelsofclockwork,dc=net\" adding new entry \"cn=ranos,ou=Group,dc=angelsofclockwork,dc=net\"","title":"Add Users via Migration"},{"location":"el/openldap/#add-users-via-ldif","text":"This is for those who don't want to create the account locally. For each user, you need to create an LDIF that satisfies their account information such as UID, GID and their group information. If you plan on having NFS exports to /lhome, make sure homeDirectory is correctly pointing as such. Otherwise, keep it as /home/username. dn: uid=zera,ou=People,dc=angelsofclockwork,dc=net objectClass: posixAccount objectClass: top objectClass: shadowAccount objectClass: inetOrgPerson cn: Zera Nalika gidNumber: 11000 sn: Nalika uidNumber: 11000 givenName: Zera uid: zera loginShell: /bin/bash homeDirectory: /home/zera displayName: Zera Nalika userPassword: changeme2 dn: cn=zera,ou=Group,dc=angelsofclockwork,dc=net objectClass: posixGroup objectClass: top cn: zera gidNumber: 11000 That's about it for that. You create these for each user as needed and then add them into ldap. # ldapadd -xWD \"cn=manager,dc=angelsofclockwork,dc=net\" -f zera.ldif adding new entry \"uid=zera,ou=People,dc=angelsofclockwork,dc=net\" adding new entry \"cn=zera,ou=Group,dc=angelsofclockwork,dc=net\" For users who are doing the /lhome thing, make their directories. When you are changing ownership, do it by UID and GID number. # mkdir /lhome # semanage fcontext -a -t home_root_t \"/lhome(/.*)?\" # mkdir /lhome/zera # cp /etc/skel/.* /lhome/zera # chown -R 11000:11000 /lhome/zera # restorecon -Rv /lhome","title":"Add Users via LDIF"},{"location":"el/openldap/#nfs-export-home-directories","text":"Warning /home vs /lhome If you used /lhome and you want to use NFS mounts, you may continue here. Otherwise, skip this section entirely. If you use /home and still want to do NFS, you will need to do persistent NFS to say /export/home, and then setup AutoFS to use /export/home as a way to automount into /home. Warning Potential Pitfall Do NOT use NFSv3. The steps below show how to prevent user squashing to allow the user to have access to their home directories. Typically, in an NFSv4 fashion, it tends to mount it with permissions set to nobody. Other solutions have been to force NFSv3. This is NOT recommended. YOU HAVE BEEN WARNED. First, we'll need to install nfs-utils, set up our exports, and modify our id map file. # dnf install nfs-utils libnfsidmap -y # vi /etc/exports /lhome *(rw,sync,root_squash,no_all_squash) # vi /etc/idmapd.conf # Comment out the first Domain line and make your own Domain = zera1.angelsofclockwork.net # systemctl start nfs-server # systemctl enable nfs-server Sometimes you'll still run into the nobody problem. Sometimes this helps. # vi /etc/sysconfig/nfs NEED_IDMAPD=yes NFSMAPID_DOMAIN=library.angelsofclockwork.net","title":"NFS Export Home Directories"},{"location":"el/openldap/#firewall","text":"Warning Keep your firewall on It is bad practice to turn your firewall off. Don't do it. We need to open up our firewall. Note Port Reference LDAP Ports: 389/tcp 636/tcp NFS Ports: 111/tcp 111/udp 2049/tcp If using firewalld, you can add these ports by service. # firewall-cmd --add-service=ldap --zone=public --permanent # firewall-cmd --add-service=ldaps --zone=public --permanent # firewall-cmd --add-service=nfs --zone=public --permanent # firewall-cmd --reload","title":"Firewall"},{"location":"el/openldap/#client","text":"Setting up the client can be straight-forward or troubling, depending on the distribution you're using. We'll be going over EL7+. Fedora also works here as well. Warning Third-party Repositories If you use third-party repositories, you may want to disable them, at least temporarily. Depending on the repository, there may be conflicts when installing the appropriate packages. You may want to consider on setting up priorities, and ensure your base and updates are higher than the rest.","title":"Client"},{"location":"el/openldap/#enterprise-linuxcurrent-fedora-releases","text":"We'll be using SSSD for this. We need to install some key packages first. Some of these packages may not install because they were either superceded or obsoleted. # yum install pki-{ca,common,silent} openldap-clients nss-pam-ldapd policycoreutils-python sssd sssd-common sssd-client sssd-ldap Use authselect to configure pam and nss. You'll need to configure /etc/sssd/sssd.conf by hand after. # authselect select sssd with-mkhomedir with-sudo # vi /etc/sssd/sssd.conf [domain/default] cache_credentials = True krb5_realm = # ldap_search_base = dc=angelsofclockwork,dc=net id_provider = ldap auth_provider = ldap chpass_provider = ldap sudo_provider = ldap ldap_uri = ldap://zera1.angelsofclockwork.net ldap_id_use_start_tls = True ldap_tls_cacertdir = /etc/openldap/certs ldap_tls_cacert = /etc/openldap/certs/ca.pem # Add the below ldap_sudo_search_base = ou=SUDOers,dc=angelsofclockwork,dc=net [sssd] # Modify this line and add sudo to the list services = nss, pam, autofs, sudo [sudo] Now, let's get our CA cert that we made way long ago and download it. If you used a real CA to sign your certificate, obtain their certificate. Note Hash Remember your hash from when you were making your certificates? You need to obtain it. In both examples, we created it while using a symbolic link. # scp zera1.angelsofclockwork.net:/etc/pki/ldap/ca.pem /etc/openldap/certs/ca.pem # cd /etc/openldap/certs # ln -s ca.pem 39642ab3.0 Now, modify /etc/openldap/ldap.conf and add the following to the bottom, ensuring your BASE is set correctly. URI ldap://library.angelsofclockwork.net BASE dc=angelsofclockwork,dc=net ssl start_tls You can attempt an ldapsearch and it should work. Search for one of your users. # ldapsearch -x -LLL uid=zera dn: uid=zera,ou=People,dc=angelsofclockwork,dc=net cn: Zera Nalika gidNumber: 11000 uidNumber: 11000 givenName: Zera objectClass: posixAccount objectClass: top objectClass: shadowAccount objectClass: hostObject objectClass: radiusprofile objectClass: inetOrgPerson objectClass: ldapPublicKey uid: zera loginShell: /bin/bash homeDirectory: /lhome/zera displayName: Zera Nalika","title":"Enterprise Linux/Current Fedora Releases"},{"location":"el/openldap/#automounting-home-directories","text":"If you chose to do /lhome NFS mounting, proceed here. # mkdir /lhome # semanage fcontext -a -t autofs_t \"/lhome(/.*)?\" # restorecon -v /lhome # setsebool use_nfs_home_dirs 1 Now, let's get our automounting setup. # vi /etc/auto.master . . . /lhome /etc/auto.lhome # Add this under the /misc line Let's copy the misc template and make a change to it. # cp /etc/auto.misc /etc/auto.lhome # vi /etc/auto.lhome # Comment the cd line, and add our mount under it. #cd -fstype=iso9660,ro,nosuid,nodev :/dev/cdrom * -rw,soft,intr zera1.angelsofclockwork.net:/lhome/& # restorecon -v /etc/auto.lhome # systemctl enable autofs # systemctl start autofs Let's make our change to the idmapd configuration. # vi /etc/idmapd.conf #Domain = local.domain.edu Domain = zera1.angelsofclockwork.net # systemctl restart sssd autofs","title":"Automounting Home Directories"},{"location":"el/openldap/#ldap-structure-add-ons","text":"Here you'll find my value-added portions of getting LDAP going further than what the above presented.","title":"LDAP Structure Add-ons"},{"location":"el/openldap/#sudo","text":"Getting SUDO to work in LDAP can be a real pain. It doesn't have to be. The default sudo schema provided by the LDAP packages, which I have taken and converted into the proper olc format. dn: cn=sudo,cn=schema,cn=config objectClass: olcSchemaConfig cn: sudo olcAttributeTypes: {0}( 1.3.6.1.4.1.15953.9.1.1 NAME 'sudoUser' DESC 'User(s) who may run sudo' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMa tch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) olcAttributeTypes: {1}( 1.3.6.1.4.1.15953.9.1.2 NAME 'sudoHost' DESC 'Host(s) who may run sudo' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMat ch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) olcAttributeTypes: {2}( 1.3.6.1.4.1.15953.9.1.3 NAME 'sudoCommand' DESC 'Comma nd(s) to be executed by sudo' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1 466.115.121.1.26 ) olcAttributeTypes: {3}( 1.3.6.1.4.1.15953.9.1.4 NAME 'sudoRunAs' DESC 'User(s) impersonated by sudo (deprecated)' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1 .4.1.1466.115.121.1.26 ) olcAttributeTypes: {4}( 1.3.6.1.4.1.15953.9.1.5 NAME 'sudoOption' DESC 'Option s(s) followed by sudo' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115 .121.1.26 ) olcAttributeTypes: {5}( 1.3.6.1.4.1.15953.9.1.6 NAME 'sudoRunAsUser' DESC 'Use r(s) impersonated by sudo' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466 .115.121.1.26 ) olcAttributeTypes: {6}( 1.3.6.1.4.1.15953.9.1.7 NAME 'sudoRunAsGroup' DESC 'Gr oup(s) impersonated by sudo' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.14 66.115.121.1.26 ) olcAttributeTypes: {7}( 1.3.6.1.4.1.15953.9.1.8 NAME 'sudoNotBefore' DESC 'Sta rt of time interval for which the entry is valid' EQUALITY generalizedTimeMat ch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 ) olcAttributeTypes: {8}( 1.3.6.1.4.1.15953.9.1.9 NAME 'sudoNotAfter' DESC 'End of time interval for which the entry is valid' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 ) olcAttributeTypes: {9}( 1.3.6.1.4.1.15953.9.1.10 NAME 'sudoOrder' DESC 'an int eger to order the sudoRole entries' EQUALITY integerMatch ORDERING integerOrd eringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 ) olcObjectClasses: {0}( 1.3.6.1.4.1.15953.9.2.1 NAME 'sudoRole' DESC 'Sudoer En tries' SUP top STRUCTURAL MUST cn MAY ( sudoUser $ sudoHost $ sudoCommand $ s udoRunAs $ sudoRunAsUser $ sudoRunAsGroup $ sudoOption $ sudoOrder $ sudoNotB efore $ sudoNotAfter $ description ) ) Save this as sudoschema.ldif and add it in. # ldapadd -Y EXTERNAL -H ldapi:/// -f sudoschema.ldif Let's create our defaults. This will start our sudo OU and give it some defaults. You may change these if you so desire. # vi sudo.ldif dn: ou=SUDOers,dc=angelsofclockwork,dc=net objectClass: top objectClass: organizationalUnit ou: SUDOers dn: cn=defaults,ou=SUDOers,dc=angelsofclockwork,dc=net objectClass: top objectClass: sudoRole cn: defaults description: SUDOers Default values sudoOption: requiretty sudoOption: env_reset sudoOption: env_keep = \"COLORS DISPLAY HOSTNAME HISTSIZE INPUTRC KDEDIR LS_COLORS\" sudoOption: env_keep += \"MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE\" sudoOption: env_keep += \"LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES\" sudoOption: env_keep += \"LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE\" sudoOption: env_keep += \"LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY\" # ldapadd -xWD \"cn=manager,dc=angelsofclockwork,dc=net\" -f sudo.ldif Now, let's create our first SUDO container. It will be for our \\\"admins\\\". We could specify \\\"sudoHost: ALL\\\" if we wanted. But for the example, I chose a couple of hosts. # vi admins.ldif dn: cn=ADMINS,ou=SUDOers,dc=angelsofclockwork,dc=net objectClass: sudoRole cn: ADMINS description: Administration Role sudoCommand: ALL sudoHost: zera2.angelsofclockwork.net sudoHost: zera3.angelsofclockwork.net sudoRunAs: ALL sudoRunAsGroup: ALL sudoRunAsUser: ALL sudoUser: zera # ldapadd -xWD \"cn=manager,dc=angelsofclockwork,dc=net\" -f admins.ldif If you used authselect with the with-sudo option, this should have turned on sss for sudoers. You may want to verify /etc/nsswitch.conf just to be sure. Note SSSD Cache Sometimes SSSD likes to cache things or never update things for whatever reason or another. To get around this, stop sssd, delete everything under /var/lib/sss/db/ and then start sssd again. Now, let's test. [root@zera3 ~]# su - zera [zera@zera3 ~]$ sudo -l [sudo] password for zera: Matching Defaults entries for zera on this host: requiretty, env_reset, env_keep=\"COLORS DISPLAY HOSTNAME HISTSIZE INPUTRC KDEDIR LS_COLORS\", env_keep+=\"MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE\", env_keep+=\"LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES\", env_keep+=\"LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE\", env_keep+=\"LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY\", secure_path=/sbin\\:/bin\\:/usr/sbin\\:/usr/bin, env_reset, requiretty User sokel may run the following commands on this host: (ALL : ALL) ALL","title":"SUDO"},{"location":"el/openldap/#member-groups","text":"Member groups are extremely useful, especially for when you're granting permissions to external applications (and SSSD if you wish). # vi modules.ldif dn: cn=module,cn=config objectClass: olcModuleList cn: module olcModulePath: /usr/lib64/openldap olcModuleLoad: memberof.la # vi memberof.ldif dn: olcOverlay=memberof,olcDatabase={2}mdb,cn=config objectClass: olcMemberOf objectClass: olcOverlayConfig objectClass: olcConfig objectClass: top olcOverlay: memberof olcMemberOfDangling: ignore olcMemberOfRefInt: TRUE olcMemberOfGroupOC: groupOfNames olcMemberOfMemberAD: member olcMemberOfMemberOfAD: memberOf # ldapadd -Y EXTERNAL -H ldapi:/// -f modules.ldif # ldapadd -Y EXTERNAL -H ldapI:/// -f memberof.ldif After that, we can now create our groups. Example. dn: cn=Admins,ou=Group,dc=angelsofclockwork,dc=net objectClass: groupOfNames cn: Admins member: uid=chris,ou=People,dc=angelsofclockwork,dc=net member: uid=zera,ou=People,dc=angelsofclockwork,dc=net member: uid=sithlord,ou=People,dc=angelsofclockwork,dc=net In SSSD, we can make some minor changes. ldap_search_base = dc=angelsofclockwork,dc=net?sub?|(memberOf=cn=Admins,ou=Group,dc=angelsofclockwork,dc=net) ldap_access_filter = (|(memberOf=cn=Admins,ou=Group,dc=angelsofclockwork,dc=net)) # Change this to rfc2307 if you are using nis ldap_schema = rfc2307bis enumerate = True # systemctl stop sssd ; rm -rf /var/lib/sss/db/* ; systemctl start sssd If we were to do an ldapsearch, we can see the groups show up. # ldapsearch -x -LLL uid=zera memberOf dn: uid=zera,ou=People,dc=angelsofclockwork,dc=net memberOf: cn=Admins,ou=Group,dc=angelsofclockwork,dc=net Make sure you turn on referential integrity!","title":"Member Groups"},{"location":"el/openldap/#referential-integrity","text":"Having referential integrity is absolutely important. It basically means that if a user gets deleted, their group membership disappears also. This prevents you from having to clean up manually. # vi module.ldif dn: cn=module,cn=config changetype: modify replace: olcModuleLoad olcModuleLoad: refint.la olcModuleLoad: memberof.la # ldapmodify -Y EXTERNAL -H ldapi:/// -f module.ldif You also need the overlay. An overlay allows certain plugins to work on a DIT. # vi overlay.ldif dn: olcOverlay=refint,olcDatabase={2}mdb,cn=config objectClass: olcOverlayConfig objectClass: olcConfig objectClass: olcRefintConfig objectClass: top olcOverlay: refint olcRefintAttribute: memberOf member manager # ldapmodify -Y EXTERNAL -H ldapi:/// -f overlay.ldif","title":"Referential Integrity"},{"location":"el/openldap/#acl","text":"An ACL (Access Control List) allows permissions to be given to those in the LDAP tree. The problem with a default LDAP setup is that, attributes like userPassword show up in an ldapsearch. This gives little protection. So, to get around this issue, we have to create ACLs. Note The Manager's Rights The manager has all rights to the DIT. In previous implementations, I have put him in access controls as a reference and would put \\\"write\\\" as his access. This isn't needed, but it doesn't hurt to have it. This ldif creates an ACL that allows the Admins group to do anything they want on the DIT (similar to manager). This also prevents anonymous searches from pulling up a user's password. # vi acl.ldif dn: olcDatabase={2}mdb,cn=config changetype: modify replace: olcAccess olcAccess: {0}to attrs=userPassword,shadowLastChange by group.exact=\"cn=Admins,ou=Group,dc=angelsofclockwork,dc=net\" write by anonymous auth by self write by * none break olcAccess: {2}to * by group.exact=\"cn=Admins,ou=Group,dc=angelsofclockwork,dc=net\" write by * read olcAccess: {3}to dn.base=\"\" by * read # ldapmodify -Y EXTERNAL -H ldapi:/// -f acl.ldif It's highly recommended, however, to disable anonymous searching, especially if you go production with LDAP. A lot of LDAP implementations disallow anonymous searching by default. You can do this with ACLs, but it's not recommended. We cover this in the search.","title":"ACL"},{"location":"el/openldap/#disable-anonymous-binding","text":"It's recommended to disable anonymous searching. This can be handled by making a modification to the global configuration and the DIT configuration. dn: cn=config changetype: modify add: olcDisallows olcDisallows: bind_anon dn: olcDatabase={2}mdb,cn=config changetype: modify add: olcRequires olcRequires: authc Once you add this in, all anonymous searching will cease. # ldapsearch -x -LLL uid=zera ldap_bind: Inappropriate authentication (48) additional info: anonymous bind disallowed","title":"Disable Anonymous Binding"},{"location":"el/openldap/#ldap-logging","text":"Logging is of course, very important for an LDAP server. There are a few types of logs we can do. There are the standard logs and then there are also audit logs. Audit logs allow an administrator to view changes being done to LDAP in an LDIF form. We can setup both. Let's create our modification LDIF. This will turn on standard logging and enable the audit module. Run an ldapmodify against this LDIF. dn: cn=config changetype: modify replace: olcLogFile olcLogFile: /var/log/ldap-standard.log - replace: olcLogLevel olcLogLevel: 256 # Keep in mind, if you have other modules being loaded, # add them to the list dn: cn=module,cn=config changetype: modify replace: olcModuleLoad olcModuleLoad: refint.la olcModuleLoad: memberof.la olcModuleLoad: auditlog.la Now, we need to make sure audit logging is done on our database. dn: olcOverlay=auditlog,olcDatabase={2}mdb,cn=config objectClass: olcAuditlogConfig objectClass: olcOverlayConfig olcOverlay: auditlog olcAuditlogFile: /var/log/ldap-audit.log It's recommended to have logrotate working for our logs. Here is a file I've dropped into /etc/logrotate.d. Experiment with these options. Since I work in an environment that has tons of transactions going all the time, and thus, my rotations are at 100M and 250M respectively. /var/log/ldap-standard.log { missingok compress notifempty daily rotate 10 size=100M } /var/log/ldap-audit.log { missingok compress notifempty daily rotate 10 size=250M } In /etc/rsyslog.conf, optionally, you can create this. If you find that logs are not appearing after the changes above, use this. local4.* /var/log/ldap.log","title":"LDAP Logging"},{"location":"el/openldap/#password-policy","text":"Password policies are a great asset, especially when working in an environment that have or require security policies. First, let's load our module and then add our overlay. This LDIF will do both. You may want to remove the comments before adding. dn: cn=module,cn=config changetype: modify replace: olcModuleLoad olcModuleLoad: refint.la olcModuleLoad: memberof.la olcModuleLoad: auditlog.la olcModuleLoad: ppolicy.la dn: olcOverlay=ppolicy,olcDatabase={2}mdb,cn=config objectClass: olcOverlayConfig objectClass: olcPPolicyConfig olcOverlay: ppolicy olcPPolicyDefault: cn=default,ou=policies,dc=angelsofclockwork,dc=net # Set the below to TRUE if you want users to get locked out after failed attempted olcPPolicyUseLockout: TRUE # Set the below to TRUE if you want passwords to be hashed. # HIGHLY RECOMMENDED YOU SET THIS TO TRUE olcPPolicyHashCleartext: TRUE Now, we need an LDIF to create our standard password policy. It's important to have a default password policy and then create separate ones as needed. Make sure to read the comments. You may want to remove the comments before adding. dn: cn=default,ou=policies,dc=angelsofclockwork,dc=net objectClass: pwdPolicy objectClass: person objectClass: top cn: passwordDefault sn: passwordDefault pwdAttribute: userPassword # If set to 0, quality is not checked. # If set to 1, quality is checked by an internal module which you setup. # If set to 2, the system used to change the password must have a checking mechanism. # Pick your poison. pwdCheckQuality: 0 # Password lives for 84 days pwdMinAge: 0 pwdMaxAge: 7257600 # Minimum length is 7 pwdMinLength: 7 # Password history of 10, cannot use a password that's in history pwdInHistory: 10 # 5 Failures till a lockout, 10 minutes for it to reset, 30 minute lockout. pwdMaxFailure: 5 pwdFailureCountInterval: 600 pwdLockout: TRUE pwdLockoutDuration: 1800 # A user can change their own password. pwdAllowUserChange: TRUE # Systems that authenticate to LDAP can warn 14 days before an expiration pwdExpireWarning: 1209600 # Allowed binds on an expired password. pwdGraceAuthNLimit: 5 pwdMustChange: TRUE pwdSafeModify: FALSE In the instance you want to use the built-in module for password checking, your LDIF would have these lines. pwdCheckQuality: 1 pwdCheckModule: check_password.so","title":"Password Policy"},{"location":"el/pxeboot/","text":"This page goes over setting up a pxeboot system using tftp on Enterprise Linux or Fedora Requirements \u00b6 Here are the list of requirements below. Enterprise Linux 8, 9, or Fedora A DHCP server setup that allows you to setup the next_server directive or setup the tftp server location Optionally if you are using a local mirror, httpd or nginx installed. (This guide assumes httpd ) Tutorial Preface, Notes, and Recommendations \u00b6 In some environments, it may be better (or easier, depending on your perspective) to setup a PXE server and roll out systems in a lab or otherwise in that fashion. It's one of the most straight forward ways to build out systems easily and consistently. The difference between a typical PXE setup and this is we're using grub2 menus, rather than the classic menu style. This makes it simpler to keep all configurations consistent between classic boot and EFI boot. If you plan on using supporting other architectures, it will be easier to use that architecture to run the grub2-mknetdir command and brings those to your tftp server. Cobbler \u00b6 While cobbler is a perfectly viable solution to setting up a pxeboot system for various distros and configurations, it is out of scope for this article. It is unknown if it sets up or directly supports grub2. Server Setup \u00b6 This section goes over the server setup portion for the tftp server. TFTP \u00b6 Let's install the tftpserver package plus some additional grub packages. If you are wanting other architectures, you can obtain the other grub2 module packages from your distribution's BaseOS or equivalent repository for that architecture and install it manually. # x86_64 % dnf install \\ grub2-efi-x64-modules \\ grub2-tools-extra \\ grub2-pc-modules \\ shim-ia32 \\ # this does not exist on el9+ tftp-server # aarch64 % dnf install \\ grub2-efi-aa64-modules \\ grub2-tools-extra \\ tftp-server Let's make our initial net directories and ensure the selinux contexts are correct. % grub2-mknetdir --net-directory /var/lib/tftpboot/ Netboot directory for i386-pc created. Configure your DHCP server to point to /srv/tftp/boot/grub2/i386-pc/core.0 Netboot directory for x86_64-efi created. Configure your DHCP server to point to /srv/tftp/boot/grub2/x86_64-efi/core.efi % restorecon -R /var/lib/tftpboot Now you'll need to enable the tftp socket and open the port. Traditionally, you would use xinetd. It's no longer required for the tftp service. # Note: This is port 69 with the UDP protocol % firewall-cmd --add-service=tftp --permanent % systemctl enable tftp.socket --now DHCP (ISC) \u00b6 On your DHCP server configuration (typically /etc/dhcp/dhcpd.conf if running on Fedora or EL), you should set the following options: option pxe-system-type code 93 = unsigned integer 16; option rfc3442-classless-static-routes code 121 = array of integer 8; option ms-classless-static-routes code 249 = array of integer 8; option space pxelinux; option pxelinux.magic code 208 = string; option pxelinux.configfile code 209 = text; option pxelinux.pathprefix code 210 = text; option pxelinux.reboottime code 211 = unsigned integer 32; option architecture-type code 93 = unsigned integer 16; option pxelinux.mtftp-ip code 1 = ip-address; option pxelinux.mtftp-cport code 2 = unsigned integer 16; option pxelinux.mtftp-sport code 3 = unsigned integer 16; option pxelinux.mtftp-tmout code 4 = unsigned integer 8; option pxelinux.mtftp-delay code 5 = unsigned integer 8; Whether this section is within a subnet block or not, it is needed to ensure the right bootloader is called. Note that we're only loading x86. If you are loading armhfp, use 00:0a. If you are loading aarch64, use 00:0b. class \"pxeclients\" { match if substring (option vendor-class-identifier, 0, 9) = \"PXEClient\"; # x86_64 EFI if option pxe-system-type = 00:07 { filename \"boot/grub2/x86_64-efi/core.efi\"; } else if option pxe-system-type = 00:08 { filename \"boot/grub2/x86_64-efi/core.efi\"; } else if option pxe-system-type = 00:09 { filename \"boot/grub2/x86_64-efi/core.efi\"; } else { # BIOS boot only filename \"boot/grub2/i386-pc/core.0\"; } } Note that in your subnet blocks, you should also mention next_server , which should point to your TFTP server. The DHCP and TFTP server can be on the same machine and there's nothing stopping you from doing that; next_server needs to be set regardless here. See an example below of a full work dhcpd.conf. ddns-update-style interim; allow booting; allow bootp; authoritative; log-facility local6; ignore client-updates; set vendorclass = option vendor-class-identifier; ## Allowing EFI Clients option pxe-system-type code 93 = unsigned integer 16; option rfc3442-classless-static-routes code 121 = array of integer 8; option ms-classless-static-routes code 249 = array of integer 8; option space pxelinux; option pxelinux.magic code 208 = string; option pxelinux.configfile code 209 = text; option pxelinux.pathprefix code 210 = text; option pxelinux.reboottime code 211 = unsigned integer 32; option architecture-type code 93 = unsigned integer 16; option pxelinux.mtftp-ip code 1 = ip-address; option pxelinux.mtftp-cport code 2 = unsigned integer 16; option pxelinux.mtftp-sport code 3 = unsigned integer 16; option pxelinux.mtftp-tmout code 4 = unsigned integer 8; option pxelinux.mtftp-delay code 5 = unsigned integer 8; subnet 10.100.0.0 netmask 255.255.255.0 { interface br1000; option routers 10.100.0.1; option domain-name-servers 10.100.0.1, 10.100.0.231; option domain-name \"angelsofclockwork.net\"; option subnet-mask 255.255.255.0; range 10.100.0.110 10.100.0.199; ## EFI Client Catch class \"pxeclients\" { match if substring (option vendor-class-identifier, 0, 9) = \"PXEClient\"; if option pxe-system-type = 00:07 { filename \"boot/grub2/x86_64-efi/core.efi\"; } else if option pxe-system-type = 00:08 { filename \"boot/grub2/x86_64-efi/core.efi\"; } else if option pxe-system-type = 00:09 { filename \"boot/grub2/x86_64-efi/core.efi\"; } else if option pxe-system-type = 00:0a { filename \"boot/grub2/armv7a-efi/core.efi\"; } else if option pxe-system-type = 00:0b { filename \"boot/grub2/aarch64-efi/core.efi\"; } else { filename \"boot/grub2/i386-pc/core.0\"; } } default-lease-time 21600; max-lease-time 43200; next-server 10.100.0.1; } Ensure that the dhcpd service is restarted after making the necessary changes. DHCP (Kea) \u00b6 Kea is a different configuration style from ISC. At this time, we do not have a full working example. Web Server (httpd) \u00b6 If we plan on hosting the installation mirror in your environment, it's recommended to stand up a simple web server. It does not require any kind of special configuration. We'll use the default /var/www/html/ path. If you wish to use another such as /srv/www, you will need to setup a virtual host (this is outside the scope of this page). % dnf install httpd -y % systemctl enable httpd --now % firewall-cmd --add-service=http --permanent % firewall-cmd --complete-reload # create the directories for our distributions % mkdir -p /var/www/html/os/{fedora,centos,rocky} Setting up Grub \u00b6 When you run grub2-mknetdir, it created a core.* set of files. An accompanying grub.cfg must sit next to them. To prevent a duplication of work, it can be simplified by making all grub configurations at /var/lib/tftpboot and then symlink them next to each directory containing core.*. Let's make a very, very simple one. set default=0 set timeout=60 menuentry 'EFI Firmware System Setup' $menuentry_id_option 'uefi-firmware' { fwsetup } menuentry 'Reboot' { reboot } menuentry 'Shutdown' { halt } Now let's just symlink it. % cd /var/lib/tftpboot/boot/grub2/x86_64-efi % ln -s ../../../grub.cfg % cd /var/lib/tftpboot/boot/grub2/i386-pc % ln -s ../../../grub.cfg This should produce a grub menu for both EFI and BIOS systems that contain three bootable options. Adding Distributions \u00b6 Now that grub is sort of setup, we should add a distribution to it at least. Below are a couple examples using Fedora, Rocky Linux, and CentOS Stream. Rocky Linux \u00b6 Setting up Rocky Linux (or any other Enterprise Linux distribution) should be straight forward. We'll download both Rocky Linux 8 and Rocky Linux 9 and setup the menus. Note If you plan on not hosting a mirror of the base repositories, ensure that your inst.repo/inst.stage2 commands are accurate to a mirror of your choice. The below assumes we are hosting a mirror of the downloaded ISO, which will make installations quicker as it'll be confined to your network. % cd /var/tmp # Rocky Linux 8 % wget https://dl.rockylinux.org/pub/rocky/8/isos/x86_64/Rocky-8-latest-x86_64-dvd.iso # Rocky Linux 9 % wget https://dl.rockylinux.org/pub/rocky/9/isos/x86_64/Rocky-9-latest-x86_64-dvd.iso # Optionally, if you plan on supporting ARM... % wget https://dl.rockylinux.org/pub/rocky/8/isos/aarch64/Rocky-8-latest-aarch64-dvd.iso % wget https://dl.rockylinux.org/pub/rocky/9/isos/aarch64/Rocky-9-latest-aarch64-dvd.iso Here we'll copy the data we want into the necessary directories. Any pxeboot related images will go to /var/lib/tftpboot/rocky-X-ARCH (X being the major version, ARCH being the architecture). If we are keeping a local mirror of the DVD, we'll put it into /var/www/html/os/rocky/X/ARCH. Below is for x86_64, but the same steps can be repeated for aarch64 without any issues. Just replace x86_64 with aarch64. ## Rocky 8 % mount -o loop Rocky-8-latest-x86_64-dvd.iso /mnt % mkdir /var/lib/tftpboot/rocky-8-x86_64 % cp /mnt/images/pxeboot/* /var/lib/tftpboot/rocky-8-x86_64 % mkdir -p /var/www/html/os/rocky/8/x86_64 % rsync -vrlptDSH --delete /mnt/ /var/www/html/os/rocky/8/x86_64 % umount /mnt ## Rocky 9 % mount -o loop Rocky-9-latest-x86_64-dvd.iso /mnt % mkdir /var/lib/tftpboot/rocky-9-x86_64 % cp /mnt/images/pxeboot/* /var/lib/tftpboot/rocky-9-x86_64 % mkdir -p /var/www/html/os/rocky/9/x86_64 % rsync -vrlptDSH --delete /mnt/ /var/www/html/os/rocky/9/x86_64 % umount /mnt % restorecon -R /var/www/html/os/rocky At this point, we'll need to setup the grub menus. We'll setup non-kickstart examples for BIOS and UEFI. . . . # Rocky 8 menuentry 'Install Rocky Linux 8 (No KS) (UEFI)' --class fedora --class gnu-linux --class gnu --class os { echo \"Loading Rocky Linux 8 kernel...\" linuxefi rocky-8-x86_64/vmlinuz inst.repo=http://10.100.0.1/os/rocky/8/x86_64 inst.stage2=http://10.100.0.1/os/rocky/8/x86_64 ip=dhcp initrdefi rocky-8-x86_64/initrd.img } menuentry 'Install Rocky Linux 8 (No KS) (BIOS)' --class fedora --class gnu-linux --class gnu --class os { echo \"Loading Rocky Linux 8 kernel...\" linux16 rocky-8-x86_64/vmlinuz inst.repo=http://10.100.0.1/os/rocky/8/x86_64 inst.stage2=http://10.100.0.1/os/rocky/8/x86_64 ip=dhcp initrd16 rocky-8-x86_64/initrd.img } # if you are setting up arm... menuentry 'Install Rocky Linux 8 (No KS) (aarch64)' --class fedora --class gnu-linux --class gnu --class os { echo \"Loading Rocky Linux 8 kernel...\" linux rocky-9-aarch64/vmlinuz inst.repo=http://10.100.0.1/os/rocky/8/aarch64 inst.stage2=http://10.100.0.1/os/rocky/8/aarch64 ip=dhcp initrd rocky-9-aarch64/initrd.img } . . . # Rocky 9 menuentry 'Install Rocky Linux 9 (No KS) (UEFI)' --class fedora --class gnu-linux --class gnu --class os { echo \"Loading Rocky Linux 9 kernel...\" linuxefi rocky-9-x86_64/vmlinuz inst.repo=http://10.100.0.1/os/rocky/9/x86_64 inst.stage2=http://10.100.0.1/os/rocky/9/x86_64 ip=dhcp initrdefi rocky-9-x86_64/initrd.img } menuentry 'Install Rocky Linux 9 (No KS) (BIOS)' --class fedora --class gnu-linux --class gnu --class os { echo \"Loading Rocky Linux 9 kernel...\" linux16 rocky-9-x86_64/vmlinuz inst.repo=http://10.100.0.1/os/rocky/9/x86_64 inst.stage2=http://10.100.0.1/os/rocky/9/x86_64 ip=dhcp initrd16 rocky-9-x86_64/initrd.img } # if you are setting up arm... menuentry 'Install Rocky Linux 9 (No KS) (aarch64)' --class fedora --class gnu-linux --class gnu --class os { echo \"Loading Rocky Linux 9 kernel...\" linux rocky-9-aarch64/vmlinuz inst.repo=http://10.100.0.1/os/rocky/9/aarch64 inst.stage2=http://10.100.0.1/os/rocky/9/aarch64 ip=dhcp initrd rocky-9-aarch64/initrd.img } The Rocky Linuxinstallation should now be bootable. CentOS Stream \u00b6 Much like Rocky Linux (or other derivatives), the path is the same for setting it up. Using upstream mirror path If you plan on not hosting a mirror of the base repositories, ensure that your inst.repo/inst.stage2 commands are accurate to a mirror of your choice. % cd /var/tmp # CentOS Stream 9 % wget -O CentOS-Stream-9-latest-x86_64-dvd1.iso \\ 'https://mirrors.centos.org/mirrorlist?path=/9-stream/BaseOS/x86_64/iso/CentOS-Stream-9-latest-x86_64-dvd1.iso&redirect=1&protocol=https' # Optionally, if you plan on supporting ARM... % wget -O CentOS-Stream-9-latest-aarch64-dvd1.iso \\ 'https://mirrors.centos.org/mirrorlist?path=/9-stream/BaseOS/aarch64/iso/CentOS-Stream-9-latest-aarch64-dvd1.iso&redirect=1&protocol=https' Here we'll copy the data we want into the necessary directories. Any pxeboot related images will go to /var/lib/tftpboot/rocky-X-ARCH (X being the major version, ARCH being the architecture). If we are keeping a local mirror of the DVD, we'll put it into /var/www/html/os/rocky/X/ARCH. Below is for x86_64, but the same steps can be repeated for aarch64 without any issues. Just replace x86_64 with aarch64. ## CentOS Stream 9 % mount -o loop CentOS-Stream-9-latest-x86_64-dvd1.iso /mnt % mkdir /var/lib/tftpboot/centos-9-x86_64 % cp /mnt/images/pxeboot/* /var/lib/tftpboot/centos-9-x86_64 % mkdir -p /var/www/html/os/centos/9/x86_64 % rsync -vrlptDSH --delete /mnt/ /var/www/html/os/centos/9/x86_64 % restorecon -R /var/www/html/os/centos/9 % umount /mnt} At this point, we'll need to setup the grub menus. We'll setup non-kickstart examples for BIOS and UEFI. . . . # CentOS Stream 9 menuentry 'Install CentOS Stream 9 (No KS) (UEFI)' --class fedora --class gnu-linux --class gnu --class os { echo \"Loading CentOS Stream 9 kernel...\" linuxefi centos-9-x86_64/vmlinuz inst.repo=http://10.100.0.1/os/centos/9/x86_64 inst.stage2=http://10.100.0.1/os/centos/9/x86_64 ip=dhcp initrdefi centos-9-x86_64/initrd.img } menuentry 'Install CentOS Stream 9 (No KS) (BIOS)' --class fedora --class gnu-linux --class gnu --class os { echo \"Loading CentOS Stream 9 kernel...\" linux16 centos-9-x86_64/vmlinuz inst.repo=http://10.100.0.1/os/centos/9/x86_64 inst.stage2=http://10.100.0.1/os/centos/9/x86_64 ip=dhcp initrd16 centos-9-x86_64/initrd.img } # if you are setting up arm... menuentry 'Install CentOS Stream 9 (No KS) (aarch64)' --class fedora --class gnu-linux --class gnu --class os { echo \"Loading CentOS Stream 9 kernel...\" linux centos-9-aarch64/vmlinuz inst.repo=http://10.100.0.1/os/centos/9/aarch64 inst.stage2=http://10.100.0.1/os/centos/9/aarch64 ip=dhcp initrd centos-9-aarch64/initrd.img } The CentOS Stream installation should now be bootable. Fedora \u00b6 Let's put up a regular installer with no kickstart for Fedora. This does not involve pulling down any ISO's and will rely entirely on using upstream repositories. % cd /var/lib/tftpboot % mkdir fedora-x86_64 % cd fedora-x86_64 # Replace XX with the current fedora version % wget https://dl.fedoraproject.org/pub/fedora/linux/releases/XX/Everything/x86_64/os/images/pxeboot/initrd.img % wget https://dl.fedoraproject.org/pub/fedora/linux/releases/XX/Everything/x86_64/os/images/pxeboot/vmlinuz # If you want arm systems... aarch64 % cd .. % mkdir fedora-aarch64 # Replace XX with the current fedora version % wget https://dl.fedoraproject.org/pub/fedora/linux/releases/XX/Everything/aarch64/os/images/pxeboot/initrd.img % wget https://dl.fedoraproject.org/pub/fedora/linux/releases/XX/Everything/aarch64/os/images/pxeboot/vmlinuz Now we can add a couple menu entry items for Fedora. I'm making both EFI and Classic entries to ensure we can boot both EFI and BIOS systems from the same menu. . . . menuentry 'Install Fedora Linux (EFI)' --class fedora --class gnu-linux --class gnu --class os { linuxefi fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os ip=dhcp initrdefi fedora-x86_64/initrd.img } menuentry 'Install Fedora Linux (Classic)' --class fedora --class gnu-linux --class gnu --class os { linux16 fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ ip=dhcp initrd16 fedora-x86_64/initrd.img } # Add the below for ARM systems menuentry 'Install Fedora Linux (ARM)' --class fedora --class gnu-linux --class gnu --class os { linux fedora-aarch64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os/ ip=dhcp initrd fedora-aarch64/initrd.img } Now the Fedora installation should be bootable. Customizing Grub \u00b6 Grub is customizable. It is possible to setup background images, choose menu colors, setup themes, and so on. Colors and Backgrounds \u00b6 Colors and backgrounds are perfectly possible, even in an EFI setup. We'll need to load a few modules and then set the colors and background we want. Note that if you're using a background, it should live in /var/lib/tftpboot to make things easier. . . . insmod all_video insmod gfxterm insmod gfxterm_menu insmod gfxmenu insmod gfxterm_background insmod png terminal_output gfxterm background_image -m stretch /bg.png set menu_color_highlight=cyan/black set menu_color_normal=white/black set color_normal=white/black . . . The background would be /var/lib/tftpboot/bg.png in this example. Selected items will appear to be cyan and the typical gray selection box is now transparent, which is done by setting it to black. Everything else should appear as white text with a transparent background. Example below. Special Submenus \u00b6 Submenus are easily created using submenu in the grub configuration. For example: submenu 'Fedora Linux' --class fedora --class gnu-linux --class gnu --class os { set menu_color_highlight=black/light-cyan set menu_color_normal=white/black set color_normal=white/black menuentry 'Install Fedora Linux (EFI)' --class fedora --class gnu-linux --class gnu --class os { linuxefi fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os ip=dhcp initrdefi fedora-x86_64/initrd.img } menuentry 'Install Fedora Linux (Classic)' --class fedora --class gnu-linux --class gnu --class os { linux16 fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ ip=dhcp initrd16 fedora-x86_64/initrd.img } menuentry 'Install Fedora Linux (ARM)' --class fedora --class gnu-linux --class gnu --class os { linux fedora-aarch64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os/ ip=dhcp initrd fedora-aarch64/initrd.img } } This now means \"Fedora Linux\" will show up as a menu option and it will take you to a brand new menu with the two listed items, and another color scheme. Note that we created color items because submenus will reset the theme options. Example of how it looks is below. It is also possible to place everything into separate source-able files. Note that when you do this, you will need to symlink those files just like you did with grub.cfg. submenu 'Fedora Linux' --class fedora --class gnu-linux --class gnu --class os { set menu_color_highlight=black/light-cyan set menu_color_normal=white/black set color_normal=white/black source fedora.cfg } menuentry 'Install Fedora Linux (EFI)' --class fedora --class gnu-linux --class gnu --class os { linuxefi fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os ip=dhcp initrdefi fedora-x86_64/initrd.img } menuentry 'Install Fedora Linux (Classic)' --class fedora --class gnu-linux --class gnu --class os { linux16 fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ ip=dhcp initrd16 fedora-x86_64/initrd.img } menuentry 'Install Fedora Linux (ARM)' --class fedora --class gnu-linux --class gnu --class os { linux fedora-aarch64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os/ ip=dhcp initrd fedora-aarch64/initrd.img } Submenus can be nested too. Here's a deeper, working example of my own setup using Fedora 35. # grub.cfg set default=0 set timeout=60 insmod all_video insmod gfxterm insmod gfxterm_menu insmod gfxmenu insmod gfxterm_background insmod png terminal_output gfxterm loadfont /unicode.pf2 background_image -m stretch /bg.png set menu_color_highlight=cyan/black set menu_color_normal=white/black set color_normal=white/black submenu 'Fedora Linux' --class fedora --class gnu-linux --class gnu --class os { set menu_color_highlight=black/light-cyan set menu_color_normal=white/black set color_normal=white/black source fedora.cfg } menuentry 'EFI System Setup' $menuentry_id_option 'uefi-firmware' { fwsetup } menuentry 'Reboot' { reboot } menuentry 'Shutdown' { halt } # fedora.cfg submenu 'Fedora Linux (latest stable)' --class fedora --class gnu-linux --class gnu --class os { set menu_color_highlight=black/light-cyan set menu_color_normal=white/black set color_normal=white/black # EFI Only submenu 'EFI Mode' --class fedora --class gnu-linux --class gnu --class os { set menu_color_highlight=black/light-cyan set menu_color_normal=white/black set color_normal=white/black menuentry 'Install Fedora Linux (No KS)' --class fedora --class gnu-linux --class gnu --class os { linuxefi fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os ip=dhcp initrdefi fedora-x86_64/initrd.img } menuentry 'Install Fedora Linux' --class fedora --class gnu-linux --class gnu --class os { linuxefi fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os ip=dhcp initrdefi fedora-x86_64/initrd.img } menuentry 'Fedora Linux (Rescue Mode)' --class fedora --class gnu-linux --class gnu --class os { linuxefi fedora-x86_64/vmlinuz inst.rescue inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os initrdefi fedora-x86_64/initrd.img } } # Classic Only submenu 'Classic Mode' --class fedora --class gnu-linux --class gnu --class os { set menu_color_highlight=black/light-cyan set menu_color_normal=white/black set color_normal=white/black menuentry 'Install Fedora Linux (No KS)' --class fedora --class gnu-linux --class gnu --class os { linux16 fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ ip=dhcp initrd16 fedora-x86_64/initrd.img } menuentry 'Install Fedora Linux' --class fedora --class gnu-linux --class gnu --class os { linux16 fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ ip=dhcp initrd16 fedora-x86_64/initrd.img } menuentry 'Fedora Linux (Rescue Mode)' --class fedora --class gnu-linux --class gnu --class os { linux16 fedora-x86_64/vmlinuz inst.rescue inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ initrd16 fedora-x86_64/initrd.img } } # EFI mode for ARM submenu 'EFI Mode (aarch64)' --class fedora --class gnu-linux --class gnu --class os { set menu_color_highlight=black/light-cyan set menu_color_normal=white/black set color_normal=white/black menuentry 'Install Fedora Linux (No KS)' --class fedora --class gnu-linux --class gnu --class os { linuxefi fedora-aarch64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os ip=dhcp initrdefi fedora-aarch64/initrd.img } menuentry 'Install Fedora Linux' --class fedora --class gnu-linux --class gnu --class os { linuxefi fedora-aarch64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os ip=dhcp initrdefi fedora-aarch64/initrd.img } menuentry 'Fedora Linux (Rescue Mode)' --class fedora --class gnu-linux --class gnu --class os { linuxefi fedora-aarch64/vmlinuz inst.rescue inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os initrdefi fedora-aarch64/initrd.img } } }","title":"PXE (with grub2)"},{"location":"el/pxeboot/#requirements","text":"Here are the list of requirements below. Enterprise Linux 8, 9, or Fedora A DHCP server setup that allows you to setup the next_server directive or setup the tftp server location Optionally if you are using a local mirror, httpd or nginx installed. (This guide assumes httpd )","title":"Requirements"},{"location":"el/pxeboot/#tutorial-preface-notes-and-recommendations","text":"In some environments, it may be better (or easier, depending on your perspective) to setup a PXE server and roll out systems in a lab or otherwise in that fashion. It's one of the most straight forward ways to build out systems easily and consistently. The difference between a typical PXE setup and this is we're using grub2 menus, rather than the classic menu style. This makes it simpler to keep all configurations consistent between classic boot and EFI boot. If you plan on using supporting other architectures, it will be easier to use that architecture to run the grub2-mknetdir command and brings those to your tftp server.","title":"Tutorial Preface, Notes, and Recommendations"},{"location":"el/pxeboot/#cobbler","text":"While cobbler is a perfectly viable solution to setting up a pxeboot system for various distros and configurations, it is out of scope for this article. It is unknown if it sets up or directly supports grub2.","title":"Cobbler"},{"location":"el/pxeboot/#server-setup","text":"This section goes over the server setup portion for the tftp server.","title":"Server Setup"},{"location":"el/pxeboot/#tftp","text":"Let's install the tftpserver package plus some additional grub packages. If you are wanting other architectures, you can obtain the other grub2 module packages from your distribution's BaseOS or equivalent repository for that architecture and install it manually. # x86_64 % dnf install \\ grub2-efi-x64-modules \\ grub2-tools-extra \\ grub2-pc-modules \\ shim-ia32 \\ # this does not exist on el9+ tftp-server # aarch64 % dnf install \\ grub2-efi-aa64-modules \\ grub2-tools-extra \\ tftp-server Let's make our initial net directories and ensure the selinux contexts are correct. % grub2-mknetdir --net-directory /var/lib/tftpboot/ Netboot directory for i386-pc created. Configure your DHCP server to point to /srv/tftp/boot/grub2/i386-pc/core.0 Netboot directory for x86_64-efi created. Configure your DHCP server to point to /srv/tftp/boot/grub2/x86_64-efi/core.efi % restorecon -R /var/lib/tftpboot Now you'll need to enable the tftp socket and open the port. Traditionally, you would use xinetd. It's no longer required for the tftp service. # Note: This is port 69 with the UDP protocol % firewall-cmd --add-service=tftp --permanent % systemctl enable tftp.socket --now","title":"TFTP"},{"location":"el/pxeboot/#dhcp-isc","text":"On your DHCP server configuration (typically /etc/dhcp/dhcpd.conf if running on Fedora or EL), you should set the following options: option pxe-system-type code 93 = unsigned integer 16; option rfc3442-classless-static-routes code 121 = array of integer 8; option ms-classless-static-routes code 249 = array of integer 8; option space pxelinux; option pxelinux.magic code 208 = string; option pxelinux.configfile code 209 = text; option pxelinux.pathprefix code 210 = text; option pxelinux.reboottime code 211 = unsigned integer 32; option architecture-type code 93 = unsigned integer 16; option pxelinux.mtftp-ip code 1 = ip-address; option pxelinux.mtftp-cport code 2 = unsigned integer 16; option pxelinux.mtftp-sport code 3 = unsigned integer 16; option pxelinux.mtftp-tmout code 4 = unsigned integer 8; option pxelinux.mtftp-delay code 5 = unsigned integer 8; Whether this section is within a subnet block or not, it is needed to ensure the right bootloader is called. Note that we're only loading x86. If you are loading armhfp, use 00:0a. If you are loading aarch64, use 00:0b. class \"pxeclients\" { match if substring (option vendor-class-identifier, 0, 9) = \"PXEClient\"; # x86_64 EFI if option pxe-system-type = 00:07 { filename \"boot/grub2/x86_64-efi/core.efi\"; } else if option pxe-system-type = 00:08 { filename \"boot/grub2/x86_64-efi/core.efi\"; } else if option pxe-system-type = 00:09 { filename \"boot/grub2/x86_64-efi/core.efi\"; } else { # BIOS boot only filename \"boot/grub2/i386-pc/core.0\"; } } Note that in your subnet blocks, you should also mention next_server , which should point to your TFTP server. The DHCP and TFTP server can be on the same machine and there's nothing stopping you from doing that; next_server needs to be set regardless here. See an example below of a full work dhcpd.conf. ddns-update-style interim; allow booting; allow bootp; authoritative; log-facility local6; ignore client-updates; set vendorclass = option vendor-class-identifier; ## Allowing EFI Clients option pxe-system-type code 93 = unsigned integer 16; option rfc3442-classless-static-routes code 121 = array of integer 8; option ms-classless-static-routes code 249 = array of integer 8; option space pxelinux; option pxelinux.magic code 208 = string; option pxelinux.configfile code 209 = text; option pxelinux.pathprefix code 210 = text; option pxelinux.reboottime code 211 = unsigned integer 32; option architecture-type code 93 = unsigned integer 16; option pxelinux.mtftp-ip code 1 = ip-address; option pxelinux.mtftp-cport code 2 = unsigned integer 16; option pxelinux.mtftp-sport code 3 = unsigned integer 16; option pxelinux.mtftp-tmout code 4 = unsigned integer 8; option pxelinux.mtftp-delay code 5 = unsigned integer 8; subnet 10.100.0.0 netmask 255.255.255.0 { interface br1000; option routers 10.100.0.1; option domain-name-servers 10.100.0.1, 10.100.0.231; option domain-name \"angelsofclockwork.net\"; option subnet-mask 255.255.255.0; range 10.100.0.110 10.100.0.199; ## EFI Client Catch class \"pxeclients\" { match if substring (option vendor-class-identifier, 0, 9) = \"PXEClient\"; if option pxe-system-type = 00:07 { filename \"boot/grub2/x86_64-efi/core.efi\"; } else if option pxe-system-type = 00:08 { filename \"boot/grub2/x86_64-efi/core.efi\"; } else if option pxe-system-type = 00:09 { filename \"boot/grub2/x86_64-efi/core.efi\"; } else if option pxe-system-type = 00:0a { filename \"boot/grub2/armv7a-efi/core.efi\"; } else if option pxe-system-type = 00:0b { filename \"boot/grub2/aarch64-efi/core.efi\"; } else { filename \"boot/grub2/i386-pc/core.0\"; } } default-lease-time 21600; max-lease-time 43200; next-server 10.100.0.1; } Ensure that the dhcpd service is restarted after making the necessary changes.","title":"DHCP (ISC)"},{"location":"el/pxeboot/#dhcp-kea","text":"Kea is a different configuration style from ISC. At this time, we do not have a full working example.","title":"DHCP (Kea)"},{"location":"el/pxeboot/#web-server-httpd","text":"If we plan on hosting the installation mirror in your environment, it's recommended to stand up a simple web server. It does not require any kind of special configuration. We'll use the default /var/www/html/ path. If you wish to use another such as /srv/www, you will need to setup a virtual host (this is outside the scope of this page). % dnf install httpd -y % systemctl enable httpd --now % firewall-cmd --add-service=http --permanent % firewall-cmd --complete-reload # create the directories for our distributions % mkdir -p /var/www/html/os/{fedora,centos,rocky}","title":"Web Server (httpd)"},{"location":"el/pxeboot/#setting-up-grub","text":"When you run grub2-mknetdir, it created a core.* set of files. An accompanying grub.cfg must sit next to them. To prevent a duplication of work, it can be simplified by making all grub configurations at /var/lib/tftpboot and then symlink them next to each directory containing core.*. Let's make a very, very simple one. set default=0 set timeout=60 menuentry 'EFI Firmware System Setup' $menuentry_id_option 'uefi-firmware' { fwsetup } menuentry 'Reboot' { reboot } menuentry 'Shutdown' { halt } Now let's just symlink it. % cd /var/lib/tftpboot/boot/grub2/x86_64-efi % ln -s ../../../grub.cfg % cd /var/lib/tftpboot/boot/grub2/i386-pc % ln -s ../../../grub.cfg This should produce a grub menu for both EFI and BIOS systems that contain three bootable options.","title":"Setting up Grub"},{"location":"el/pxeboot/#adding-distributions","text":"Now that grub is sort of setup, we should add a distribution to it at least. Below are a couple examples using Fedora, Rocky Linux, and CentOS Stream.","title":"Adding Distributions"},{"location":"el/pxeboot/#rocky-linux","text":"Setting up Rocky Linux (or any other Enterprise Linux distribution) should be straight forward. We'll download both Rocky Linux 8 and Rocky Linux 9 and setup the menus. Note If you plan on not hosting a mirror of the base repositories, ensure that your inst.repo/inst.stage2 commands are accurate to a mirror of your choice. The below assumes we are hosting a mirror of the downloaded ISO, which will make installations quicker as it'll be confined to your network. % cd /var/tmp # Rocky Linux 8 % wget https://dl.rockylinux.org/pub/rocky/8/isos/x86_64/Rocky-8-latest-x86_64-dvd.iso # Rocky Linux 9 % wget https://dl.rockylinux.org/pub/rocky/9/isos/x86_64/Rocky-9-latest-x86_64-dvd.iso # Optionally, if you plan on supporting ARM... % wget https://dl.rockylinux.org/pub/rocky/8/isos/aarch64/Rocky-8-latest-aarch64-dvd.iso % wget https://dl.rockylinux.org/pub/rocky/9/isos/aarch64/Rocky-9-latest-aarch64-dvd.iso Here we'll copy the data we want into the necessary directories. Any pxeboot related images will go to /var/lib/tftpboot/rocky-X-ARCH (X being the major version, ARCH being the architecture). If we are keeping a local mirror of the DVD, we'll put it into /var/www/html/os/rocky/X/ARCH. Below is for x86_64, but the same steps can be repeated for aarch64 without any issues. Just replace x86_64 with aarch64. ## Rocky 8 % mount -o loop Rocky-8-latest-x86_64-dvd.iso /mnt % mkdir /var/lib/tftpboot/rocky-8-x86_64 % cp /mnt/images/pxeboot/* /var/lib/tftpboot/rocky-8-x86_64 % mkdir -p /var/www/html/os/rocky/8/x86_64 % rsync -vrlptDSH --delete /mnt/ /var/www/html/os/rocky/8/x86_64 % umount /mnt ## Rocky 9 % mount -o loop Rocky-9-latest-x86_64-dvd.iso /mnt % mkdir /var/lib/tftpboot/rocky-9-x86_64 % cp /mnt/images/pxeboot/* /var/lib/tftpboot/rocky-9-x86_64 % mkdir -p /var/www/html/os/rocky/9/x86_64 % rsync -vrlptDSH --delete /mnt/ /var/www/html/os/rocky/9/x86_64 % umount /mnt % restorecon -R /var/www/html/os/rocky At this point, we'll need to setup the grub menus. We'll setup non-kickstart examples for BIOS and UEFI. . . . # Rocky 8 menuentry 'Install Rocky Linux 8 (No KS) (UEFI)' --class fedora --class gnu-linux --class gnu --class os { echo \"Loading Rocky Linux 8 kernel...\" linuxefi rocky-8-x86_64/vmlinuz inst.repo=http://10.100.0.1/os/rocky/8/x86_64 inst.stage2=http://10.100.0.1/os/rocky/8/x86_64 ip=dhcp initrdefi rocky-8-x86_64/initrd.img } menuentry 'Install Rocky Linux 8 (No KS) (BIOS)' --class fedora --class gnu-linux --class gnu --class os { echo \"Loading Rocky Linux 8 kernel...\" linux16 rocky-8-x86_64/vmlinuz inst.repo=http://10.100.0.1/os/rocky/8/x86_64 inst.stage2=http://10.100.0.1/os/rocky/8/x86_64 ip=dhcp initrd16 rocky-8-x86_64/initrd.img } # if you are setting up arm... menuentry 'Install Rocky Linux 8 (No KS) (aarch64)' --class fedora --class gnu-linux --class gnu --class os { echo \"Loading Rocky Linux 8 kernel...\" linux rocky-9-aarch64/vmlinuz inst.repo=http://10.100.0.1/os/rocky/8/aarch64 inst.stage2=http://10.100.0.1/os/rocky/8/aarch64 ip=dhcp initrd rocky-9-aarch64/initrd.img } . . . # Rocky 9 menuentry 'Install Rocky Linux 9 (No KS) (UEFI)' --class fedora --class gnu-linux --class gnu --class os { echo \"Loading Rocky Linux 9 kernel...\" linuxefi rocky-9-x86_64/vmlinuz inst.repo=http://10.100.0.1/os/rocky/9/x86_64 inst.stage2=http://10.100.0.1/os/rocky/9/x86_64 ip=dhcp initrdefi rocky-9-x86_64/initrd.img } menuentry 'Install Rocky Linux 9 (No KS) (BIOS)' --class fedora --class gnu-linux --class gnu --class os { echo \"Loading Rocky Linux 9 kernel...\" linux16 rocky-9-x86_64/vmlinuz inst.repo=http://10.100.0.1/os/rocky/9/x86_64 inst.stage2=http://10.100.0.1/os/rocky/9/x86_64 ip=dhcp initrd16 rocky-9-x86_64/initrd.img } # if you are setting up arm... menuentry 'Install Rocky Linux 9 (No KS) (aarch64)' --class fedora --class gnu-linux --class gnu --class os { echo \"Loading Rocky Linux 9 kernel...\" linux rocky-9-aarch64/vmlinuz inst.repo=http://10.100.0.1/os/rocky/9/aarch64 inst.stage2=http://10.100.0.1/os/rocky/9/aarch64 ip=dhcp initrd rocky-9-aarch64/initrd.img } The Rocky Linuxinstallation should now be bootable.","title":"Rocky Linux"},{"location":"el/pxeboot/#centos-stream","text":"Much like Rocky Linux (or other derivatives), the path is the same for setting it up. Using upstream mirror path If you plan on not hosting a mirror of the base repositories, ensure that your inst.repo/inst.stage2 commands are accurate to a mirror of your choice. % cd /var/tmp # CentOS Stream 9 % wget -O CentOS-Stream-9-latest-x86_64-dvd1.iso \\ 'https://mirrors.centos.org/mirrorlist?path=/9-stream/BaseOS/x86_64/iso/CentOS-Stream-9-latest-x86_64-dvd1.iso&redirect=1&protocol=https' # Optionally, if you plan on supporting ARM... % wget -O CentOS-Stream-9-latest-aarch64-dvd1.iso \\ 'https://mirrors.centos.org/mirrorlist?path=/9-stream/BaseOS/aarch64/iso/CentOS-Stream-9-latest-aarch64-dvd1.iso&redirect=1&protocol=https' Here we'll copy the data we want into the necessary directories. Any pxeboot related images will go to /var/lib/tftpboot/rocky-X-ARCH (X being the major version, ARCH being the architecture). If we are keeping a local mirror of the DVD, we'll put it into /var/www/html/os/rocky/X/ARCH. Below is for x86_64, but the same steps can be repeated for aarch64 without any issues. Just replace x86_64 with aarch64. ## CentOS Stream 9 % mount -o loop CentOS-Stream-9-latest-x86_64-dvd1.iso /mnt % mkdir /var/lib/tftpboot/centos-9-x86_64 % cp /mnt/images/pxeboot/* /var/lib/tftpboot/centos-9-x86_64 % mkdir -p /var/www/html/os/centos/9/x86_64 % rsync -vrlptDSH --delete /mnt/ /var/www/html/os/centos/9/x86_64 % restorecon -R /var/www/html/os/centos/9 % umount /mnt} At this point, we'll need to setup the grub menus. We'll setup non-kickstart examples for BIOS and UEFI. . . . # CentOS Stream 9 menuentry 'Install CentOS Stream 9 (No KS) (UEFI)' --class fedora --class gnu-linux --class gnu --class os { echo \"Loading CentOS Stream 9 kernel...\" linuxefi centos-9-x86_64/vmlinuz inst.repo=http://10.100.0.1/os/centos/9/x86_64 inst.stage2=http://10.100.0.1/os/centos/9/x86_64 ip=dhcp initrdefi centos-9-x86_64/initrd.img } menuentry 'Install CentOS Stream 9 (No KS) (BIOS)' --class fedora --class gnu-linux --class gnu --class os { echo \"Loading CentOS Stream 9 kernel...\" linux16 centos-9-x86_64/vmlinuz inst.repo=http://10.100.0.1/os/centos/9/x86_64 inst.stage2=http://10.100.0.1/os/centos/9/x86_64 ip=dhcp initrd16 centos-9-x86_64/initrd.img } # if you are setting up arm... menuentry 'Install CentOS Stream 9 (No KS) (aarch64)' --class fedora --class gnu-linux --class gnu --class os { echo \"Loading CentOS Stream 9 kernel...\" linux centos-9-aarch64/vmlinuz inst.repo=http://10.100.0.1/os/centos/9/aarch64 inst.stage2=http://10.100.0.1/os/centos/9/aarch64 ip=dhcp initrd centos-9-aarch64/initrd.img } The CentOS Stream installation should now be bootable.","title":"CentOS Stream"},{"location":"el/pxeboot/#fedora","text":"Let's put up a regular installer with no kickstart for Fedora. This does not involve pulling down any ISO's and will rely entirely on using upstream repositories. % cd /var/lib/tftpboot % mkdir fedora-x86_64 % cd fedora-x86_64 # Replace XX with the current fedora version % wget https://dl.fedoraproject.org/pub/fedora/linux/releases/XX/Everything/x86_64/os/images/pxeboot/initrd.img % wget https://dl.fedoraproject.org/pub/fedora/linux/releases/XX/Everything/x86_64/os/images/pxeboot/vmlinuz # If you want arm systems... aarch64 % cd .. % mkdir fedora-aarch64 # Replace XX with the current fedora version % wget https://dl.fedoraproject.org/pub/fedora/linux/releases/XX/Everything/aarch64/os/images/pxeboot/initrd.img % wget https://dl.fedoraproject.org/pub/fedora/linux/releases/XX/Everything/aarch64/os/images/pxeboot/vmlinuz Now we can add a couple menu entry items for Fedora. I'm making both EFI and Classic entries to ensure we can boot both EFI and BIOS systems from the same menu. . . . menuentry 'Install Fedora Linux (EFI)' --class fedora --class gnu-linux --class gnu --class os { linuxefi fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os ip=dhcp initrdefi fedora-x86_64/initrd.img } menuentry 'Install Fedora Linux (Classic)' --class fedora --class gnu-linux --class gnu --class os { linux16 fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ ip=dhcp initrd16 fedora-x86_64/initrd.img } # Add the below for ARM systems menuentry 'Install Fedora Linux (ARM)' --class fedora --class gnu-linux --class gnu --class os { linux fedora-aarch64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os/ ip=dhcp initrd fedora-aarch64/initrd.img } Now the Fedora installation should be bootable.","title":"Fedora"},{"location":"el/pxeboot/#customizing-grub","text":"Grub is customizable. It is possible to setup background images, choose menu colors, setup themes, and so on.","title":"Customizing Grub"},{"location":"el/pxeboot/#colors-and-backgrounds","text":"Colors and backgrounds are perfectly possible, even in an EFI setup. We'll need to load a few modules and then set the colors and background we want. Note that if you're using a background, it should live in /var/lib/tftpboot to make things easier. . . . insmod all_video insmod gfxterm insmod gfxterm_menu insmod gfxmenu insmod gfxterm_background insmod png terminal_output gfxterm background_image -m stretch /bg.png set menu_color_highlight=cyan/black set menu_color_normal=white/black set color_normal=white/black . . . The background would be /var/lib/tftpboot/bg.png in this example. Selected items will appear to be cyan and the typical gray selection box is now transparent, which is done by setting it to black. Everything else should appear as white text with a transparent background. Example below.","title":"Colors and Backgrounds"},{"location":"el/pxeboot/#special-submenus","text":"Submenus are easily created using submenu in the grub configuration. For example: submenu 'Fedora Linux' --class fedora --class gnu-linux --class gnu --class os { set menu_color_highlight=black/light-cyan set menu_color_normal=white/black set color_normal=white/black menuentry 'Install Fedora Linux (EFI)' --class fedora --class gnu-linux --class gnu --class os { linuxefi fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os ip=dhcp initrdefi fedora-x86_64/initrd.img } menuentry 'Install Fedora Linux (Classic)' --class fedora --class gnu-linux --class gnu --class os { linux16 fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ ip=dhcp initrd16 fedora-x86_64/initrd.img } menuentry 'Install Fedora Linux (ARM)' --class fedora --class gnu-linux --class gnu --class os { linux fedora-aarch64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os/ ip=dhcp initrd fedora-aarch64/initrd.img } } This now means \"Fedora Linux\" will show up as a menu option and it will take you to a brand new menu with the two listed items, and another color scheme. Note that we created color items because submenus will reset the theme options. Example of how it looks is below. It is also possible to place everything into separate source-able files. Note that when you do this, you will need to symlink those files just like you did with grub.cfg. submenu 'Fedora Linux' --class fedora --class gnu-linux --class gnu --class os { set menu_color_highlight=black/light-cyan set menu_color_normal=white/black set color_normal=white/black source fedora.cfg } menuentry 'Install Fedora Linux (EFI)' --class fedora --class gnu-linux --class gnu --class os { linuxefi fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os ip=dhcp initrdefi fedora-x86_64/initrd.img } menuentry 'Install Fedora Linux (Classic)' --class fedora --class gnu-linux --class gnu --class os { linux16 fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ ip=dhcp initrd16 fedora-x86_64/initrd.img } menuentry 'Install Fedora Linux (ARM)' --class fedora --class gnu-linux --class gnu --class os { linux fedora-aarch64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os/ ip=dhcp initrd fedora-aarch64/initrd.img } Submenus can be nested too. Here's a deeper, working example of my own setup using Fedora 35. # grub.cfg set default=0 set timeout=60 insmod all_video insmod gfxterm insmod gfxterm_menu insmod gfxmenu insmod gfxterm_background insmod png terminal_output gfxterm loadfont /unicode.pf2 background_image -m stretch /bg.png set menu_color_highlight=cyan/black set menu_color_normal=white/black set color_normal=white/black submenu 'Fedora Linux' --class fedora --class gnu-linux --class gnu --class os { set menu_color_highlight=black/light-cyan set menu_color_normal=white/black set color_normal=white/black source fedora.cfg } menuentry 'EFI System Setup' $menuentry_id_option 'uefi-firmware' { fwsetup } menuentry 'Reboot' { reboot } menuentry 'Shutdown' { halt } # fedora.cfg submenu 'Fedora Linux (latest stable)' --class fedora --class gnu-linux --class gnu --class os { set menu_color_highlight=black/light-cyan set menu_color_normal=white/black set color_normal=white/black # EFI Only submenu 'EFI Mode' --class fedora --class gnu-linux --class gnu --class os { set menu_color_highlight=black/light-cyan set menu_color_normal=white/black set color_normal=white/black menuentry 'Install Fedora Linux (No KS)' --class fedora --class gnu-linux --class gnu --class os { linuxefi fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os ip=dhcp initrdefi fedora-x86_64/initrd.img } menuentry 'Install Fedora Linux' --class fedora --class gnu-linux --class gnu --class os { linuxefi fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os ip=dhcp initrdefi fedora-x86_64/initrd.img } menuentry 'Fedora Linux (Rescue Mode)' --class fedora --class gnu-linux --class gnu --class os { linuxefi fedora-x86_64/vmlinuz inst.rescue inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os initrdefi fedora-x86_64/initrd.img } } # Classic Only submenu 'Classic Mode' --class fedora --class gnu-linux --class gnu --class os { set menu_color_highlight=black/light-cyan set menu_color_normal=white/black set color_normal=white/black menuentry 'Install Fedora Linux (No KS)' --class fedora --class gnu-linux --class gnu --class os { linux16 fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ ip=dhcp initrd16 fedora-x86_64/initrd.img } menuentry 'Install Fedora Linux' --class fedora --class gnu-linux --class gnu --class os { linux16 fedora-x86_64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ ip=dhcp initrd16 fedora-x86_64/initrd.img } menuentry 'Fedora Linux (Rescue Mode)' --class fedora --class gnu-linux --class gnu --class os { linux16 fedora-x86_64/vmlinuz inst.rescue inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ initrd16 fedora-x86_64/initrd.img } } # EFI mode for ARM submenu 'EFI Mode (aarch64)' --class fedora --class gnu-linux --class gnu --class os { set menu_color_highlight=black/light-cyan set menu_color_normal=white/black set color_normal=white/black menuentry 'Install Fedora Linux (No KS)' --class fedora --class gnu-linux --class gnu --class os { linuxefi fedora-aarch64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os ip=dhcp initrdefi fedora-aarch64/initrd.img } menuentry 'Install Fedora Linux' --class fedora --class gnu-linux --class gnu --class os { linuxefi fedora-aarch64/vmlinuz inst.repo=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os ip=dhcp initrdefi fedora-aarch64/initrd.img } menuentry 'Fedora Linux (Rescue Mode)' --class fedora --class gnu-linux --class gnu --class os { linuxefi fedora-aarch64/vmlinuz inst.rescue inst.stage2=http://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/aarch64/os initrdefi fedora-aarch64/initrd.img } } }","title":"Special Submenus"},{"location":"el/sysadmin/","text":"This write up provides steps on the System Administrator experience. This is not an end-all, be-all, and has many variables to keep in mind. But can provide a baseline for you. Please keep in mind, this is for Red Hat based distributions, mainly Enterprise Linux 8 and 9. CentOS Stream works within reason. Also note that it will be recommended that you do things in ansible. The RHCE for RHEL 9 will require you to be able to use ansible. As such, we will be focusing on RHEL 9. Recommendations \u00b6 Note Software Replacements Postgresql can be replaced with MySQL/MariaDB Use Katello/Foreman, straight Pulp, or Uyuni You can use any hypervisor other than KVM if you wish, with specific caveats nagios can be replaced with icinga You can replace firewalld with the regular nftables service. This may be required for your virtual host Note General Notes It's recommended to use colored vim syntax. Root doesn't use vim when vim-enhanced is installed. You can make an alias for vi to run vim (not recommended). Turn on syntaxing in ~/.vimrc with syntax on Make the vim colors brighter in ~/.vimrc with set background=dark Export your EDITOR variable in ~/.bash_profile with export EDITOR=vim Keep selinux set to enforcing Note Hardware Requirements RAM - Minimum: 32GB, Recommended: 64GB CPU - Minimum: Intel or AMD Quad Core, Recommended: 8 Core with HyperThreading Storage - Minimum: 4TB, Recommended: 8TB Network - minium 1gb link recommended Please consider on building an actual lab machine that you can do this on. Certification Completions \u00b6 Certification guidelines will be updated later. Notes and Changelog \u00b6 Note Post Experience Notes While this write up uses KVM exclusively, you may want to enhance your learning after the fact by setting up another virtualization platform on your virtual host. It may require you to redesign everything or even start over, but it is something you can consider which you like best overall. Date Changes January 07, 2024 Restructure with markdown Begin \u00b6 We'll now begin the system administrator experience. We will provide from beginning to end, what to do, without giving away what has to be done or has to be configured. This is on you to perform. At the end, there is a \"wiki\" that you create where you will have a chance to document everything you did. I recommend writing down or putting in a word document what you are doing or have done throughout so it'll make your wiki documentation much, much better. Setup a KVM Hypervisor \u00b6 Now you'll need to setup a KVM Hypervisor. You can do this on Fedora 39+ or Enterprise Linux 9. Because EL9 is a stable platform for libvirt, I recommend using going that route. If you want the latest features for the cost of some stability, Fedora will work for you. You may want to make sure your hardware supports virtualization. egrep --color 'vmx|svm' /proc/cpuinfo Recommendations and Options \u00b6 Create multiple datastores (storage pools where the VM images will sit) Example, 2x2TB means you can make two datastores, 4x1TB means you can make four. Attempt to use LVM as the backing for the store. You can create a Volume Group and have the VM's live as Logical Volumes. You can create a Volume Group and have one or more partitions to make \"more\" datastores Destroy the \"built in\" network that libvirt already provides and make your own It already makes virbr0. Make your own OR modify it to not support DHCP/DNS (eg, static only) Hints \u00b6 Most of your commands will be from the following: virsh fdisk/parted pvcreate & vgcreate mkfs To get the most performance out of your VM's disk wise, consider these options: Avoid QCOW2 Use virtio for the hardware whenever possible Set caching to \"none\" for hard disks on VM's when using raw volumes or partitions DHCP and DNS \u00b6 You'll need to setup a DHCP and DNS server. You have a few choices. Create two VM's to run DHCP for HA and create FreeIPA servers to handle DNS (two replicas, doubles as authentication for Linux/UNIX clients) Create two VM's to run DHCP for HA and create two standalone BIND servers as master/slave Use your hypervisor to host DHCP and BIND (not recommended) It would be sensible to do \"1\", if you do \"2\", you at least get more exposure to how zone files are created and the like. For ease of use, we recommend choosing option 1. Also, it is possible to allow cobbler handle DHCP and DNS or integrate directly into DNS such as making changes, but this is outside the scope of this write up. Warning Do NOT run DHCP from the FreeIPA replicas. The FreeIPA servers should have STATIC addresses set. Note When you are setting up DHCP and DNS on separate servers (such as FreeIPA replicas), the DHCP server needs to be configured to tell all the clients the true gateway (this is either a VM in on KVM or a hypervisor of your choice if you are doing straight KVM) and the DNS servers. Setup a VM or your hypervisor as the gateway to the internet. IP forwarding enabled (/etc/sysctl.conf) NAT enabled (firewalld can help you with this, check out the zones) A virtual interface (hypervisor) or a second interface for your network (as a VM) When setting up DHCP and DNS: Decide on a domain name. This can be a domain you own or one you make up internally. I personally used one of my four domains for this lab. RFC expects that internal networks have world routable domains. This is up to you. Do NOT use '.local' domains Setup DNS forwarders to ensure your VM's can get DNS requests from the internet. You create a forwarders { } block with each outside DNS IP listed in BIND or you can optionally set them in the FreeIPA interface. You can list as many as you want. With a default configuration of FreeIPA, forwarders are not strictly required. Do NOT put these extra DNS servers in your dhcpd.conf configuration You need two zones. Forward Zone: This is for your domain, name to an IP. Reverse Zone: This is for reverse IP lookups, IP to a name. FreeIPA handles this for you on setup if you state you are handling a reverse zone and what the subnet is. Bonus Points \u00b6 Setup Dynamic DNS - This requires an almost specific configuration between dhcpd and named (bind) or FreeIPA's named. Dynamic DNS needs to be aware of a domain name Use SSSD for the IPA clients to update their DNS automatically (FreeIPA only) - this may not be required if dhcpd and named are configured correctly Setup an unbound service running on port 9053 that forwards to 1.1.1.1 for encrypted DNS From this point forward, you are to ensure each of your VM's that you create have DNS entries. If you have Dynamic DNS running, you will NOT need to do any manual changes. If using FreeIPA, you may not need to make these changes. You can use nsupdate or the ipa equivalent to add additional entries as needed if you are implementing static A records or CNAME records. Server and Content Management \u00b6 At this point, you'll need to setup Foreman/Katello, Pulp, or Uyuni on a VM. I recommend using Pulp if you want something smaller and simpler. If you want something close to Red Hat Satellite , go through katello. It is a combination of pulp, candlepin, foreman. This recommendation is primarily because of Satellite 6 existing in a large amount of Red Hat shops. Katello, go here . Note Heads up You're going to be hosting repositories, I SERIOUSLY recommend creating a VM that has at least 250GB starting and going from there. Don't try to host Fedora. Katello is resource heavy, you may need to tune it. Pulp may be easier on you, resource wise. Bonus Points \u00b6 Setup errata importation for the Enterprise Linux Channels/Repositories to properly see Advisories and Information for package updates if the repos you are importing does not contain them Create custom kickstarts for your systems (this will help you out later) Kickstart examples can be found at my github . Connect Content Management to Hypervisor \u00b6 Next you will need to connect your Content Management to your hypervisor. View their documentation to get an idea of how it works. Spin Up VM's Using Katello/Spacewalk or PXE Server \u00b6 You will need to spin up two EL8 or EL9 VM's via Katello or PXE. Do not spin them up using virt-install, virt-manager, or anything else. This will require you to connect Katello to the hypervisor. Ensure they are registered properly to your content management server. If you find the clients aren't registering on Katello, click here . If you find that you do not want to use Katello to perform this task, then you can setup cobbler and work it out from there. I currently do not have a tutorial for this, but there is plenty of documentation online. There are also ansible playbooks you could look at for examples if you wanted to go that route, but it may be time consuming and something to setup at the very end. Setup FreeIPA \u00b6 Setup FreeIPA with two replicas, using CA and DNS built in configuration. This is recommended if you do not want to setup BIND by hand. FreeIPA also provides authentication to your systems without having to go through the hassle of setting up OpenLDAP by hand nor having Windows AD. FreeIPA FreeIPA Guide I recommend against setting up OpenLDAP for the case of UNIX authentication. For anything else, go for it. Once FreeIPA is available, all systems should be using FreeIPA as your DNS servers and they should all be enrolled to your domain. Spin Up Two VM's for Databases \u00b6 Create two new VM's from your Content Management or PXE system that are EL9 and install the default postgresql on them. Attempt to install and configure pgpool-II for master-master replication. Note that this may not be default in Enterprise Linux and you can safely skip this. Spin Up Configuration Management \u00b6 While Katello has some form of ansible built in, it may be better to create a solitary configuration management VM and hook it in. Spin up a VM that is EL9 and install a master for configuration management. It is HIGHLY recommended that you use ansible. Ansible is the supported and recommended system by Red Hat and is utilized in the certification exams for EL9. At some point, you could spin up a docker container for AWX if you wanted, but this is not a strict requirement. Spin Up VM for NFS/iSCSI \u00b6 This VM should be EL9. Ensure it has an extra 20GB disk attached to it. Install the following: An NFS server (nfs-utils) An iSCSI server (scsi-target-utils, targetcli) You are to: Export an NFS directory Export a LUN to any server iSCSI for RHEL 9 Deploy Bacula Server \u00b6 Bacula is a backup service. It is actually confusing to setup. It's not easy. There are plenty of write-ups for bacula and RHEL/Enterprise Linux. The digital ocean write-ups are complete, but do NOT give you everything you need to know to do it \"correct\" or to succeed completing this portion. Your server will need the following: Ensure the system has a large disk or a large second disk (this can be any size, start small though) - You can also use your NFS server or iSCSI's LUN. Ensure it is partitioned for ext4 Ensure it is mounted to /bacula If using iSCSI or NFS, ensure the disk from that server is bigger than 20GB. 50 should suffice. Bakula will need to be configured to use postgresql (digital ocean does NOT use postgresql, you will need to do some reading) Register each machine you have to it, storing to flatfile Deploy Two/Four VM's \u00b6 First one/two will be web servers running apache (httpd) Next one/two will be app servers This is a typical \"web/app\" configuration. Some shops use apache frontends to weblogic backends. Sometimes it's tomcat backends. Some shops opt for other methods and software too. If wish to setup Wildfly and host a wiki, you will need to do the following: Setup Wildfly Wiki or on your app servers Setup apache to forward requests to your tomcat servers for the wiki Do this as a VirtualHost configuration with the ServerName as \"wiki.domain.tld\", replacing \"domain.tld\" with your domain Set a ServerAlias as wiki If you wish to setup a Git Deploy Load Balancer VM \u00b6 This will be considered a \"VIP\" of sorts for your wiki and other applications. This VM can either use iptables round-robin or HAProxy. I highly recommend trying both to see what's easier for you. HAProxy is recommended, because it's an actual load balancer application. You will need the following: A DNS CNAME for this machine called \"wiki.domain.tld\", replacing domain.tld with your domain You will need to configure apache to respond to requests for \"wiki.domain.tld\" (virtual host configuration) and forward them on to the app servers HAProxy will need to forward 80 and 443 requests to the two web servers Warning Dynamic DNS If you are using Dynamic DNS, you may need to run rndc sync before making changes in the case of standalone BIND. You will want to use the nsupdate command to make changes to your Dynamic Zones. If you are using FreeIPA DNS this is not required. Deploy Postfix VM \u00b6 You will need to do the following: Ensure postfix is listening on all interfaces Ensure postfix is setup to send and receive messages only from your internal network Setup a gmail account or another relay to allow the above to work to outside mail (this is sort of tricky for gmail, but doable) Bonus Points \u00b6 Create two relays as \"mailhost1\" and \"mailhost2\" for your domain with the same configurations Create a CNAME for \"mailhost.domain.tld\" for your load balancer, forwarding port 25 to both servers Optionally, you can use round-robin DNS instead of HAProxy Setup Nagios VM \u00b6 This will be a monitoring server on EL9. You will need to set it up to use snmp to monitor the communication state of every service above. This means: Is the right port open? I got the right kind of response. Filesystem Space, too full? If you are planning to use full on SNMP, all servers will need the appropriate SNMP ports open and they will need the snmpd clients installed (with a monitor snmpd account) Setup Syslog VM \u00b6 Setup this server as a syslog server. It can be EL8 or higher. Ensure that it is listening on port 514 UDP and TCP in the configuration and that those ports are open. You will need to go to your servers and setup /etc/rsyslog.conf to send ALL logs to this syslog server Optionally, setup an all inclusive logging solution, like graylog, elastic search, mongodb, fluentd. The sky is the limit here! Document Your Work \u00b6 On your new wiki, document everything you did, right now, on your new wiki. RPM Build Server \u00b6 For fun, you can setup a new server that is your designated RPM building machine. You will need to install mock to do this. Optionally, you can setup koji, bodhi, the things that the Fedora project uses. This is not for the faint of heart. Git Server \u00b6 Also for fun, you can setup a git server. There are many options out there. A popular opensource one is Gitea . Ansible \u00b6 Consider setting up ansible and the open source tower. Automate everything via ansible.","title":"The System Administrator Experience"},{"location":"el/sysadmin/#recommendations","text":"Note Software Replacements Postgresql can be replaced with MySQL/MariaDB Use Katello/Foreman, straight Pulp, or Uyuni You can use any hypervisor other than KVM if you wish, with specific caveats nagios can be replaced with icinga You can replace firewalld with the regular nftables service. This may be required for your virtual host Note General Notes It's recommended to use colored vim syntax. Root doesn't use vim when vim-enhanced is installed. You can make an alias for vi to run vim (not recommended). Turn on syntaxing in ~/.vimrc with syntax on Make the vim colors brighter in ~/.vimrc with set background=dark Export your EDITOR variable in ~/.bash_profile with export EDITOR=vim Keep selinux set to enforcing Note Hardware Requirements RAM - Minimum: 32GB, Recommended: 64GB CPU - Minimum: Intel or AMD Quad Core, Recommended: 8 Core with HyperThreading Storage - Minimum: 4TB, Recommended: 8TB Network - minium 1gb link recommended Please consider on building an actual lab machine that you can do this on.","title":"Recommendations"},{"location":"el/sysadmin/#certification-completions","text":"Certification guidelines will be updated later.","title":"Certification Completions"},{"location":"el/sysadmin/#notes-and-changelog","text":"Note Post Experience Notes While this write up uses KVM exclusively, you may want to enhance your learning after the fact by setting up another virtualization platform on your virtual host. It may require you to redesign everything or even start over, but it is something you can consider which you like best overall. Date Changes January 07, 2024 Restructure with markdown","title":"Notes and Changelog"},{"location":"el/sysadmin/#begin","text":"We'll now begin the system administrator experience. We will provide from beginning to end, what to do, without giving away what has to be done or has to be configured. This is on you to perform. At the end, there is a \"wiki\" that you create where you will have a chance to document everything you did. I recommend writing down or putting in a word document what you are doing or have done throughout so it'll make your wiki documentation much, much better.","title":"Begin"},{"location":"el/sysadmin/#setup-a-kvm-hypervisor","text":"Now you'll need to setup a KVM Hypervisor. You can do this on Fedora 39+ or Enterprise Linux 9. Because EL9 is a stable platform for libvirt, I recommend using going that route. If you want the latest features for the cost of some stability, Fedora will work for you. You may want to make sure your hardware supports virtualization. egrep --color 'vmx|svm' /proc/cpuinfo","title":"Setup a KVM Hypervisor"},{"location":"el/sysadmin/#recommendations-and-options","text":"Create multiple datastores (storage pools where the VM images will sit) Example, 2x2TB means you can make two datastores, 4x1TB means you can make four. Attempt to use LVM as the backing for the store. You can create a Volume Group and have the VM's live as Logical Volumes. You can create a Volume Group and have one or more partitions to make \"more\" datastores Destroy the \"built in\" network that libvirt already provides and make your own It already makes virbr0. Make your own OR modify it to not support DHCP/DNS (eg, static only)","title":"Recommendations and Options"},{"location":"el/sysadmin/#hints","text":"Most of your commands will be from the following: virsh fdisk/parted pvcreate & vgcreate mkfs To get the most performance out of your VM's disk wise, consider these options: Avoid QCOW2 Use virtio for the hardware whenever possible Set caching to \"none\" for hard disks on VM's when using raw volumes or partitions","title":"Hints"},{"location":"el/sysadmin/#dhcp-and-dns","text":"You'll need to setup a DHCP and DNS server. You have a few choices. Create two VM's to run DHCP for HA and create FreeIPA servers to handle DNS (two replicas, doubles as authentication for Linux/UNIX clients) Create two VM's to run DHCP for HA and create two standalone BIND servers as master/slave Use your hypervisor to host DHCP and BIND (not recommended) It would be sensible to do \"1\", if you do \"2\", you at least get more exposure to how zone files are created and the like. For ease of use, we recommend choosing option 1. Also, it is possible to allow cobbler handle DHCP and DNS or integrate directly into DNS such as making changes, but this is outside the scope of this write up. Warning Do NOT run DHCP from the FreeIPA replicas. The FreeIPA servers should have STATIC addresses set. Note When you are setting up DHCP and DNS on separate servers (such as FreeIPA replicas), the DHCP server needs to be configured to tell all the clients the true gateway (this is either a VM in on KVM or a hypervisor of your choice if you are doing straight KVM) and the DNS servers. Setup a VM or your hypervisor as the gateway to the internet. IP forwarding enabled (/etc/sysctl.conf) NAT enabled (firewalld can help you with this, check out the zones) A virtual interface (hypervisor) or a second interface for your network (as a VM) When setting up DHCP and DNS: Decide on a domain name. This can be a domain you own or one you make up internally. I personally used one of my four domains for this lab. RFC expects that internal networks have world routable domains. This is up to you. Do NOT use '.local' domains Setup DNS forwarders to ensure your VM's can get DNS requests from the internet. You create a forwarders { } block with each outside DNS IP listed in BIND or you can optionally set them in the FreeIPA interface. You can list as many as you want. With a default configuration of FreeIPA, forwarders are not strictly required. Do NOT put these extra DNS servers in your dhcpd.conf configuration You need two zones. Forward Zone: This is for your domain, name to an IP. Reverse Zone: This is for reverse IP lookups, IP to a name. FreeIPA handles this for you on setup if you state you are handling a reverse zone and what the subnet is.","title":"DHCP and DNS"},{"location":"el/sysadmin/#bonus-points","text":"Setup Dynamic DNS - This requires an almost specific configuration between dhcpd and named (bind) or FreeIPA's named. Dynamic DNS needs to be aware of a domain name Use SSSD for the IPA clients to update their DNS automatically (FreeIPA only) - this may not be required if dhcpd and named are configured correctly Setup an unbound service running on port 9053 that forwards to 1.1.1.1 for encrypted DNS From this point forward, you are to ensure each of your VM's that you create have DNS entries. If you have Dynamic DNS running, you will NOT need to do any manual changes. If using FreeIPA, you may not need to make these changes. You can use nsupdate or the ipa equivalent to add additional entries as needed if you are implementing static A records or CNAME records.","title":"Bonus Points"},{"location":"el/sysadmin/#server-and-content-management","text":"At this point, you'll need to setup Foreman/Katello, Pulp, or Uyuni on a VM. I recommend using Pulp if you want something smaller and simpler. If you want something close to Red Hat Satellite , go through katello. It is a combination of pulp, candlepin, foreman. This recommendation is primarily because of Satellite 6 existing in a large amount of Red Hat shops. Katello, go here . Note Heads up You're going to be hosting repositories, I SERIOUSLY recommend creating a VM that has at least 250GB starting and going from there. Don't try to host Fedora. Katello is resource heavy, you may need to tune it. Pulp may be easier on you, resource wise.","title":"Server and Content Management"},{"location":"el/sysadmin/#bonus-points_1","text":"Setup errata importation for the Enterprise Linux Channels/Repositories to properly see Advisories and Information for package updates if the repos you are importing does not contain them Create custom kickstarts for your systems (this will help you out later) Kickstart examples can be found at my github .","title":"Bonus Points"},{"location":"el/sysadmin/#connect-content-management-to-hypervisor","text":"Next you will need to connect your Content Management to your hypervisor. View their documentation to get an idea of how it works.","title":"Connect Content Management to Hypervisor"},{"location":"el/sysadmin/#spin-up-vms-using-katellospacewalk-or-pxe-server","text":"You will need to spin up two EL8 or EL9 VM's via Katello or PXE. Do not spin them up using virt-install, virt-manager, or anything else. This will require you to connect Katello to the hypervisor. Ensure they are registered properly to your content management server. If you find the clients aren't registering on Katello, click here . If you find that you do not want to use Katello to perform this task, then you can setup cobbler and work it out from there. I currently do not have a tutorial for this, but there is plenty of documentation online. There are also ansible playbooks you could look at for examples if you wanted to go that route, but it may be time consuming and something to setup at the very end.","title":"Spin Up VM's Using Katello/Spacewalk or PXE Server"},{"location":"el/sysadmin/#setup-freeipa","text":"Setup FreeIPA with two replicas, using CA and DNS built in configuration. This is recommended if you do not want to setup BIND by hand. FreeIPA also provides authentication to your systems without having to go through the hassle of setting up OpenLDAP by hand nor having Windows AD. FreeIPA FreeIPA Guide I recommend against setting up OpenLDAP for the case of UNIX authentication. For anything else, go for it. Once FreeIPA is available, all systems should be using FreeIPA as your DNS servers and they should all be enrolled to your domain.","title":"Setup FreeIPA"},{"location":"el/sysadmin/#spin-up-two-vms-for-databases","text":"Create two new VM's from your Content Management or PXE system that are EL9 and install the default postgresql on them. Attempt to install and configure pgpool-II for master-master replication. Note that this may not be default in Enterprise Linux and you can safely skip this.","title":"Spin Up Two VM's for Databases"},{"location":"el/sysadmin/#spin-up-configuration-management","text":"While Katello has some form of ansible built in, it may be better to create a solitary configuration management VM and hook it in. Spin up a VM that is EL9 and install a master for configuration management. It is HIGHLY recommended that you use ansible. Ansible is the supported and recommended system by Red Hat and is utilized in the certification exams for EL9. At some point, you could spin up a docker container for AWX if you wanted, but this is not a strict requirement.","title":"Spin Up Configuration Management"},{"location":"el/sysadmin/#spin-up-vm-for-nfsiscsi","text":"This VM should be EL9. Ensure it has an extra 20GB disk attached to it. Install the following: An NFS server (nfs-utils) An iSCSI server (scsi-target-utils, targetcli) You are to: Export an NFS directory Export a LUN to any server iSCSI for RHEL 9","title":"Spin Up VM for NFS/iSCSI"},{"location":"el/sysadmin/#deploy-bacula-server","text":"Bacula is a backup service. It is actually confusing to setup. It's not easy. There are plenty of write-ups for bacula and RHEL/Enterprise Linux. The digital ocean write-ups are complete, but do NOT give you everything you need to know to do it \"correct\" or to succeed completing this portion. Your server will need the following: Ensure the system has a large disk or a large second disk (this can be any size, start small though) - You can also use your NFS server or iSCSI's LUN. Ensure it is partitioned for ext4 Ensure it is mounted to /bacula If using iSCSI or NFS, ensure the disk from that server is bigger than 20GB. 50 should suffice. Bakula will need to be configured to use postgresql (digital ocean does NOT use postgresql, you will need to do some reading) Register each machine you have to it, storing to flatfile","title":"Deploy Bacula Server"},{"location":"el/sysadmin/#deploy-twofour-vms","text":"First one/two will be web servers running apache (httpd) Next one/two will be app servers This is a typical \"web/app\" configuration. Some shops use apache frontends to weblogic backends. Sometimes it's tomcat backends. Some shops opt for other methods and software too. If wish to setup Wildfly and host a wiki, you will need to do the following: Setup Wildfly Wiki or on your app servers Setup apache to forward requests to your tomcat servers for the wiki Do this as a VirtualHost configuration with the ServerName as \"wiki.domain.tld\", replacing \"domain.tld\" with your domain Set a ServerAlias as wiki If you wish to setup a Git","title":"Deploy Two/Four VM's"},{"location":"el/sysadmin/#deploy-load-balancer-vm","text":"This will be considered a \"VIP\" of sorts for your wiki and other applications. This VM can either use iptables round-robin or HAProxy. I highly recommend trying both to see what's easier for you. HAProxy is recommended, because it's an actual load balancer application. You will need the following: A DNS CNAME for this machine called \"wiki.domain.tld\", replacing domain.tld with your domain You will need to configure apache to respond to requests for \"wiki.domain.tld\" (virtual host configuration) and forward them on to the app servers HAProxy will need to forward 80 and 443 requests to the two web servers Warning Dynamic DNS If you are using Dynamic DNS, you may need to run rndc sync before making changes in the case of standalone BIND. You will want to use the nsupdate command to make changes to your Dynamic Zones. If you are using FreeIPA DNS this is not required.","title":"Deploy Load Balancer VM"},{"location":"el/sysadmin/#deploy-postfix-vm","text":"You will need to do the following: Ensure postfix is listening on all interfaces Ensure postfix is setup to send and receive messages only from your internal network Setup a gmail account or another relay to allow the above to work to outside mail (this is sort of tricky for gmail, but doable)","title":"Deploy Postfix VM"},{"location":"el/sysadmin/#bonus-points_2","text":"Create two relays as \"mailhost1\" and \"mailhost2\" for your domain with the same configurations Create a CNAME for \"mailhost.domain.tld\" for your load balancer, forwarding port 25 to both servers Optionally, you can use round-robin DNS instead of HAProxy","title":"Bonus Points"},{"location":"el/sysadmin/#setup-nagios-vm","text":"This will be a monitoring server on EL9. You will need to set it up to use snmp to monitor the communication state of every service above. This means: Is the right port open? I got the right kind of response. Filesystem Space, too full? If you are planning to use full on SNMP, all servers will need the appropriate SNMP ports open and they will need the snmpd clients installed (with a monitor snmpd account)","title":"Setup Nagios VM"},{"location":"el/sysadmin/#setup-syslog-vm","text":"Setup this server as a syslog server. It can be EL8 or higher. Ensure that it is listening on port 514 UDP and TCP in the configuration and that those ports are open. You will need to go to your servers and setup /etc/rsyslog.conf to send ALL logs to this syslog server Optionally, setup an all inclusive logging solution, like graylog, elastic search, mongodb, fluentd. The sky is the limit here!","title":"Setup Syslog VM"},{"location":"el/sysadmin/#document-your-work","text":"On your new wiki, document everything you did, right now, on your new wiki.","title":"Document Your Work"},{"location":"el/sysadmin/#rpm-build-server","text":"For fun, you can setup a new server that is your designated RPM building machine. You will need to install mock to do this. Optionally, you can setup koji, bodhi, the things that the Fedora project uses. This is not for the faint of heart.","title":"RPM Build Server"},{"location":"el/sysadmin/#git-server","text":"Also for fun, you can setup a git server. There are many options out there. A popular opensource one is Gitea .","title":"Git Server"},{"location":"el/sysadmin/#ansible","text":"Consider setting up ansible and the open source tower. Automate everything via ansible.","title":"Ansible"},{"location":"el/unbound/","text":"Requirements \u00b6 Setup \u00b6 Installation \u00b6 % yum install unbound -y % systemctl enable unbound DNS over TLS (DoT) \u00b6 Setting up DoT with unbound is straight forward, whether you already have a DNS server already or not. Let's go over the most basic configuration. % vi /etc/unbound/unbound.conf server: . . . # Set the below to an IP address if you wish - as I have multiple VLAN's # it is just easier for me to listen everywhere interface: 0.0.0.0 interface: :: # Optionally set a port - I have bind already running, so port 9053 works interface-automatic: no port: 9053 . . . # Set access control rules here. I'll show a few examples with just two of # my networks # REFUSE everything access-control: 0.0.0.0/0 refuse access-control: ::0/0 refuse # Allow localhost to snoop access-control: 127.0.0.1/32 allow_snoop access-control: ::1 allow_snoop # Allow the entire localhost subnet access-control: 127.0.0.0/8 allow access-control: ::ffff:127.0.0.1 allow # Allow my main network and sandbox network access-control: 10.100.0.0/24 allow access-control: 10.100.1.0/24 allow . . . # Ensure tls-cert-bundle is set tls-cert-bundle: /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem . . . # Create the forward zone for DoT queries forward-zone: name: \".\" forward-tls-upstream: yes # Cloudflare forward-addr: 1.1.1.1@853#cloudflare-dns.com forward-addr: 1.0.0.1@853#cloudflare-dns.com forward-addr: 2606:4700:4700::1111@853#cloudflare-dns.com forward-addr: 2606:4700:4700::1001@853#cloudflare-dns.com # Quad9 forward-addr: 9.9.9.9@853#dns.quad9.net forward-addr: 149.112.112.112@853#dns.quad9.net % systemctl enable unbound --now # If you are using bind already with forwarders, you should edit it. Example. % vi /etc/named.conf options { . . . forwarders { # This assumes your bind server and unbound server are on # the same server like I did. 127.0.0.1 port 9053; }; forward only; . . .","title":"Unbound"},{"location":"el/unbound/#requirements","text":"","title":"Requirements"},{"location":"el/unbound/#setup","text":"","title":"Setup"},{"location":"el/unbound/#installation","text":"% yum install unbound -y % systemctl enable unbound","title":"Installation"},{"location":"el/unbound/#dns-over-tls-dot","text":"Setting up DoT with unbound is straight forward, whether you already have a DNS server already or not. Let's go over the most basic configuration. % vi /etc/unbound/unbound.conf server: . . . # Set the below to an IP address if you wish - as I have multiple VLAN's # it is just easier for me to listen everywhere interface: 0.0.0.0 interface: :: # Optionally set a port - I have bind already running, so port 9053 works interface-automatic: no port: 9053 . . . # Set access control rules here. I'll show a few examples with just two of # my networks # REFUSE everything access-control: 0.0.0.0/0 refuse access-control: ::0/0 refuse # Allow localhost to snoop access-control: 127.0.0.1/32 allow_snoop access-control: ::1 allow_snoop # Allow the entire localhost subnet access-control: 127.0.0.0/8 allow access-control: ::ffff:127.0.0.1 allow # Allow my main network and sandbox network access-control: 10.100.0.0/24 allow access-control: 10.100.1.0/24 allow . . . # Ensure tls-cert-bundle is set tls-cert-bundle: /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem . . . # Create the forward zone for DoT queries forward-zone: name: \".\" forward-tls-upstream: yes # Cloudflare forward-addr: 1.1.1.1@853#cloudflare-dns.com forward-addr: 1.0.0.1@853#cloudflare-dns.com forward-addr: 2606:4700:4700::1111@853#cloudflare-dns.com forward-addr: 2606:4700:4700::1001@853#cloudflare-dns.com # Quad9 forward-addr: 9.9.9.9@853#dns.quad9.net forward-addr: 149.112.112.112@853#dns.quad9.net % systemctl enable unbound --now # If you are using bind already with forwarders, you should edit it. Example. % vi /etc/named.conf options { . . . forwarders { # This assumes your bind server and unbound server are on # the same server like I did. 127.0.0.1 port 9053; }; forward only; . . .","title":"DNS over TLS (DoT)"},{"location":"training/ex362/","text":"This page contains the necessary resources to help you prepare for the Red Hat Certified Specialist in Identity Management exam, EX362. This follows the youtube playlist as much as possible with various examples and ideas. Soon to come, you will also find our own example practice exam for you to try your hand at to test your knowledge. The list of objectives can be found here . Note that the exam objectives can change at any time. It is the responsibility of the reader to always review the objectives prior to studying and taking the exam to ensure success. Note Affiliation and Exam Information Please note that we are not affiliated with Red Hat. The materials and examples used are our own and do not reflect the training programs provided by Red Hat and are educational only. We do not disclose any of the tasks, questions, or material on the exam as it would violate the NDA. Any questions sent to us about anything directly related to the exam will not be answered. We also do not provide any one-on-one tutoring or online teaching courses. If exam objectives have changed to where the videos and this material are missing information, we can add on at any time upon request. If exam objectives have not changed but operational tasks have, we will note them as we find them. If there are things about FreeIPA that you'd like to see in the videos that may fit into objective, we can add it also upon request. However, it is likely those extra things would be better suited in the separate FreeIPA section on this site. Overview \u00b6 The video series goes over setting up FreeIPA in a lab/VM environment by following the objectives as outlined by Red Hat. The list of objectives can be found here . Exam Information \u00b6 The EX362 exam tests your knowledge in a real world format style test - Meaning just like any Red Hat exam, it is performance-based and you perform tasks as if you were on the job. You are evaluated on the tasks you perform and if they meet the objective criteria. The EX362 is related to FreeIPA or Red Hat Identity Management and counts toward the RHCA (Red Hat Certified Architect). To take the exam, you must have at least an RHCSA. If you are attempting to become a Red Hat Certified Architect, you must have an RHCE. Resources \u00b6 FreeIPA Red Hat Documentation Trust Anatomy/SSSD Troubleshooting Directory Server Tuning FreeIPA Workshop Curriculum Our Page Hardware Recommendations \u00b6 The minimum requirements for IdM are fairly low. 2GB of RAM, 1 core, and a 10GB disk. However, we believe that's too low, especially if we plan on scaling out. And during upgrades, you would need at least 4GB of RAM for the operations to be successful. Below are our minimum recommendations: 2 (virtual) CPU Core 4 GB of RAM 10GB+ disk or partition for /var/lib/dirsrv Per the Red Hat documentation, consider that with at least 10k users and 100 groups, you would need at least 3GB of RAM and 1GB swap. If you end up having 100k users and 50k groups, then 16GB of RAM and 4GB of swap is recommended. In fact, in larger deployments, it's more effective to increase RAM than disk, as most data is stored in cache. View the resources above in the previous section for directory server tuning information. IdM Server Installation and Configuration \u00b6 Install IdM in a scalable, fault tolerant environment \u00b6 Server Name IP Address idm1.example.com 192.168.15.2 idm2.example.com 192.168.15.3 !!! note:: IPA Servers should either have a DHCP reservation or a static address. In the event that you have either, DNS should always be pointing at 127.0.0.1, especially if your replica serves DNS. Both of our replicas serve DNS, so loopback is sufficient and recommended for our name server. In later versions of FreeIPA, there is support to force network manager to ensure resolv.conf is loopback without the need to set it by hand with nmcli. # Set a static address - It's important for your IdM servers # to have static addresses or a DHCP reservation. % nmcli con mod eth0 ipv4.address 192.168.15.2/24 % nmcli con mod eth0 ipv4.gateway 192.168.15.1 % nmcli con mod eth0 ipv4.method manual % nmcli con mod eth0 ipv4.dns-search example.com # You should set this if your replica serves DNS! If not, set it to # one or more of your IdM replicas that do. % nmcli con mod eth0 ipv4.dns 127.0.0.1 % nmcli con up eth0 # Examples of using ipa-server-install # RHEL 9 % yum install ipa-server ipa-server-dns ipa-client sssd sssd-ipa # Installation, interactive, does not setup specific components % ipa-server-install # Installation, mostly automatic (recommended) # This will setup DNS and the necessary pieces for an AD trust # Optionally, you can use the --netbios-name switch to set your forest netbios name % ipa-server-install --domain example.com --realm EXAMPLE.COM \\ --reverse-zone=15.168.192.in-addr.arpa. \\ --no-forwarders \\ --no-ntp \\ --setup-dns \\ --setup-adtrust \\ -p Passw0rd! \\ -a Passw0rd! # Configure the firewall for RHEL 7 % firewall-cmd --permanent --add-service={ntp,http,https,freeipa-ldap,freeipa-ldaps,kerberos,freeipa-replication,kpasswd,dns} # RHEL 8 % firewall-cmd --permanent --add-service={freeipa-4,ntp,dns} % kinit admin # We need to make sure that any A records get a corresponding PTR record, otherwise you're making them manually. % ipa dnsconfig-mod --allow-sync-ptr=True # Adding a replica % ipa-replica-install --setup-dns \\ --setup-ca \\ --no-forwarders # Adding a replica unattended without forwarders % ipa-client-install --realm EXAMPLE.COM % kinit admin % ipa hostgroup-add-member --hosts=ipa02.example.com ipaservers % ipa-replica-install --setup-dns \\ --setup-ca \\ --no-forwarders \\ --unattended Creating Users, Groups, and Policies \u00b6 Users Login Name Type Group Role UID/GID John Smith jsmith Normal admins Auto Bob Rufus brufus Normal corp Auto Larry Dufus ldufus Normal helpdesk Auto Robert Cole rcole Staged Auto Thomas Snyder tsnyder Preserved Auto SysHost Management syshostmgt Normal Host Manager 10000 Groups Policy HelpDesk helpdesk corp enrollers Enrollment Administrator Roles Privilege Host Manager Host administrators Host group administrators Netgroups administrators Host enrollment Note Custom UID/GID It is possible to create the users with a custom uid/gid with the switches --uid and --gidnumber which you will see below. It is also possible to set random passwords with --random. See ipa user-add --help for more switches. Note Password Expiration When you make a user with the --password switch or use ipa passwd to set a password, it is automatically expired and must be changed on next login. If you want to avoid this from happening, you will need to set a random password via --password or --random, and then use kpasswd username to change it to the desired password. This does not make the account non-expiring. # Creating users with a password, create all the accounts from the table (except from syshost) % ipa user-add --first=\"John\" --last=\"Smith\" --password jsmith # Create the system account with a password of Sup3R$ecre7! and a UID of 10000 % ipa user-add --first=\"SysHost\" --last=\"Management\" --uid=10000 --gidnumber=10000 --password syshostmgt # Stage a user % ipa stageuser-add --first=\"Robert\" --last=\"Cole\" rcole # Preserve a user % ipa user-del tsynder --preserve # Create a regular (POSIX) group % ipa group-add corp # Create a member only group % ipa group-add --nonposix HelpDesk % ipa group-add --nonposix enrollers # Add the HelpDesk group to the helpdesk policy # Add the enrollers group to the Enrollment Administrator role % ipa role-add-member \"helpdesk\" --groups=HelpDesk % ipa role-add-member \"Enrollment Administrator\" --groups=enrollers # Create a role with privileges % ipa role-add \"Host Manager\" % ipa role-add-privilege \"Host Manager\" \\ --privileges=\"Host administrators\" \\ --privileges=\"Host group administrators\" \\ --privileges=\"Netgroups administrators\" \\ --privileges=\"Host enrollment\" # Add the syshostmgt user as a member of the role % ipa role-add-member \"Host Manager\" --users=\"syshostmgt\" # Set our user passwords to CentOS123!$ so that way we don't have to change them later % kpasswd jsmith # If we already set the password we want but we don't want it to expire without making a policy or prompt for a password change (NOT RECOMMENDED) % ldapmodify -x -w 'Passw0rd!' -D 'cn=Directory Manager' dn: uid=syshostmgt,cn=users,cn=accounts,dc=example,dc=com changetype: modify delete: krbLastPwdChange (Press CTRL+D) New Passwords Expired \u00b6 The common question we receive (and even the #freeipa IRC receive) is \"Why can't we just set the password to not be expired right away?\" See this page for information on why this is. You may also look at the pagure page and the Red Hat bugzilla related bug . Implement a SSO \u00b6 To setup a very, very simple SSO, you can setup a simple location that requires a login. % ipa-getkeytab -s idm1.example.com -p http/http.example.com -k /etc/httpd/conf/http.keytab % vi /etc/httpd/conf.d/location.conf AuthType Kerberos AuthName \"IPA Kerberos Auth\" # Keytab Krb5Keytab /etc/httpd/conf/http.keytab # Kerb settings KrbMethodNegotiate on KrbMethodK5Passwd on KrbServiceName HTTP KrbAuthRealms EXAMPLE.COM KrbSaveCredentials off Require valid-user IdM Client Installation and Configuration \u00b6 Install and configure IdM Clients \u00b6 Client Name IP Address client.example.com 192.168.15.10 nfs.example.com 192.168.15.11 utility.example.com 192.168.15.12 Note Depending on your architecture and setup, IdM clients should either be pointing directly at the IdM servers for DNS (at least two of them) or pointing at the DNS server in the environment that is delegating that domain to the IdM domain controllers. In our lab, our IdM servers are our only DNS servers, thus it makes sense that our clients should point to them. In that scenario, you would configure your DHCP server to use the IdM servers as the name servers and/or configure them in a static manner depending on your environment. # If your client is not pointing at the IdM DNS and you # don't have another DNS server that's performing delegation, # change your name servers. % nmcli con mod eth0 ipv4.dns 192.168.15.2 % nmcli con mod eth0 +ipv4.dns 192.168.15.3 % nmcli con mod eth0 ipv4.dns-search example.com # Optionally, if your clients don't have DHCP # reservations, set a static address. % nmcli con mod eth0 ipv4.address 192.168.15.10/24 % nmcli con mod eth0 ipv4.gateway 192.168.15.1 % nmcli con mod eth0 ipv4.method manual # It might be a good idea to set your hostname if you haven't already % hostnamectl set-hostname client.example.com % hostname client.example.com # Install the ipa-client packages % yum install ipa-client -y % ipa-client-install --realm EXAMPLE.COM --domain example.com . . . % id admin uid=686600000(admin) gid=686600000(admins) groups=686600000(admins) Configure Kerberized services \u00b6 One of the things that you may end up doing, whether by hand or in an automated fashion, is creating kerberized services. In a previous section, we addressed creating an NFS service for both a server and a client for the purpose of automating home directory mounts on a client when a user logs in. So you already have the idea of what this entails. # Create kerberos service % ipa service-add HTTP/http.example.com Not only that, it's probably a good idea to actually get the keytab. % kinit admin % ipa-getkeytab -s idm1.example.com -p HTTP/http.example.com -k /etc/krb5.keytab For an example of automating keytab creation and retrieval, see the CentOS/FreeIPA page on this site. IdM HA Configuration \u00b6 Configure and manage a certificate authority \u00b6 By default FreeIPA stands up its own CA. And because of this, this allows you or your workplace to be able to issue certificates, that can be used in a wide variety of services, the most common or obvious one would be for Apache httpd. There's a couple of ways you can get a certificate signed by FreeIPA. One method is to generate your own CSR and request it to be signed by FreeIPA. Another way is you can do it all from one command, ipa-getcert , and optionally, either have the certificate in PEM format or an NSS database. We'll address these examples. # Creating an SSL certificate in the PEM format % ipa service-add HTTP/http.example.com % ipa-getcert request -f /etc/pki/tls/certs/http.pem -k /etc/pki/tls/private/http.key -K HTTP/http.example.com -D http.example.com New signing request \"20190902000318\" added. # Verify % ipa-getcert list Number of certificates and requests being tracked: 1. Request ID '20190902000318': status: MONITORING stuck: no key pair storage: type=FILE,location='/etc/pki/tls/private/http.key' certificate: type=FILE,location='/etc/pki/tls/certs/http.pem' CA: IPA issuer: CN=Certificate Authority,O=EXAMPLE.COM subject: CN=http.example.com,O=EXAMPLE.COM expires: 2021-09-02 00:03:19 UTC dns: http.example.com principal name: HTTP/http.example.com@EXAMPLE.COM key usage: digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment eku: id-kp-serverAuth,id-kp-clientAuth pre-save command: post-save command: track: yes auto-renew: yes # Create an SSL certificate in the NSS format % ipa-getcert request -d /etc/pki/tls/certs/nss -n 'Test' -K HTTP/http.example.com -D http.example.com New signing request \"20190902000756\" added. # Verify % ipa-getcert list . . . Request ID '20190902000756': status: MONITORING stuck: no key pair storage: type=NSSDB,location='/etc/pki/tls/certs/nss',nickname='Test',token='NSS Certificate DB' certificate: type=NSSDB,location='/etc/pki/tls/certs/nss',nickname='Test',token='NSS Certificate DB' CA: IPA issuer: CN=Certificate Authority,O=EXAMPLE.COM subject: CN=http.example.com,O=EXAMPLE.COM expires: 2021-09-02 00:07:57 UTC dns: http.example.com principal name: HTTP/http.example.com@EXAMPLE.COM key usage: digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment eku: id-kp-serverAuth,id-kp-clientAuth pre-save command: post-save command: track: yes auto-renew: yes By default, when a certificate request is performed (and succeeds to be signed by the IPA CA), it is typically tracked and auto-renewed by default. This is done by the certmonger service, which eliminates the need to have to renew anything by hand. Create Secret Vaults \u00b6 When a domain supports the KRA role, it can hold password vaults or anything that's considered \"secret\". You can add the KRA role by simply running on each relevant domain controller: % ipa-kra-install (more to come) IdM Users and Policies Management \u00b6 In FreeIPA, there are two sets of policies: Role Based Access Control (RBAC) which are the permissions, delegated or otherwise, that allow (or deny) access to various pieces of FreeIPA. This can be users that have the ability to reset passwords, modify groups, or perhaps they can issue keytabs. This was partially covered in a previous section. Host Based Access Control (HBAC) which are the permissions granted to a user or users to access systems on various (PAM) services, such as ssh or logging into a desktop system (eg, GDM). Configure Policies and User Access \u00b6 HBAC, or Host Based Access Controls, are permissions that grant user or users access to systems via any number of services. The services are PAM services. No doubt you have looked in /etc/pam.d before and have seen quite a few files or even modified them by hand at some point. % ls -l /etc/pam.d/ total 80 -rw-r--r--. 1 root root 272 May 11 2019 atd -rw-r--r--. 1 root root 232 Apr 15 15:28 config-util -rw-r--r--. 1 root root 328 Nov 8 2019 crond lrwxrwxrwx. 1 root root 32 Jan 14 2020 fingerprint-auth -> /etc/authselect/fingerprint-auth -rw-r--r--. 1 root root 70 Apr 24 06:35 ksu -rw-r--r--. 1 root root 715 Apr 24 05:38 login -rw-r--r--. 1 root root 154 Apr 15 15:28 other -rw-r--r--. 1 root root 168 Apr 6 20:08 passwd lrwxrwxrwx. 1 root root 29 Jan 14 2020 password-auth -> /etc/authselect/password-auth -rw-r--r--. 1 root root 155 Apr 8 22:00 polkit-1 lrwxrwxrwx. 1 root root 25 Jan 14 2020 postlogin -> /etc/authselect/postlogin -rw-r--r--. 1 root root 640 Apr 24 05:38 remote -rw-r--r--. 1 root root 143 Apr 24 05:38 runuser -rw-r--r--. 1 root root 138 Apr 24 05:38 runuser-l lrwxrwxrwx. 1 root root 30 Jan 14 2020 smartcard-auth -> /etc/authselect/smartcard-auth lrwxrwxrwx. 1 root root 25 Jun 15 10:18 smtp -> /etc/alternatives/mta-pam -rw-r--r--. 1 root root 76 Apr 6 20:11 smtp.postfix -rw-r--r--. 1 root root 727 Feb 4 2020 sshd -rw-r--r--. 1 root root 214 Apr 23 20:48 sssd-shadowutils -rw-r--r--. 1 root root 566 Apr 24 05:38 su -rw-r--r--. 1 root root 154 Apr 23 19:40 sudo -rw-r--r--. 1 root root 178 Apr 23 19:40 sudo-i -rw-r--r--. 1 root root 137 Apr 24 05:38 su-l lrwxrwxrwx. 1 root root 27 Jan 14 2020 system-auth -> /etc/authselect/system-auth -rw-r--r--. 1 root root 248 Jul 21 07:57 systemd-user -rw-r--r--. 1 root root 84 May 11 2019 vlock On a typical Red Hat system, the most common ones (such as su , sshd , sudo ) imports the system-auth file, so the login request is processed through those means. When defining HBAC rules, you either must allow \"all\" services or be selective. For example, if an HBAC rule allows \"sshd\", a user is allowed to ssh into a system, but wouldn't allow them to login locally, as that goes through login . If you want the user to be able to run the su and sudo commands, you would also need to allow those services. Otherwise, the user is denied, despite sudo policies being available. [label@mgt ~]$ sudo -i [sudo] password for label: sudo: PAM account management error: Permission denied In FreeIPA, there is typically a rule already predefined that allows everyone to access all systems and all services. This can be removed or disabled and this removes host access to everything immediately. This is typically recommended in most environments where there are security standards and procedures in place. # To disable % ipa hbacrule-disable allow_all # To delete instead % ipa hbacrule-del allow_all When performing a FreeIPA installation, it is possible to add --no-hbac-allow that will disable the allow_all rule. Below are some examples of adding access. # Allow all admins to access all systems % ipa hbacrule-add --hostcat=all --servicecat=all --desc='Allow all admins to access all systems' All_Admins % ipa hbacrule-add-user --groups=admins All_Admins # And then test... % ipa hbactest --rules=All_Admins --user=jsmith --host=client.example.com --service=login # Allow the corp users to access the client system only using the sshd pam services % ipa hbacrule-add --desc='Allow corp users to access client on ssh' corp_access % ipa hbacrule-add-user --groups=corp corp_access % ipa hbacrule-add-host --hosts=client.example.com corp_access % ipa hbacrule-add-service --hbacsvcs=sshd corp_access # And then test... % ipa hbactest --rules=corp_access --user=brufus --host=client.example.com --service=sshd Configure roaming/automounted home directories \u00b6 You will need to configure your NFS server to serve up roaming home directories for users and then your client should have automouting enabled. Note Client Kerberos Service It may not be required to create an nfs kerberos service for the client. The ipa-client-automount command may already handle this but it does not hurt to create one. In fact, the host keytab is used on the client side anyway. Creating an NFS client keytab may have been required back in the EL6 days. # IDM Steps % kinit admin % ipa service-add nfs/nfs.example.com % ipa service-add nfs/client.example.com # Setup the automounting locations % ipa automountmap-add default auto.home % ipa automountkey-add default --key \"/home\" --info auto.home auto.master % ipa automountkey-add default --key \"*\" --info \"-fstype=nfs4,rw,sec=krb5,soft nfs.example.com:/exports/home/&\" auto.home # NFS Server Steps % yum install nfs-utils -y % mkdir /exports/home % vi /etc/exports /exports/home *(rw,sec=sys:krb5:krb5i:krb5p) # Make the home directories for all users and move them to /export/home % mkhomedir_helper jsmith % mv /home/jsmith /export/home/ # Create the necessary keytabs % kinit admin % ipa-getkeytab -s idm1.example.com -p nfs/nfs.example.com -k /etc/krb5.keytab # Verify keytab % klist -ket /etc/krb5.keytab # Enable and start nfs % systemctl enable nfs-server --now # Open the necessary firewall ports % firewall-cmd --add-service=nfs --permanent % firewall-cmd --complete-reload # Client steps % kinit admin % ipa-getkeytab -s idm1.example.com -p nfs/client.example.com -k /etc/krb5.keytab % ipa-client-automount --location=default # Verify keytab % klist -ket /etc/krb5.keytab To test, login to the system via ssh or console and verify the home directory has mounted. /var/log/messages and secure will display errors in case of failure. Configure IdM as an LDAP backend for external services \u00b6 Most services and applications that authenticate users do typically have LDAP support. IdM can be used as an LDAP backend. You typically need only a few things to authenticate users from IdM to an application. Base DN, this always ends up being the top level of your domain: dc=example,dc=com - All accounts share this common base. Bind DN, this is a system account that binds to the directory to assist with searches and authentication Attribute mappings Groups, depending on the application Below is a table of common DN's you may specify in an application: DN's Path Filter (if applicable) Base DN dc=example,dc=com User DN cn=users,cn=accounts,dc=example,dc=com uid=... Group DN cn=groups,cn=accounts,dc=example,dc=com (objectClass=groupOfNames) Bind DN uid=account,cn=sysaccounts,cn=etc,dc=example,dc=com % ipa user-show admin --all | grep '^dn' dn: uid=admin,cn=users,cn=accounts,dc=example,dc=com Below is a table of common attributes that may be used to map user information in the application. Type Attribute Login Name uid First Name givenName Surname sn Email mail Groups memberOf Full Name cn Below are two ways to create a bind account (bind DN). The first way is the LDAP way. The second way is the ipa-ldap-updater. % kinit admin % ldapadd -Y GSSAPI . . . dn: uid=binder,cn=sysaccounts,cn=etc,dc=example,dc=com objectclass: account objectclass: simplesecurityobject uid: binder userPassword: password123 passwordExpirationTime: 20380119031407Z nsIdleTimeout: 0 # Press CTRL+d adding new entry \"uid=binder,cn=sysaccounts,cn=etc,dc=example,dc=com\" % kinit admin % cat << EOF > binder.update dn: uid=binder,cn=sysaccounts,cn=etc,dc=example,dc=com add:objectclass:account add:objectclass:simplesecurityobject add:uid:binder add:userPassword:password123 add:passwordExpirationTime:20380119031407Z add:nsIdleTimeout:0 EOF % ipa-ldap-updater binder.update When this account is created, you can then specify the full DN for that object into a bind DN field, along with it's password into an accompanying bind password field. If you'd like an example of setting up Ansible Tower (or AWX, the open source version of tower) against IdM, you can click here . Note Kerberos On some applications, it is possible to use kerberos authentication rather than a straight bind account. The general idea is the same when picking out the base dn, attributes, and the like. However, instead you would create an account with an accompanying LDAP/... service principal to do the authentication. Creating Trust with AD Domain \u00b6 Create trust relationships with Active Directory \u00b6 Note AD Setup We do not cover setting up an AD forest here. This is out of scope for this series. If you are using Server 2016 or higher and you are using \"core\", look up the commands: Install-WindowsFeature AD-domain-services Import-Module ADDSDeployment Install-ADDSForest Server Name IP Address ad.example.net 192.168.15.15 For our trust, the AD server will need to be configured to be the example.net domain with the hostname of ad.example.net. This way, we are not colliding in DNS and both AD and IdM should be able to communicate with each other as two separate forests. It is recommended to use Windows Server 2016 (with the same domain functional level) for this setup, as experience with that product is a recommended prerequisite for the exam. % yum install ipa-server-trust-ad -y % firewall-cmd --add-service=freeipa-trust --permanent success % firewall-cmd --reload success % ipa-adtrust-install . . . # This is the admin@REALM IPA account admin password: WARNING: The smb.conf already exists. Running ipa-adtrust-install will break your existing samba configuration. # Type 'yes' here Do you wish to continue? [no]: yes Do you want to enable support for trusted domains in Schema Compatibility plugin? This will allow clients older than SSSD 1.9 and non-Linux clients to work with trusted users. # You can press enter here to accept the default. If you have BSD, Solaris, Omnios, HP-UX, AIX, or RHEL 5 and older clients # you may want to enable this. Some apps may benefit from this also. Enable trusted domains support in slapi-nis? [no]: Enter the NetBIOS name for the IPA domain. Only up to 15 uppercase ASCII letters, digits and dashes are allowed. Example: EXAMPLE. # You can accept the default or put your own. NetBIOS domain name [IPA]: IPA0 WARNING: 4 existing users or groups do not have a SID identifier assigned. Installer can run a task to have ipa-sidgen Directory Server plugin generate the SID identifier for all these users. Please note, in case of a high number of users and groups, the operation might lead to high replication traffic and performance degradation. Refer to ipa-adtrust-install(1) man page for details. # You should always say yes. Do you want to run the ipa-sidgen task? [no]: yes . . . # This will complete and list ports to open and such. We did this earlier. Now that the AD trust components are prepped, depending on the setup, we'll need to do some DNS zone forwards. It is likely you have IPA and AD running their own DNS. Note : This may not be the case in a real world scenario. # We need to create a forward zone here for the example.net zone % ipa dnsforwardzone-add example.net --forwarder=192.168.15.15 --forward-policy=only Server will check DNS forwarder(s). This may take some time, please wait ... Zone name: example.net. Active zone: TRUE Zone forwarders: 192.168.15.15 Forward policy: only # We should probably create a few dns records... # Assuming the AD netbios name is EXAMPLEAD, use the syntax hostname.NETBIOS here % ipa dnsrecord-add example.com ad.EXAMPLEAD --a-ip-address=192.168.15.15 # Same idea here, but we're only doing the netbios name and saying the name server record is the AD server % ipa dnsrecord-add example.com EXAMPLEAD --ns-hostname=ad.EXAMPLEAD # We need to allow the zones to be transferable to the AD domain % ipa dnszone-mod example.com --allow-transfer=192.168.15.15 On the AD side, we need to create the IPA zone. It's absolutely required. C:\\Windows\\System32>dnscmd 127.0.0.1 /ZoneAdd example.com /Secondary 192.168.15.2 You should probably double check that the DNS records are returning on the IDM servers. % dig _ldap._tcp.example.com SRV ; <<>> DiG 9.9.4-RedHat-9.9.4-61.el7 <<>> SRV _ldap._tcp.example.com ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 14793 ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 2 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;_ldap._tcp.example.com. IN SRV ;; ANSWER SECTION: _ldap._tcp.example.com. 86400 IN SRV 0 100 389 idm1.example.com. _ldap._tcp.example.com. 86400 IN SRV 0 100 389 idm2.example.com. ;; AUTHORITY SECTION: example.com. 86400 IN NS idm1.example.com. example.com. 86400 IN NS idm2.example.com. ;; ADDITIONAL SECTION: idm1.example.com. 1200 IN A 192.168.15.2 idm2.example.com. 1200 IN A 192.168.15.3 # Same with the AD records % dig _ldap._tcp.example.net SRV ; <<>> DiG 9.9.4-RedHat-9.9.4-61.el7 <<>> SRV _ldap._tcp.example.net ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12195 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 4, ADDITIONAL: 9 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;_ldap._tcp.example.net. IN SRV ;; ANSWER SECTION: _ldap._tcp.example.net. 600 IN SRV 0 100 389 ad.example.net. . . . Now that they are returning, intiate the trust. % ipa trust-add --type=ad example.net --admin Administrator --password Active Directiron domain administrator's password: (type password here) ----------------------------------------------------- Added Active Directory trust for realm \"example.net\" ----------------------------------------------------- Realm name: example.net Domain NetBIOS name: EXAMPLEAD Domain Security Identifier: S-1-5-21-XXXXXXXXXX-YYYYYYYYY-ZZZZZZZZZZ Trust direction: Trusting forest Trust type: Active Directory domain Trust status: Established and verified # Check that an AD user is resolvable. You can do this with DOMAIN\\name or name@DOMAIN % id EXAMPLEAD\\\\administrator % id administrator@example.net Authenticate users with an Active Directory domain \u00b6 As we disabled the allow_all rule, let's create a set of groups first and then the HBAC rule. # Create the starting AD group % ipa group-add adusers # Create an external group. This is required for AD users. % ipa group-add --external adgroup_external # Add an AD user into the external group % ipa group-add-member --users=administrator@example.net adgroup_external # Make the external group a member of ad users % ipa group-add-member --groups=adgroup_external adusers As we've made an HBAC rule before, this should be simple. % ipa hbacrule-add --hostcat=all --servicecat=all --desc='ad users all access' adusers_access % ipa hbacrule-add-user --groups=adusers adusers_access % ipa hbactest --rules=adusers_access --user=administrator@example.net --host=client.example.com --service=sshd The test should pass without any issues. Note Group Types While this may not be required information while working on the exam, it's important to understand that there are different group types in AD and the behavior changes based on the group type. Groups in Active Directory have three types. These three types can actually change the behavior of how SSSD on the IPA domain controllers resolve them or if they'll even be resolvable at all. The three types are 'Domain Local', 'Global', and 'Universal'. If at all possible, avoid groups being 'Global'. Domain Local or Universal is recommended. IdM maintenance \u00b6 Back up an IdM infrastructure \u00b6 There are multiple ways you can backup IPA. Full backup: Default, shuts down IPA before performing a backup. This backs up with raw files. As such, it must be done offline. Data backup: Backs up a copy of the ldap data and the changelog (the IPA-REALM instance, DogTag, IPA backend). This can be done online. # Turns off IPA completely and perform a backup % ipa-backup # Backs up and gpg encrypts % ipa-backup --gpg --gpg-keyring=/root/keys To restore a backup, the ipa-restore command is available. % ipa-restore /var/lib/ipa/backup/ Perform a backup without interruption of services \u00b6 The backup command allows you to pass an online flag to ensure a backup taken doesn't down the IPA services. Note that not everything can be backed up online. # Backs up data only and doesn't take down IPA % ipa-backup --data --online # Backs up data only and gpg encrypts % ipa-backup --gpg --gpg-keyring=/root/keys --data --online Value Add \u00b6 Use the REST api to query IdM \u00b6 When you invoke the ipa command, you are actually communicating with the API that runs on the IdM replicas. Operations done are sent via a POST with JSON data. The return data is also in JSON and translated to be readable in the terminal. Because it's JSON, custom scripts can be made with say perl or python that communicates with the API to send the calls, perhaps for specific tasks, jobs, or other operations that could be automated. You can also use curl to do this also if you so choose. The question becomes, \"well, how do I know the right data to send?\" You can issue the -vv switch to see the request being sent. % ipa -vv ping ipa: INFO: trying https://idm1.example.com/ipa/json ipa: INFO: [try 1]: Forwarding 'schema' to json server 'https://idm1.example.com/ipa/json' ipa: INFO: trying https://idm1.example.com/ipa/session/json ipa: INFO: [try 1]: Forwarding 'ping/1' to json server 'https://idm1.example.com/ipa/session/json' ipa: INFO: Request: { \"id\": 0, \"method\": \"ping/1\", \"params\": [ [], { \"version\": \"2.251\" } ] } ipa: INFO: Response: { \"error\": null, \"id\": 0, \"principal\": \"admin@EXAMPLE.COM\", \"result\": { \"summary\": \"IPA server version 4.10.2. API version 2.251\" }, \"version\": \"4.10.2\" } -------------------------------------------- IPA server version 4.10.2. API version 2.251 -------------------------------------------- If you look at the 'request' section, you can see the data that is sent. Each request has a method and params , where method is a command to be excuted and params is simply an array that contains positional arguments and a dictionary of options. If you take a look at say, group-show, you would see a different request. % ipa -vv group-show admins ipa: INFO: trying https://idm1.example.com/ipa/session/json ipa: INFO: [try 1]: Forwarding 'group_show/1' to json server 'https://idm1.example.com/ipa/session/json' ipa: INFO: Request: { \"id\": 0, \"method\": \"group_show/1\", \"params\": [ [ \"admins\" ], { \"version\": \"2.230\" } ] } ### Lots of output ### Let's say I wanted to perform that in a simple bash script that uses curl. I would perform a kinit and then run the script below to have it login for me via kerberos and do the work. #!/bin/bash ipaReplica=idm1.example.com cookieJar=my.cookie.jar # Login with Kerberos curl -v \\ -H referer:https://$ipaReplica/ipa \\ -c $cookieJar -b $cookieJar \\ --cacert /etc/ipa/ca.crt \\ --negotiate -u : \\ -X POST \\ https://$ipaReplica/ipa/session/login_kerberos # Send user_find method request curl -v \\ -H referer:https://$ipaReplica/ipa \\ -H \"Content-Type:application/json\" \\ -H \"Accept:applicaton/json\"\\ -c $cookieJar -b $cookieJar \\ --cacert /etc/ipa/ca.crt \\ -d '{\"method\":\"group_show/1\",\"params\":[[\"admins\"],{}],\"id\":0}' \\ -X POST \\ https://$ipaReplica/ipa/session/json Any of the commands ran via ipa can be reviewed with the -vv switch so you can see what kind of call it's making and how it's making it. Thus, making it easier to tie into your own scripts. On the Web UI, you can go to IPA Server -> API Browser to find more information on the specific API calls. Implement an IdP \u00b6 While not strictly a certification objective at this present time, there may be a chance it could be. FreeIPA 4.10+ have the ability to implement authentication to external identity providers. FreeIPA allows you to use RADIUS proxy authentication for example, but using this same method, an OAuth 2.0 authorization server could be used as well. When it comes to an IdP, you can instead configure IdP clients using ipa idp-add and use software like Keycloak or otherwise that supports OAuth 2.0 workflows. We recommend checking out the FreeIPA Workshop Unit 12 for more details.","title":"EX362 Exam Prep"},{"location":"training/ex362/#overview","text":"The video series goes over setting up FreeIPA in a lab/VM environment by following the objectives as outlined by Red Hat. The list of objectives can be found here .","title":"Overview"},{"location":"training/ex362/#exam-information","text":"The EX362 exam tests your knowledge in a real world format style test - Meaning just like any Red Hat exam, it is performance-based and you perform tasks as if you were on the job. You are evaluated on the tasks you perform and if they meet the objective criteria. The EX362 is related to FreeIPA or Red Hat Identity Management and counts toward the RHCA (Red Hat Certified Architect). To take the exam, you must have at least an RHCSA. If you are attempting to become a Red Hat Certified Architect, you must have an RHCE.","title":"Exam Information"},{"location":"training/ex362/#resources","text":"FreeIPA Red Hat Documentation Trust Anatomy/SSSD Troubleshooting Directory Server Tuning FreeIPA Workshop Curriculum Our Page","title":"Resources"},{"location":"training/ex362/#hardware-recommendations","text":"The minimum requirements for IdM are fairly low. 2GB of RAM, 1 core, and a 10GB disk. However, we believe that's too low, especially if we plan on scaling out. And during upgrades, you would need at least 4GB of RAM for the operations to be successful. Below are our minimum recommendations: 2 (virtual) CPU Core 4 GB of RAM 10GB+ disk or partition for /var/lib/dirsrv Per the Red Hat documentation, consider that with at least 10k users and 100 groups, you would need at least 3GB of RAM and 1GB swap. If you end up having 100k users and 50k groups, then 16GB of RAM and 4GB of swap is recommended. In fact, in larger deployments, it's more effective to increase RAM than disk, as most data is stored in cache. View the resources above in the previous section for directory server tuning information.","title":"Hardware Recommendations"},{"location":"training/ex362/#idm-server-installation-and-configuration","text":"","title":"IdM Server Installation and Configuration"},{"location":"training/ex362/#install-idm-in-a-scalable-fault-tolerant-environment","text":"Server Name IP Address idm1.example.com 192.168.15.2 idm2.example.com 192.168.15.3 !!! note:: IPA Servers should either have a DHCP reservation or a static address. In the event that you have either, DNS should always be pointing at 127.0.0.1, especially if your replica serves DNS. Both of our replicas serve DNS, so loopback is sufficient and recommended for our name server. In later versions of FreeIPA, there is support to force network manager to ensure resolv.conf is loopback without the need to set it by hand with nmcli. # Set a static address - It's important for your IdM servers # to have static addresses or a DHCP reservation. % nmcli con mod eth0 ipv4.address 192.168.15.2/24 % nmcli con mod eth0 ipv4.gateway 192.168.15.1 % nmcli con mod eth0 ipv4.method manual % nmcli con mod eth0 ipv4.dns-search example.com # You should set this if your replica serves DNS! If not, set it to # one or more of your IdM replicas that do. % nmcli con mod eth0 ipv4.dns 127.0.0.1 % nmcli con up eth0 # Examples of using ipa-server-install # RHEL 9 % yum install ipa-server ipa-server-dns ipa-client sssd sssd-ipa # Installation, interactive, does not setup specific components % ipa-server-install # Installation, mostly automatic (recommended) # This will setup DNS and the necessary pieces for an AD trust # Optionally, you can use the --netbios-name switch to set your forest netbios name % ipa-server-install --domain example.com --realm EXAMPLE.COM \\ --reverse-zone=15.168.192.in-addr.arpa. \\ --no-forwarders \\ --no-ntp \\ --setup-dns \\ --setup-adtrust \\ -p Passw0rd! \\ -a Passw0rd! # Configure the firewall for RHEL 7 % firewall-cmd --permanent --add-service={ntp,http,https,freeipa-ldap,freeipa-ldaps,kerberos,freeipa-replication,kpasswd,dns} # RHEL 8 % firewall-cmd --permanent --add-service={freeipa-4,ntp,dns} % kinit admin # We need to make sure that any A records get a corresponding PTR record, otherwise you're making them manually. % ipa dnsconfig-mod --allow-sync-ptr=True # Adding a replica % ipa-replica-install --setup-dns \\ --setup-ca \\ --no-forwarders # Adding a replica unattended without forwarders % ipa-client-install --realm EXAMPLE.COM % kinit admin % ipa hostgroup-add-member --hosts=ipa02.example.com ipaservers % ipa-replica-install --setup-dns \\ --setup-ca \\ --no-forwarders \\ --unattended","title":"Install IdM in a scalable, fault tolerant environment"},{"location":"training/ex362/#creating-users-groups-and-policies","text":"Users Login Name Type Group Role UID/GID John Smith jsmith Normal admins Auto Bob Rufus brufus Normal corp Auto Larry Dufus ldufus Normal helpdesk Auto Robert Cole rcole Staged Auto Thomas Snyder tsnyder Preserved Auto SysHost Management syshostmgt Normal Host Manager 10000 Groups Policy HelpDesk helpdesk corp enrollers Enrollment Administrator Roles Privilege Host Manager Host administrators Host group administrators Netgroups administrators Host enrollment Note Custom UID/GID It is possible to create the users with a custom uid/gid with the switches --uid and --gidnumber which you will see below. It is also possible to set random passwords with --random. See ipa user-add --help for more switches. Note Password Expiration When you make a user with the --password switch or use ipa passwd to set a password, it is automatically expired and must be changed on next login. If you want to avoid this from happening, you will need to set a random password via --password or --random, and then use kpasswd username to change it to the desired password. This does not make the account non-expiring. # Creating users with a password, create all the accounts from the table (except from syshost) % ipa user-add --first=\"John\" --last=\"Smith\" --password jsmith # Create the system account with a password of Sup3R$ecre7! and a UID of 10000 % ipa user-add --first=\"SysHost\" --last=\"Management\" --uid=10000 --gidnumber=10000 --password syshostmgt # Stage a user % ipa stageuser-add --first=\"Robert\" --last=\"Cole\" rcole # Preserve a user % ipa user-del tsynder --preserve # Create a regular (POSIX) group % ipa group-add corp # Create a member only group % ipa group-add --nonposix HelpDesk % ipa group-add --nonposix enrollers # Add the HelpDesk group to the helpdesk policy # Add the enrollers group to the Enrollment Administrator role % ipa role-add-member \"helpdesk\" --groups=HelpDesk % ipa role-add-member \"Enrollment Administrator\" --groups=enrollers # Create a role with privileges % ipa role-add \"Host Manager\" % ipa role-add-privilege \"Host Manager\" \\ --privileges=\"Host administrators\" \\ --privileges=\"Host group administrators\" \\ --privileges=\"Netgroups administrators\" \\ --privileges=\"Host enrollment\" # Add the syshostmgt user as a member of the role % ipa role-add-member \"Host Manager\" --users=\"syshostmgt\" # Set our user passwords to CentOS123!$ so that way we don't have to change them later % kpasswd jsmith # If we already set the password we want but we don't want it to expire without making a policy or prompt for a password change (NOT RECOMMENDED) % ldapmodify -x -w 'Passw0rd!' -D 'cn=Directory Manager' dn: uid=syshostmgt,cn=users,cn=accounts,dc=example,dc=com changetype: modify delete: krbLastPwdChange (Press CTRL+D)","title":"Creating Users, Groups, and Policies"},{"location":"training/ex362/#new-passwords-expired","text":"The common question we receive (and even the #freeipa IRC receive) is \"Why can't we just set the password to not be expired right away?\" See this page for information on why this is. You may also look at the pagure page and the Red Hat bugzilla related bug .","title":"New Passwords Expired"},{"location":"training/ex362/#implement-a-sso","text":"To setup a very, very simple SSO, you can setup a simple location that requires a login. % ipa-getkeytab -s idm1.example.com -p http/http.example.com -k /etc/httpd/conf/http.keytab % vi /etc/httpd/conf.d/location.conf AuthType Kerberos AuthName \"IPA Kerberos Auth\" # Keytab Krb5Keytab /etc/httpd/conf/http.keytab # Kerb settings KrbMethodNegotiate on KrbMethodK5Passwd on KrbServiceName HTTP KrbAuthRealms EXAMPLE.COM KrbSaveCredentials off Require valid-user ","title":"Implement a SSO"},{"location":"training/ex362/#idm-client-installation-and-configuration","text":"","title":"IdM Client Installation and Configuration"},{"location":"training/ex362/#install-and-configure-idm-clients","text":"Client Name IP Address client.example.com 192.168.15.10 nfs.example.com 192.168.15.11 utility.example.com 192.168.15.12 Note Depending on your architecture and setup, IdM clients should either be pointing directly at the IdM servers for DNS (at least two of them) or pointing at the DNS server in the environment that is delegating that domain to the IdM domain controllers. In our lab, our IdM servers are our only DNS servers, thus it makes sense that our clients should point to them. In that scenario, you would configure your DHCP server to use the IdM servers as the name servers and/or configure them in a static manner depending on your environment. # If your client is not pointing at the IdM DNS and you # don't have another DNS server that's performing delegation, # change your name servers. % nmcli con mod eth0 ipv4.dns 192.168.15.2 % nmcli con mod eth0 +ipv4.dns 192.168.15.3 % nmcli con mod eth0 ipv4.dns-search example.com # Optionally, if your clients don't have DHCP # reservations, set a static address. % nmcli con mod eth0 ipv4.address 192.168.15.10/24 % nmcli con mod eth0 ipv4.gateway 192.168.15.1 % nmcli con mod eth0 ipv4.method manual # It might be a good idea to set your hostname if you haven't already % hostnamectl set-hostname client.example.com % hostname client.example.com # Install the ipa-client packages % yum install ipa-client -y % ipa-client-install --realm EXAMPLE.COM --domain example.com . . . % id admin uid=686600000(admin) gid=686600000(admins) groups=686600000(admins)","title":"Install and configure IdM Clients"},{"location":"training/ex362/#configure-kerberized-services","text":"One of the things that you may end up doing, whether by hand or in an automated fashion, is creating kerberized services. In a previous section, we addressed creating an NFS service for both a server and a client for the purpose of automating home directory mounts on a client when a user logs in. So you already have the idea of what this entails. # Create kerberos service % ipa service-add HTTP/http.example.com Not only that, it's probably a good idea to actually get the keytab. % kinit admin % ipa-getkeytab -s idm1.example.com -p HTTP/http.example.com -k /etc/krb5.keytab For an example of automating keytab creation and retrieval, see the CentOS/FreeIPA page on this site.","title":"Configure Kerberized services"},{"location":"training/ex362/#idm-ha-configuration","text":"","title":"IdM HA Configuration"},{"location":"training/ex362/#configure-and-manage-a-certificate-authority","text":"By default FreeIPA stands up its own CA. And because of this, this allows you or your workplace to be able to issue certificates, that can be used in a wide variety of services, the most common or obvious one would be for Apache httpd. There's a couple of ways you can get a certificate signed by FreeIPA. One method is to generate your own CSR and request it to be signed by FreeIPA. Another way is you can do it all from one command, ipa-getcert , and optionally, either have the certificate in PEM format or an NSS database. We'll address these examples. # Creating an SSL certificate in the PEM format % ipa service-add HTTP/http.example.com % ipa-getcert request -f /etc/pki/tls/certs/http.pem -k /etc/pki/tls/private/http.key -K HTTP/http.example.com -D http.example.com New signing request \"20190902000318\" added. # Verify % ipa-getcert list Number of certificates and requests being tracked: 1. Request ID '20190902000318': status: MONITORING stuck: no key pair storage: type=FILE,location='/etc/pki/tls/private/http.key' certificate: type=FILE,location='/etc/pki/tls/certs/http.pem' CA: IPA issuer: CN=Certificate Authority,O=EXAMPLE.COM subject: CN=http.example.com,O=EXAMPLE.COM expires: 2021-09-02 00:03:19 UTC dns: http.example.com principal name: HTTP/http.example.com@EXAMPLE.COM key usage: digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment eku: id-kp-serverAuth,id-kp-clientAuth pre-save command: post-save command: track: yes auto-renew: yes # Create an SSL certificate in the NSS format % ipa-getcert request -d /etc/pki/tls/certs/nss -n 'Test' -K HTTP/http.example.com -D http.example.com New signing request \"20190902000756\" added. # Verify % ipa-getcert list . . . Request ID '20190902000756': status: MONITORING stuck: no key pair storage: type=NSSDB,location='/etc/pki/tls/certs/nss',nickname='Test',token='NSS Certificate DB' certificate: type=NSSDB,location='/etc/pki/tls/certs/nss',nickname='Test',token='NSS Certificate DB' CA: IPA issuer: CN=Certificate Authority,O=EXAMPLE.COM subject: CN=http.example.com,O=EXAMPLE.COM expires: 2021-09-02 00:07:57 UTC dns: http.example.com principal name: HTTP/http.example.com@EXAMPLE.COM key usage: digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment eku: id-kp-serverAuth,id-kp-clientAuth pre-save command: post-save command: track: yes auto-renew: yes By default, when a certificate request is performed (and succeeds to be signed by the IPA CA), it is typically tracked and auto-renewed by default. This is done by the certmonger service, which eliminates the need to have to renew anything by hand.","title":"Configure and manage a certificate authority"},{"location":"training/ex362/#create-secret-vaults","text":"When a domain supports the KRA role, it can hold password vaults or anything that's considered \"secret\". You can add the KRA role by simply running on each relevant domain controller: % ipa-kra-install (more to come)","title":"Create Secret Vaults"},{"location":"training/ex362/#idm-users-and-policies-management","text":"In FreeIPA, there are two sets of policies: Role Based Access Control (RBAC) which are the permissions, delegated or otherwise, that allow (or deny) access to various pieces of FreeIPA. This can be users that have the ability to reset passwords, modify groups, or perhaps they can issue keytabs. This was partially covered in a previous section. Host Based Access Control (HBAC) which are the permissions granted to a user or users to access systems on various (PAM) services, such as ssh or logging into a desktop system (eg, GDM).","title":"IdM Users and Policies Management"},{"location":"training/ex362/#configure-policies-and-user-access","text":"HBAC, or Host Based Access Controls, are permissions that grant user or users access to systems via any number of services. The services are PAM services. No doubt you have looked in /etc/pam.d before and have seen quite a few files or even modified them by hand at some point. % ls -l /etc/pam.d/ total 80 -rw-r--r--. 1 root root 272 May 11 2019 atd -rw-r--r--. 1 root root 232 Apr 15 15:28 config-util -rw-r--r--. 1 root root 328 Nov 8 2019 crond lrwxrwxrwx. 1 root root 32 Jan 14 2020 fingerprint-auth -> /etc/authselect/fingerprint-auth -rw-r--r--. 1 root root 70 Apr 24 06:35 ksu -rw-r--r--. 1 root root 715 Apr 24 05:38 login -rw-r--r--. 1 root root 154 Apr 15 15:28 other -rw-r--r--. 1 root root 168 Apr 6 20:08 passwd lrwxrwxrwx. 1 root root 29 Jan 14 2020 password-auth -> /etc/authselect/password-auth -rw-r--r--. 1 root root 155 Apr 8 22:00 polkit-1 lrwxrwxrwx. 1 root root 25 Jan 14 2020 postlogin -> /etc/authselect/postlogin -rw-r--r--. 1 root root 640 Apr 24 05:38 remote -rw-r--r--. 1 root root 143 Apr 24 05:38 runuser -rw-r--r--. 1 root root 138 Apr 24 05:38 runuser-l lrwxrwxrwx. 1 root root 30 Jan 14 2020 smartcard-auth -> /etc/authselect/smartcard-auth lrwxrwxrwx. 1 root root 25 Jun 15 10:18 smtp -> /etc/alternatives/mta-pam -rw-r--r--. 1 root root 76 Apr 6 20:11 smtp.postfix -rw-r--r--. 1 root root 727 Feb 4 2020 sshd -rw-r--r--. 1 root root 214 Apr 23 20:48 sssd-shadowutils -rw-r--r--. 1 root root 566 Apr 24 05:38 su -rw-r--r--. 1 root root 154 Apr 23 19:40 sudo -rw-r--r--. 1 root root 178 Apr 23 19:40 sudo-i -rw-r--r--. 1 root root 137 Apr 24 05:38 su-l lrwxrwxrwx. 1 root root 27 Jan 14 2020 system-auth -> /etc/authselect/system-auth -rw-r--r--. 1 root root 248 Jul 21 07:57 systemd-user -rw-r--r--. 1 root root 84 May 11 2019 vlock On a typical Red Hat system, the most common ones (such as su , sshd , sudo ) imports the system-auth file, so the login request is processed through those means. When defining HBAC rules, you either must allow \"all\" services or be selective. For example, if an HBAC rule allows \"sshd\", a user is allowed to ssh into a system, but wouldn't allow them to login locally, as that goes through login . If you want the user to be able to run the su and sudo commands, you would also need to allow those services. Otherwise, the user is denied, despite sudo policies being available. [label@mgt ~]$ sudo -i [sudo] password for label: sudo: PAM account management error: Permission denied In FreeIPA, there is typically a rule already predefined that allows everyone to access all systems and all services. This can be removed or disabled and this removes host access to everything immediately. This is typically recommended in most environments where there are security standards and procedures in place. # To disable % ipa hbacrule-disable allow_all # To delete instead % ipa hbacrule-del allow_all When performing a FreeIPA installation, it is possible to add --no-hbac-allow that will disable the allow_all rule. Below are some examples of adding access. # Allow all admins to access all systems % ipa hbacrule-add --hostcat=all --servicecat=all --desc='Allow all admins to access all systems' All_Admins % ipa hbacrule-add-user --groups=admins All_Admins # And then test... % ipa hbactest --rules=All_Admins --user=jsmith --host=client.example.com --service=login # Allow the corp users to access the client system only using the sshd pam services % ipa hbacrule-add --desc='Allow corp users to access client on ssh' corp_access % ipa hbacrule-add-user --groups=corp corp_access % ipa hbacrule-add-host --hosts=client.example.com corp_access % ipa hbacrule-add-service --hbacsvcs=sshd corp_access # And then test... % ipa hbactest --rules=corp_access --user=brufus --host=client.example.com --service=sshd","title":"Configure Policies and User Access"},{"location":"training/ex362/#configure-roamingautomounted-home-directories","text":"You will need to configure your NFS server to serve up roaming home directories for users and then your client should have automouting enabled. Note Client Kerberos Service It may not be required to create an nfs kerberos service for the client. The ipa-client-automount command may already handle this but it does not hurt to create one. In fact, the host keytab is used on the client side anyway. Creating an NFS client keytab may have been required back in the EL6 days. # IDM Steps % kinit admin % ipa service-add nfs/nfs.example.com % ipa service-add nfs/client.example.com # Setup the automounting locations % ipa automountmap-add default auto.home % ipa automountkey-add default --key \"/home\" --info auto.home auto.master % ipa automountkey-add default --key \"*\" --info \"-fstype=nfs4,rw,sec=krb5,soft nfs.example.com:/exports/home/&\" auto.home # NFS Server Steps % yum install nfs-utils -y % mkdir /exports/home % vi /etc/exports /exports/home *(rw,sec=sys:krb5:krb5i:krb5p) # Make the home directories for all users and move them to /export/home % mkhomedir_helper jsmith % mv /home/jsmith /export/home/ # Create the necessary keytabs % kinit admin % ipa-getkeytab -s idm1.example.com -p nfs/nfs.example.com -k /etc/krb5.keytab # Verify keytab % klist -ket /etc/krb5.keytab # Enable and start nfs % systemctl enable nfs-server --now # Open the necessary firewall ports % firewall-cmd --add-service=nfs --permanent % firewall-cmd --complete-reload # Client steps % kinit admin % ipa-getkeytab -s idm1.example.com -p nfs/client.example.com -k /etc/krb5.keytab % ipa-client-automount --location=default # Verify keytab % klist -ket /etc/krb5.keytab To test, login to the system via ssh or console and verify the home directory has mounted. /var/log/messages and secure will display errors in case of failure.","title":"Configure roaming/automounted home directories"},{"location":"training/ex362/#configure-idm-as-an-ldap-backend-for-external-services","text":"Most services and applications that authenticate users do typically have LDAP support. IdM can be used as an LDAP backend. You typically need only a few things to authenticate users from IdM to an application. Base DN, this always ends up being the top level of your domain: dc=example,dc=com - All accounts share this common base. Bind DN, this is a system account that binds to the directory to assist with searches and authentication Attribute mappings Groups, depending on the application Below is a table of common DN's you may specify in an application: DN's Path Filter (if applicable) Base DN dc=example,dc=com User DN cn=users,cn=accounts,dc=example,dc=com uid=... Group DN cn=groups,cn=accounts,dc=example,dc=com (objectClass=groupOfNames) Bind DN uid=account,cn=sysaccounts,cn=etc,dc=example,dc=com % ipa user-show admin --all | grep '^dn' dn: uid=admin,cn=users,cn=accounts,dc=example,dc=com Below is a table of common attributes that may be used to map user information in the application. Type Attribute Login Name uid First Name givenName Surname sn Email mail Groups memberOf Full Name cn Below are two ways to create a bind account (bind DN). The first way is the LDAP way. The second way is the ipa-ldap-updater. % kinit admin % ldapadd -Y GSSAPI . . . dn: uid=binder,cn=sysaccounts,cn=etc,dc=example,dc=com objectclass: account objectclass: simplesecurityobject uid: binder userPassword: password123 passwordExpirationTime: 20380119031407Z nsIdleTimeout: 0 # Press CTRL+d adding new entry \"uid=binder,cn=sysaccounts,cn=etc,dc=example,dc=com\" % kinit admin % cat << EOF > binder.update dn: uid=binder,cn=sysaccounts,cn=etc,dc=example,dc=com add:objectclass:account add:objectclass:simplesecurityobject add:uid:binder add:userPassword:password123 add:passwordExpirationTime:20380119031407Z add:nsIdleTimeout:0 EOF % ipa-ldap-updater binder.update When this account is created, you can then specify the full DN for that object into a bind DN field, along with it's password into an accompanying bind password field. If you'd like an example of setting up Ansible Tower (or AWX, the open source version of tower) against IdM, you can click here . Note Kerberos On some applications, it is possible to use kerberos authentication rather than a straight bind account. The general idea is the same when picking out the base dn, attributes, and the like. However, instead you would create an account with an accompanying LDAP/... service principal to do the authentication.","title":"Configure IdM as an LDAP backend for external services"},{"location":"training/ex362/#creating-trust-with-ad-domain","text":"","title":"Creating Trust with AD Domain"},{"location":"training/ex362/#create-trust-relationships-with-active-directory","text":"Note AD Setup We do not cover setting up an AD forest here. This is out of scope for this series. If you are using Server 2016 or higher and you are using \"core\", look up the commands: Install-WindowsFeature AD-domain-services Import-Module ADDSDeployment Install-ADDSForest Server Name IP Address ad.example.net 192.168.15.15 For our trust, the AD server will need to be configured to be the example.net domain with the hostname of ad.example.net. This way, we are not colliding in DNS and both AD and IdM should be able to communicate with each other as two separate forests. It is recommended to use Windows Server 2016 (with the same domain functional level) for this setup, as experience with that product is a recommended prerequisite for the exam. % yum install ipa-server-trust-ad -y % firewall-cmd --add-service=freeipa-trust --permanent success % firewall-cmd --reload success % ipa-adtrust-install . . . # This is the admin@REALM IPA account admin password: WARNING: The smb.conf already exists. Running ipa-adtrust-install will break your existing samba configuration. # Type 'yes' here Do you wish to continue? [no]: yes Do you want to enable support for trusted domains in Schema Compatibility plugin? This will allow clients older than SSSD 1.9 and non-Linux clients to work with trusted users. # You can press enter here to accept the default. If you have BSD, Solaris, Omnios, HP-UX, AIX, or RHEL 5 and older clients # you may want to enable this. Some apps may benefit from this also. Enable trusted domains support in slapi-nis? [no]: Enter the NetBIOS name for the IPA domain. Only up to 15 uppercase ASCII letters, digits and dashes are allowed. Example: EXAMPLE. # You can accept the default or put your own. NetBIOS domain name [IPA]: IPA0 WARNING: 4 existing users or groups do not have a SID identifier assigned. Installer can run a task to have ipa-sidgen Directory Server plugin generate the SID identifier for all these users. Please note, in case of a high number of users and groups, the operation might lead to high replication traffic and performance degradation. Refer to ipa-adtrust-install(1) man page for details. # You should always say yes. Do you want to run the ipa-sidgen task? [no]: yes . . . # This will complete and list ports to open and such. We did this earlier. Now that the AD trust components are prepped, depending on the setup, we'll need to do some DNS zone forwards. It is likely you have IPA and AD running their own DNS. Note : This may not be the case in a real world scenario. # We need to create a forward zone here for the example.net zone % ipa dnsforwardzone-add example.net --forwarder=192.168.15.15 --forward-policy=only Server will check DNS forwarder(s). This may take some time, please wait ... Zone name: example.net. Active zone: TRUE Zone forwarders: 192.168.15.15 Forward policy: only # We should probably create a few dns records... # Assuming the AD netbios name is EXAMPLEAD, use the syntax hostname.NETBIOS here % ipa dnsrecord-add example.com ad.EXAMPLEAD --a-ip-address=192.168.15.15 # Same idea here, but we're only doing the netbios name and saying the name server record is the AD server % ipa dnsrecord-add example.com EXAMPLEAD --ns-hostname=ad.EXAMPLEAD # We need to allow the zones to be transferable to the AD domain % ipa dnszone-mod example.com --allow-transfer=192.168.15.15 On the AD side, we need to create the IPA zone. It's absolutely required. C:\\Windows\\System32>dnscmd 127.0.0.1 /ZoneAdd example.com /Secondary 192.168.15.2 You should probably double check that the DNS records are returning on the IDM servers. % dig _ldap._tcp.example.com SRV ; <<>> DiG 9.9.4-RedHat-9.9.4-61.el7 <<>> SRV _ldap._tcp.example.com ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 14793 ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 2 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;_ldap._tcp.example.com. IN SRV ;; ANSWER SECTION: _ldap._tcp.example.com. 86400 IN SRV 0 100 389 idm1.example.com. _ldap._tcp.example.com. 86400 IN SRV 0 100 389 idm2.example.com. ;; AUTHORITY SECTION: example.com. 86400 IN NS idm1.example.com. example.com. 86400 IN NS idm2.example.com. ;; ADDITIONAL SECTION: idm1.example.com. 1200 IN A 192.168.15.2 idm2.example.com. 1200 IN A 192.168.15.3 # Same with the AD records % dig _ldap._tcp.example.net SRV ; <<>> DiG 9.9.4-RedHat-9.9.4-61.el7 <<>> SRV _ldap._tcp.example.net ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12195 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 4, ADDITIONAL: 9 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;_ldap._tcp.example.net. IN SRV ;; ANSWER SECTION: _ldap._tcp.example.net. 600 IN SRV 0 100 389 ad.example.net. . . . Now that they are returning, intiate the trust. % ipa trust-add --type=ad example.net --admin Administrator --password Active Directiron domain administrator's password: (type password here) ----------------------------------------------------- Added Active Directory trust for realm \"example.net\" ----------------------------------------------------- Realm name: example.net Domain NetBIOS name: EXAMPLEAD Domain Security Identifier: S-1-5-21-XXXXXXXXXX-YYYYYYYYY-ZZZZZZZZZZ Trust direction: Trusting forest Trust type: Active Directory domain Trust status: Established and verified # Check that an AD user is resolvable. You can do this with DOMAIN\\name or name@DOMAIN % id EXAMPLEAD\\\\administrator % id administrator@example.net","title":"Create trust relationships with Active Directory"},{"location":"training/ex362/#authenticate-users-with-an-active-directory-domain","text":"As we disabled the allow_all rule, let's create a set of groups first and then the HBAC rule. # Create the starting AD group % ipa group-add adusers # Create an external group. This is required for AD users. % ipa group-add --external adgroup_external # Add an AD user into the external group % ipa group-add-member --users=administrator@example.net adgroup_external # Make the external group a member of ad users % ipa group-add-member --groups=adgroup_external adusers As we've made an HBAC rule before, this should be simple. % ipa hbacrule-add --hostcat=all --servicecat=all --desc='ad users all access' adusers_access % ipa hbacrule-add-user --groups=adusers adusers_access % ipa hbactest --rules=adusers_access --user=administrator@example.net --host=client.example.com --service=sshd The test should pass without any issues. Note Group Types While this may not be required information while working on the exam, it's important to understand that there are different group types in AD and the behavior changes based on the group type. Groups in Active Directory have three types. These three types can actually change the behavior of how SSSD on the IPA domain controllers resolve them or if they'll even be resolvable at all. The three types are 'Domain Local', 'Global', and 'Universal'. If at all possible, avoid groups being 'Global'. Domain Local or Universal is recommended.","title":"Authenticate users with an Active Directory domain"},{"location":"training/ex362/#idm-maintenance","text":"","title":"IdM maintenance"},{"location":"training/ex362/#back-up-an-idm-infrastructure","text":"There are multiple ways you can backup IPA. Full backup: Default, shuts down IPA before performing a backup. This backs up with raw files. As such, it must be done offline. Data backup: Backs up a copy of the ldap data and the changelog (the IPA-REALM instance, DogTag, IPA backend). This can be done online. # Turns off IPA completely and perform a backup % ipa-backup # Backs up and gpg encrypts % ipa-backup --gpg --gpg-keyring=/root/keys To restore a backup, the ipa-restore command is available. % ipa-restore /var/lib/ipa/backup/","title":"Back up an IdM infrastructure"},{"location":"training/ex362/#perform-a-backup-without-interruption-of-services","text":"The backup command allows you to pass an online flag to ensure a backup taken doesn't down the IPA services. Note that not everything can be backed up online. # Backs up data only and doesn't take down IPA % ipa-backup --data --online # Backs up data only and gpg encrypts % ipa-backup --gpg --gpg-keyring=/root/keys --data --online","title":"Perform a backup without interruption of services"},{"location":"training/ex362/#value-add","text":"","title":"Value Add"},{"location":"training/ex362/#use-the-rest-api-to-query-idm","text":"When you invoke the ipa command, you are actually communicating with the API that runs on the IdM replicas. Operations done are sent via a POST with JSON data. The return data is also in JSON and translated to be readable in the terminal. Because it's JSON, custom scripts can be made with say perl or python that communicates with the API to send the calls, perhaps for specific tasks, jobs, or other operations that could be automated. You can also use curl to do this also if you so choose. The question becomes, \"well, how do I know the right data to send?\" You can issue the -vv switch to see the request being sent. % ipa -vv ping ipa: INFO: trying https://idm1.example.com/ipa/json ipa: INFO: [try 1]: Forwarding 'schema' to json server 'https://idm1.example.com/ipa/json' ipa: INFO: trying https://idm1.example.com/ipa/session/json ipa: INFO: [try 1]: Forwarding 'ping/1' to json server 'https://idm1.example.com/ipa/session/json' ipa: INFO: Request: { \"id\": 0, \"method\": \"ping/1\", \"params\": [ [], { \"version\": \"2.251\" } ] } ipa: INFO: Response: { \"error\": null, \"id\": 0, \"principal\": \"admin@EXAMPLE.COM\", \"result\": { \"summary\": \"IPA server version 4.10.2. API version 2.251\" }, \"version\": \"4.10.2\" } -------------------------------------------- IPA server version 4.10.2. API version 2.251 -------------------------------------------- If you look at the 'request' section, you can see the data that is sent. Each request has a method and params , where method is a command to be excuted and params is simply an array that contains positional arguments and a dictionary of options. If you take a look at say, group-show, you would see a different request. % ipa -vv group-show admins ipa: INFO: trying https://idm1.example.com/ipa/session/json ipa: INFO: [try 1]: Forwarding 'group_show/1' to json server 'https://idm1.example.com/ipa/session/json' ipa: INFO: Request: { \"id\": 0, \"method\": \"group_show/1\", \"params\": [ [ \"admins\" ], { \"version\": \"2.230\" } ] } ### Lots of output ### Let's say I wanted to perform that in a simple bash script that uses curl. I would perform a kinit and then run the script below to have it login for me via kerberos and do the work. #!/bin/bash ipaReplica=idm1.example.com cookieJar=my.cookie.jar # Login with Kerberos curl -v \\ -H referer:https://$ipaReplica/ipa \\ -c $cookieJar -b $cookieJar \\ --cacert /etc/ipa/ca.crt \\ --negotiate -u : \\ -X POST \\ https://$ipaReplica/ipa/session/login_kerberos # Send user_find method request curl -v \\ -H referer:https://$ipaReplica/ipa \\ -H \"Content-Type:application/json\" \\ -H \"Accept:applicaton/json\"\\ -c $cookieJar -b $cookieJar \\ --cacert /etc/ipa/ca.crt \\ -d '{\"method\":\"group_show/1\",\"params\":[[\"admins\"],{}],\"id\":0}' \\ -X POST \\ https://$ipaReplica/ipa/session/json Any of the commands ran via ipa can be reviewed with the -vv switch so you can see what kind of call it's making and how it's making it. Thus, making it easier to tie into your own scripts. On the Web UI, you can go to IPA Server -> API Browser to find more information on the specific API calls.","title":"Use the REST api to query IdM"},{"location":"training/ex362/#implement-an-idp","text":"While not strictly a certification objective at this present time, there may be a chance it could be. FreeIPA 4.10+ have the ability to implement authentication to external identity providers. FreeIPA allows you to use RADIUS proxy authentication for example, but using this same method, an OAuth 2.0 authorization server could be used as well. When it comes to an IdP, you can instead configure IdP clients using ipa idp-add and use software like Keycloak or otherwise that supports OAuth 2.0 workflows. We recommend checking out the FreeIPA Workshop Unit 12 for more details.","title":"Implement an IdP"}]} \ No newline at end of file diff --git a/search/worker.js b/search/worker.js new file mode 100644 index 00000000..8628dbce --- /dev/null +++ b/search/worker.js @@ -0,0 +1,133 @@ +var base_path = 'function' === typeof importScripts ? '.' : '/search/'; +var allowSearch = false; +var index; +var documents = {}; +var lang = ['en']; +var data; + +function getScript(script, callback) { + console.log('Loading script: ' + script); + $.getScript(base_path + script).done(function () { + callback(); + }).fail(function (jqxhr, settings, exception) { + console.log('Error: ' + exception); + }); +} + +function getScriptsInOrder(scripts, callback) { + if (scripts.length === 0) { + callback(); + return; + } + getScript(scripts[0], function() { + getScriptsInOrder(scripts.slice(1), callback); + }); +} + +function loadScripts(urls, callback) { + if( 'function' === typeof importScripts ) { + importScripts.apply(null, urls); + callback(); + } else { + getScriptsInOrder(urls, callback); + } +} + +function onJSONLoaded () { + data = JSON.parse(this.responseText); + var scriptsToLoad = ['lunr.js']; + if (data.config && data.config.lang && data.config.lang.length) { + lang = data.config.lang; + } + if (lang.length > 1 || lang[0] !== "en") { + scriptsToLoad.push('lunr.stemmer.support.js'); + if (lang.length > 1) { + scriptsToLoad.push('lunr.multi.js'); + } + if (lang.includes("ja") || lang.includes("jp")) { + scriptsToLoad.push('tinyseg.js'); + } + for (var i=0; i < lang.length; i++) { + if (lang[i] != 'en') { + scriptsToLoad.push(['lunr', lang[i], 'js'].join('.')); + } + } + } + loadScripts(scriptsToLoad, onScriptsLoaded); +} + +function onScriptsLoaded () { + console.log('All search scripts loaded, building Lunr index...'); + if (data.config && data.config.separator && data.config.separator.length) { + lunr.tokenizer.separator = new RegExp(data.config.separator); + } + + if (data.index) { + index = lunr.Index.load(data.index); + data.docs.forEach(function (doc) { + documents[doc.location] = doc; + }); + console.log('Lunr pre-built index loaded, search ready'); + } else { + index = lunr(function () { + if (lang.length === 1 && lang[0] !== "en" && lunr[lang[0]]) { + this.use(lunr[lang[0]]); + } else if (lang.length > 1) { + this.use(lunr.multiLanguage.apply(null, lang)); // spread operator not supported in all browsers: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator#Browser_compatibility + } + this.field('title'); + this.field('text'); + this.ref('location'); + + for (var i=0; i < data.docs.length; i++) { + var doc = data.docs[i]; + this.add(doc); + documents[doc.location] = doc; + } + }); + console.log('Lunr index built, search ready'); + } + allowSearch = true; + postMessage({config: data.config}); + postMessage({allowSearch: allowSearch}); +} + +function init () { + var oReq = new XMLHttpRequest(); + oReq.addEventListener("load", onJSONLoaded); + var index_path = base_path + '/search_index.json'; + if( 'function' === typeof importScripts ){ + index_path = 'search_index.json'; + } + oReq.open("GET", index_path); + oReq.send(); +} + +function search (query) { + if (!allowSearch) { + console.error('Assets for search still loading'); + return; + } + + var resultDocuments = []; + var results = index.search(query); + for (var i=0; i < results.length; i++){ + var result = results[i]; + doc = documents[result.ref]; + doc.summary = doc.text.substring(0, 200); + resultDocuments.push(doc); + } + return resultDocuments; +} + +if( 'function' === typeof importScripts ) { + onmessage = function (e) { + if (e.data.init) { + init(); + } else if (e.data.query) { + postMessage({ results: search(e.data.query) }); + } else { + console.error("Worker - Unrecognized message: " + e); + } + }; +} diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 00000000..d8f70603 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,58 @@ + + + + https://linuxguideandhints.com/ + 2024-01-07 + daily + + + https://linuxguideandhints.com/archive/ + 2024-01-07 + daily + + + https://linuxguideandhints.com/el/ + 2024-01-07 + daily + + + https://linuxguideandhints.com/el/builds/ + 2024-01-07 + daily + + + https://linuxguideandhints.com/el/freeipa/ + 2024-01-07 + daily + + + https://linuxguideandhints.com/el/nat/ + 2024-01-07 + daily + + + https://linuxguideandhints.com/el/openldap/ + 2024-01-07 + daily + + + https://linuxguideandhints.com/el/pxeboot/ + 2024-01-07 + daily + + + https://linuxguideandhints.com/el/sysadmin/ + 2024-01-07 + daily + + + https://linuxguideandhints.com/el/unbound/ + 2024-01-07 + daily + + + https://linuxguideandhints.com/training/ex362/ + 2024-01-07 + daily + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 00000000..4ced9731 Binary files /dev/null and b/sitemap.xml.gz differ diff --git a/training/ex362/index.html b/training/ex362/index.html new file mode 100644 index 00000000..66dc4810 --- /dev/null +++ b/training/ex362/index.html @@ -0,0 +1,1202 @@ + + + + + + + + + + + EX362 Exam Prep - Linux Guide and Hints + + + + + + + + + + + + + + + +
+
+ +
+ +

This page contains the necessary resources to help you prepare for the Red Hat Certified Specialist in Identity Management exam, EX362. This follows the youtube playlist as much as possible with various examples and ideas. Soon to come, you will also find our own example practice exam for you to try your hand at to test your knowledge.

+

The list of objectives can be found here. Note that the exam objectives can change at any time. It is the responsibility of the reader to always review the objectives prior to studying and taking the exam to ensure success.

+
+

Note

+

Affiliation and Exam Information

+

Please note that we are not affiliated with Red Hat. The materials and examples used are our own and do not reflect the training programs provided by Red Hat and are educational only. We do not disclose any of the tasks, questions, or material on the exam as it would violate the NDA. Any questions sent to us about anything directly related to the exam will not be answered. We also do not provide any one-on-one tutoring or online teaching courses.

+

If exam objectives have changed to where the videos and this material are missing information, we can add on at any time upon request. If exam objectives have not changed but operational tasks have, we will note them as we find them. If there are things about FreeIPA that you'd like to see in the videos that may fit into objective, we can add it also upon request. However, it is likely those extra things would be better suited in the separate FreeIPA section on this site.

+
+

Overview

+

The video series goes over setting up FreeIPA in a lab/VM environment by following the objectives as outlined by Red Hat. The list of objectives can be found here.

+

Exam Information

+

The EX362 exam tests your knowledge in a real world format style test - Meaning just like any Red Hat exam, it is performance-based and you perform tasks as if you were on the job. You are evaluated on the tasks you perform and if they meet the objective criteria. The EX362 is related to FreeIPA or Red Hat Identity Management and counts toward the RHCA (Red Hat Certified Architect).

+

To take the exam, you must have at least an RHCSA. If you are attempting to become a Red Hat Certified Architect, you must have an RHCE.

+

Resources

+ +

Hardware Recommendations

+

The minimum requirements for IdM are fairly low. 2GB of RAM, 1 core, and a 10GB disk. However, we believe that's too low, especially if we plan on scaling out. And during upgrades, you would need at least 4GB of RAM for the operations to be successful. Below are our minimum recommendations:

+
    +
  • 2 (virtual) CPU Core
  • +
  • 4 GB of RAM
  • +
  • 10GB+ disk or partition for /var/lib/dirsrv
  • +
+

Per the Red Hat documentation, consider that with at least 10k users and 100 groups, you would need at least 3GB of RAM and 1GB swap. If you end up having 100k users and 50k groups, then 16GB of RAM and 4GB of swap is recommended. In fact, in larger deployments, it's more effective to increase RAM than disk, as most data is stored in cache.

+

View the resources above in the previous section for directory server tuning information.

+

IdM Server Installation and Configuration

+

Install IdM in a scalable, fault tolerant environment

+ + + + + + + + + + + + + + + + + +
Server NameIP Address
idm1.example.com192.168.15.2
idm2.example.com192.168.15.3
+

!!! note:: + IPA Servers should either have a DHCP reservation or a static address. In the event that you have either, DNS should always be pointing at 127.0.0.1, especially if your replica serves DNS. Both of our replicas serve DNS, so loopback is sufficient and recommended for our name server.

+
In later versions of FreeIPA, there is support to force network manager to ensure resolv.conf is loopback without the need to set it by hand with nmcli.
+
+
# Set a static address - It's important for your IdM servers
+# to have static addresses or a DHCP reservation.
+% nmcli con mod eth0 ipv4.address 192.168.15.2/24
+% nmcli con mod eth0 ipv4.gateway 192.168.15.1
+% nmcli con mod eth0 ipv4.method manual
+% nmcli con mod eth0 ipv4.dns-search example.com
+
+# You should set this if your replica serves DNS! If not, set it to
+# one or more of your IdM replicas that do.
+% nmcli con mod eth0 ipv4.dns 127.0.0.1
+% nmcli con up eth0
+
+
# Examples of using ipa-server-install
+# RHEL 9
+% yum install ipa-server ipa-server-dns ipa-client sssd sssd-ipa
+# Installation, interactive, does not setup specific components
+% ipa-server-install
+
+# Installation, mostly automatic (recommended)
+# This will setup DNS and the necessary pieces for an AD trust
+# Optionally, you can use the --netbios-name switch to set your forest netbios name
+% ipa-server-install --domain example.com --realm EXAMPLE.COM \
+    --reverse-zone=15.168.192.in-addr.arpa. \
+    --no-forwarders \
+    --no-ntp \
+    --setup-dns \
+    --setup-adtrust \
+    -p Passw0rd! \
+    -a Passw0rd!
+
+
# Configure the firewall for RHEL 7
+% firewall-cmd --permanent --add-service={ntp,http,https,freeipa-ldap,freeipa-ldaps,kerberos,freeipa-replication,kpasswd,dns}
+# RHEL 8
+% firewall-cmd --permanent --add-service={freeipa-4,ntp,dns}
+
+
% kinit admin
+# We need to make sure that any A records get a corresponding PTR record, otherwise you're making them manually.
+% ipa dnsconfig-mod --allow-sync-ptr=True
+
+
# Adding a replica
+% ipa-replica-install --setup-dns \
+    --setup-ca \
+    --no-forwarders
+
+# Adding a replica unattended without forwarders
+% ipa-client-install --realm EXAMPLE.COM
+% kinit admin
+% ipa hostgroup-add-member --hosts=ipa02.example.com ipaservers
+% ipa-replica-install --setup-dns \
+    --setup-ca \
+    --no-forwarders \
+    --unattended
+
+

Creating Users, Groups, and Policies

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
UsersLogin NameTypeGroupRoleUID/GID
John SmithjsmithNormaladminsAuto
Bob RufusbrufusNormalcorpAuto
Larry DufusldufusNormalhelpdeskAuto
Robert ColercoleStagedAuto
Thomas SnydertsnyderPreservedAuto
SysHost ManagementsyshostmgtNormalHost Manager10000
+ + + + + + + + + + + + + + + + + + + + + +
GroupsPolicy
HelpDeskhelpdesk
corp
enrollersEnrollment Administrator
+ + + + + + + + + + + + + + + + + + + + + + + + + +
RolesPrivilege
Host ManagerHost administrators
Host group administrators
Netgroups administrators
Host enrollment
+
+

Note

+

Custom UID/GID

+

It is possible to create the users with a custom uid/gid with the switches --uid and --gidnumber which you will see below. It is also possible to set random passwords with --random.

+

See ipa user-add --help for more switches.

+
+
+

Note

+

Password Expiration

+

When you make a user with the --password switch or use ipa passwd to set a password, it is automatically expired and must be changed on next login. If you want to avoid this from happening, you will need to set a random password via --password or --random, and then use kpasswd username to change it to the desired password. This does not make the account non-expiring.

+
+
# Creating users with a password, create all the accounts from the table (except from syshost)
+% ipa user-add --first="John" --last="Smith" --password jsmith
+
+# Create the system account with a password of Sup3R$ecre7! and a UID of 10000
+% ipa user-add --first="SysHost" --last="Management" --uid=10000 --gidnumber=10000 --password syshostmgt
+
+# Stage a user
+% ipa stageuser-add --first="Robert" --last="Cole" rcole
+
+# Preserve a user
+% ipa user-del tsynder --preserve
+
+# Create a regular (POSIX) group
+% ipa group-add corp
+
+# Create a member only group
+% ipa group-add --nonposix HelpDesk
+% ipa group-add --nonposix enrollers
+
+# Add the HelpDesk group to the helpdesk policy
+# Add the enrollers group to the Enrollment Administrator role
+% ipa role-add-member "helpdesk" --groups=HelpDesk
+% ipa role-add-member "Enrollment Administrator" --groups=enrollers
+
+# Create a role with privileges
+% ipa role-add "Host Manager"
+% ipa role-add-privilege "Host Manager" \
+    --privileges="Host administrators" \
+    --privileges="Host group administrators" \
+    --privileges="Netgroups administrators" \
+    --privileges="Host enrollment"
+
+# Add the syshostmgt user as a member of the role
+% ipa role-add-member "Host Manager" --users="syshostmgt"
+
+# Set our user passwords to CentOS123!$ so that way we don't have to change them later
+% kpasswd jsmith
+
+# If we already set the password we want but we don't want it to expire without making a policy or prompt for a password change (NOT RECOMMENDED)
+% ldapmodify -x -w 'Passw0rd!' -D 'cn=Directory Manager'
+dn: uid=syshostmgt,cn=users,cn=accounts,dc=example,dc=com
+changetype: modify
+delete: krbLastPwdChange
+
+(Press CTRL+D)
+
+

New Passwords Expired

+

The common question we receive (and even the #freeipa IRC receive) is "Why can't we just set the password to not be expired right away?" See this page for information on why this is. You may also look at the pagure page and the Red Hat bugzilla related bug.

+

Implement a SSO

+

To setup a very, very simple SSO, you can setup a simple location that requires a login.

+
% ipa-getkeytab -s idm1.example.com -p http/http.example.com -k /etc/httpd/conf/http.keytab
+% vi /etc/httpd/conf.d/location.conf
+<Location "/">
+  AuthType Kerberos
+  AuthName "IPA Kerberos Auth"
+  # Keytab
+  Krb5Keytab /etc/httpd/conf/http.keytab
+  # Kerb settings
+  KrbMethodNegotiate on
+  KrbMethodK5Passwd on
+  KrbServiceName HTTP
+  KrbAuthRealms EXAMPLE.COM
+  KrbSaveCredentials off
+  Require valid-user
+</Location>
+
+

IdM Client Installation and Configuration

+

Install and configure IdM Clients

+ + + + + + + + + + + + + + + + + + + + + +
Client NameIP Address
client.example.com192.168.15.10
nfs.example.com192.168.15.11
utility.example.com192.168.15.12
+
+

Note

+

Depending on your architecture and setup, IdM clients should either be pointing directly at the IdM servers for DNS (at least two of them) or pointing at the DNS server in the environment that is delegating that domain to the IdM domain controllers.

+

In our lab, our IdM servers are our only DNS servers, thus it makes sense that our clients should point to them. In that scenario, you would configure your DHCP server to use the IdM servers as the name servers and/or configure them in a static manner depending on your environment.

+
+
# If your client is not pointing at the IdM DNS and you
+# don't have another DNS server that's performing delegation,
+# change your name servers.
+% nmcli con mod eth0 ipv4.dns 192.168.15.2
+% nmcli con mod eth0 +ipv4.dns 192.168.15.3
+% nmcli con mod eth0 ipv4.dns-search example.com
+
+# Optionally, if your clients don't have DHCP 
+# reservations, set a static address.
+% nmcli con mod eth0 ipv4.address 192.168.15.10/24
+% nmcli con mod eth0 ipv4.gateway 192.168.15.1
+% nmcli con mod eth0 ipv4.method manual
+
+# It might be a good idea to set your hostname if you haven't already
+% hostnamectl set-hostname client.example.com
+% hostname client.example.com
+
+# Install the ipa-client packages
+% yum install ipa-client -y
+% ipa-client-install --realm EXAMPLE.COM --domain example.com
+. . .
+% id admin
+uid=686600000(admin) gid=686600000(admins) groups=686600000(admins)
+
+

Configure Kerberized services

+

One of the things that you may end up doing, whether by hand or in an automated fashion, is creating kerberized services. In a previous section, we addressed creating an NFS service for both a server and a client for the purpose of automating home directory mounts on a client when a user logs in. So you already have the idea of what this entails.

+
# Create kerberos service
+% ipa service-add HTTP/http.example.com
+
+

Not only that, it's probably a good idea to actually get the keytab.

+
% kinit admin
+% ipa-getkeytab -s idm1.example.com -p HTTP/http.example.com -k /etc/krb5.keytab
+
+

For an example of automating keytab creation and retrieval, see the CentOS/FreeIPA page on this site.

+

IdM HA Configuration

+

Configure and manage a certificate authority

+

By default FreeIPA stands up its own CA. And because of this, this allows you or your workplace to be able to issue certificates, that can be used in a wide variety of services, the most common or obvious one would be for Apache httpd.

+

There's a couple of ways you can get a certificate signed by FreeIPA. One method is to generate your own CSR and request it to be signed by FreeIPA. Another way is you can do it all from one command, ipa-getcert, and optionally, either have the certificate in PEM format or an NSS database. We'll address these examples.

+
# Creating an SSL certificate in the PEM format
+% ipa service-add HTTP/http.example.com
+% ipa-getcert request -f /etc/pki/tls/certs/http.pem -k /etc/pki/tls/private/http.key -K HTTP/http.example.com -D http.example.com
+New signing request "20190902000318" added.
+# Verify
+% ipa-getcert list
+Number of certificates and requests being tracked: 1.
+Request ID '20190902000318':
+        status: MONITORING
+        stuck: no
+        key pair storage: type=FILE,location='/etc/pki/tls/private/http.key'
+        certificate: type=FILE,location='/etc/pki/tls/certs/http.pem'
+        CA: IPA
+        issuer: CN=Certificate Authority,O=EXAMPLE.COM
+        subject: CN=http.example.com,O=EXAMPLE.COM
+        expires: 2021-09-02 00:03:19 UTC
+        dns: http.example.com
+        principal name: HTTP/http.example.com@EXAMPLE.COM
+        key usage: digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment
+        eku: id-kp-serverAuth,id-kp-clientAuth
+        pre-save command:
+        post-save command:
+        track: yes
+        auto-renew: yes
+
+# Create an SSL certificate in the NSS format
+% ipa-getcert request -d /etc/pki/tls/certs/nss -n 'Test' -K HTTP/http.example.com -D http.example.com
+New signing request "20190902000756" added.
+# Verify
+% ipa-getcert list
+. . .
+Request ID '20190902000756':
+        status: MONITORING
+        stuck: no
+        key pair storage: type=NSSDB,location='/etc/pki/tls/certs/nss',nickname='Test',token='NSS Certificate DB'
+        certificate: type=NSSDB,location='/etc/pki/tls/certs/nss',nickname='Test',token='NSS Certificate DB'
+        CA: IPA
+        issuer: CN=Certificate Authority,O=EXAMPLE.COM
+        subject: CN=http.example.com,O=EXAMPLE.COM
+        expires: 2021-09-02 00:07:57 UTC
+        dns: http.example.com
+        principal name: HTTP/http.example.com@EXAMPLE.COM
+        key usage: digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment
+        eku: id-kp-serverAuth,id-kp-clientAuth
+        pre-save command:
+        post-save command:
+        track: yes
+        auto-renew: yes
+
+

By default, when a certificate request is performed (and succeeds to be signed by the IPA CA), it is typically tracked and auto-renewed by default. This is done by the certmonger service, which eliminates the need to have to renew anything by hand.

+

Create Secret Vaults

+

When a domain supports the KRA role, it can hold password vaults or anything that's considered "secret". You can add the KRA role by simply running on each relevant domain controller:

+
% ipa-kra-install
+
+

(more to come)

+

IdM Users and Policies Management

+

In FreeIPA, there are two sets of policies:

+
    +
  • Role Based Access Control (RBAC) which are the permissions, delegated or otherwise, that allow (or deny) access to various pieces of FreeIPA. This can be users that have the ability to reset passwords, modify groups, or perhaps they can issue keytabs. This was partially covered in a previous section.
  • +
  • Host Based Access Control (HBAC) which are the permissions granted to a user or users to access systems on various (PAM) services, such as ssh or logging into a desktop system (eg, GDM).
  • +
+

Configure Policies and User Access

+

HBAC, or Host Based Access Controls, are permissions that grant user or users access to systems via any number of services. The services are PAM services. No doubt you have looked in /etc/pam.d before and have seen quite a few files or even modified them by hand at some point.

+
% ls -l /etc/pam.d/
+total 80
+-rw-r--r--. 1 root root 272 May 11  2019 atd
+-rw-r--r--. 1 root root 232 Apr 15 15:28 config-util
+-rw-r--r--. 1 root root 328 Nov  8  2019 crond
+lrwxrwxrwx. 1 root root  32 Jan 14  2020 fingerprint-auth -> /etc/authselect/fingerprint-auth
+-rw-r--r--. 1 root root  70 Apr 24 06:35 ksu
+-rw-r--r--. 1 root root 715 Apr 24 05:38 login
+-rw-r--r--. 1 root root 154 Apr 15 15:28 other
+-rw-r--r--. 1 root root 168 Apr  6 20:08 passwd
+lrwxrwxrwx. 1 root root  29 Jan 14  2020 password-auth -> /etc/authselect/password-auth
+-rw-r--r--. 1 root root 155 Apr  8 22:00 polkit-1
+lrwxrwxrwx. 1 root root  25 Jan 14  2020 postlogin -> /etc/authselect/postlogin
+-rw-r--r--. 1 root root 640 Apr 24 05:38 remote
+-rw-r--r--. 1 root root 143 Apr 24 05:38 runuser
+-rw-r--r--. 1 root root 138 Apr 24 05:38 runuser-l
+lrwxrwxrwx. 1 root root  30 Jan 14  2020 smartcard-auth -> /etc/authselect/smartcard-auth
+lrwxrwxrwx. 1 root root  25 Jun 15 10:18 smtp -> /etc/alternatives/mta-pam
+-rw-r--r--. 1 root root  76 Apr  6 20:11 smtp.postfix
+-rw-r--r--. 1 root root 727 Feb  4  2020 sshd
+-rw-r--r--. 1 root root 214 Apr 23 20:48 sssd-shadowutils
+-rw-r--r--. 1 root root 566 Apr 24 05:38 su
+-rw-r--r--. 1 root root 154 Apr 23 19:40 sudo
+-rw-r--r--. 1 root root 178 Apr 23 19:40 sudo-i
+-rw-r--r--. 1 root root 137 Apr 24 05:38 su-l
+lrwxrwxrwx. 1 root root  27 Jan 14  2020 system-auth -> /etc/authselect/system-auth
+-rw-r--r--. 1 root root 248 Jul 21 07:57 systemd-user
+-rw-r--r--. 1 root root  84 May 11  2019 vlock
+
+

On a typical Red Hat system, the most common ones (such as su, sshd, sudo) imports the system-auth file, so the login request is processed through those means. When defining HBAC rules, you either must allow "all" services or be selective. For example, if an HBAC rule allows "sshd", a user is allowed to ssh into a system, but wouldn't allow them to login locally, as that goes through login. If you want the user to be able to run the su and sudo commands, you would also need to allow those services. Otherwise, the user is denied, despite sudo policies being available.

+
[label@mgt ~]$ sudo -i
+[sudo] password for label:
+sudo: PAM account management error: Permission denied
+
+

In FreeIPA, there is typically a rule already predefined that allows everyone to access all systems and all services. This can be removed or disabled and this removes host access to everything immediately. This is typically recommended in most environments where there are security standards and procedures in place.

+
# To disable
+% ipa hbacrule-disable allow_all
+# To delete instead
+% ipa hbacrule-del allow_all
+
+

When performing a FreeIPA installation, it is possible to add --no-hbac-allow that will disable the allow_all rule.

+

Below are some examples of adding access.

+
# Allow all admins to access all systems
+% ipa hbacrule-add --hostcat=all --servicecat=all --desc='Allow all admins to access all systems' All_Admins
+% ipa hbacrule-add-user --groups=admins All_Admins
+
+# And then test...
+% ipa hbactest --rules=All_Admins --user=jsmith --host=client.example.com --service=login
+
+
# Allow the corp users to access the client system only using the sshd pam services
+% ipa hbacrule-add --desc='Allow corp users to access client on ssh' corp_access
+% ipa hbacrule-add-user --groups=corp corp_access
+% ipa hbacrule-add-host --hosts=client.example.com corp_access
+% ipa hbacrule-add-service --hbacsvcs=sshd corp_access
+
+# And then test...
+% ipa hbactest --rules=corp_access --user=brufus --host=client.example.com --service=sshd
+
+

Configure roaming/automounted home directories

+

You will need to configure your NFS server to serve up roaming home directories for users and then your client should have automouting enabled.

+
+

Note

+

Client Kerberos Service

+

It may not be required to create an nfs kerberos service for the client. The ipa-client-automount command may already handle this but it does not hurt to create one. In fact, the host keytab is used on the client side anyway. Creating an NFS client keytab may have been required back in the EL6 days.

+
+
# IDM Steps
+% kinit admin
+% ipa service-add nfs/nfs.example.com
+% ipa service-add nfs/client.example.com
+
+# Setup the automounting locations
+% ipa automountmap-add default auto.home
+% ipa automountkey-add default --key "/home" --info auto.home auto.master
+% ipa automountkey-add default --key "*" --info "-fstype=nfs4,rw,sec=krb5,soft nfs.example.com:/exports/home/&" auto.home
+
+# NFS Server Steps
+% yum install nfs-utils -y
+% mkdir /exports/home
+% vi /etc/exports
+/exports/home *(rw,sec=sys:krb5:krb5i:krb5p)
+
+# Make the home directories for all users and move them to /export/home
+% mkhomedir_helper jsmith
+% mv /home/jsmith /export/home/
+
+# Create the necessary keytabs
+% kinit admin
+% ipa-getkeytab -s idm1.example.com -p nfs/nfs.example.com -k /etc/krb5.keytab
+
+# Verify keytab
+% klist -ket /etc/krb5.keytab
+
+# Enable and start nfs
+% systemctl enable nfs-server --now
+
+# Open the necessary firewall ports
+% firewall-cmd --add-service=nfs --permanent
+% firewall-cmd --complete-reload
+
+# Client steps
+% kinit admin
+% ipa-getkeytab -s idm1.example.com -p nfs/client.example.com -k /etc/krb5.keytab
+% ipa-client-automount --location=default
+
+# Verify keytab
+% klist -ket /etc/krb5.keytab
+
+

To test, login to the system via ssh or console and verify the home directory has mounted. /var/log/messages and secure will display errors in case of failure.

+

Configure IdM as an LDAP backend for external services

+

Most services and applications that authenticate users do typically have LDAP support. IdM can be used as an LDAP backend. You typically need only a few things to authenticate users from IdM to an application.

+
    +
  • Base DN, this always ends up being the top level of your domain: dc=example,dc=com - All accounts share this common base.
  • +
  • Bind DN, this is a system account that binds to the directory to assist with searches and authentication
  • +
  • Attribute mappings
  • +
  • Groups, depending on the application
  • +
+

Below is a table of common DN's you may specify in an application:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DN'sPathFilter (if applicable)
Base DNdc=example,dc=com
User DNcn=users,cn=accounts,dc=example,dc=comuid=...
Group DNcn=groups,cn=accounts,dc=example,dc=com(objectClass=groupOfNames)
Bind DNuid=account,cn=sysaccounts,cn=etc,dc=example,dc=com
+
% ipa user-show admin --all | grep '^dn'
+  dn: uid=admin,cn=users,cn=accounts,dc=example,dc=com
+
+

Below is a table of common attributes that may be used to map user information in the application.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeAttribute
Login Nameuid
First NamegivenName
Surnamesn
Emailmail
GroupsmemberOf
Full Namecn
+

Below are two ways to create a bind account (bind DN). The first way is the LDAP way. The second way is the ipa-ldap-updater.

+
% kinit admin
+% ldapadd -Y GSSAPI
+. . .
+dn: uid=binder,cn=sysaccounts,cn=etc,dc=example,dc=com
+objectclass: account
+objectclass: simplesecurityobject
+uid: binder
+userPassword: password123
+passwordExpirationTime: 20380119031407Z
+nsIdleTimeout: 0
+# Press CTRL+d
+adding new entry "uid=binder,cn=sysaccounts,cn=etc,dc=example,dc=com"
+
+
% kinit admin
+% cat << EOF > binder.update
+dn: uid=binder,cn=sysaccounts,cn=etc,dc=example,dc=com
+add:objectclass:account
+add:objectclass:simplesecurityobject
+add:uid:binder
+add:userPassword:password123
+add:passwordExpirationTime:20380119031407Z
+add:nsIdleTimeout:0
+EOF
+% ipa-ldap-updater binder.update
+
+

When this account is created, you can then specify the full DN for that object into a bind DN field, along with it's password into an accompanying bind password field.

+

If you'd like an example of setting up Ansible Tower (or AWX, the open source version of tower) against IdM, you can click here.

+
+

Note

+

Kerberos

+

On some applications, it is possible to use kerberos authentication rather than a straight bind account. The general idea is the same when picking out the base dn, attributes, and the like. However, instead you would create an account with an accompanying LDAP/... service principal to do the authentication.

+
+

Creating Trust with AD Domain

+

Create trust relationships with Active Directory

+
+

Note

+

AD Setup

+

We do not cover setting up an AD forest here. This is out of scope for this series. If you are using Server 2016 or higher and you are using "core", look up the commands:

+

Install-WindowsFeature AD-domain-services +Import-Module ADDSDeployment +Install-ADDSForest

+
+ + + + + + + + + + + + + +
Server NameIP Address
ad.example.net192.168.15.15
+

For our trust, the AD server will need to be configured to be the example.net domain with the hostname of ad.example.net. This way, we are not colliding in DNS and both AD and IdM should be able to communicate with each other as two separate forests. It is recommended to use Windows Server 2016 (with the same domain functional level) for this setup, as experience with that product is a recommended prerequisite for the exam.

+
% yum install ipa-server-trust-ad -y
+% firewall-cmd --add-service=freeipa-trust --permanent
+success
+% firewall-cmd --reload
+success
+% ipa-adtrust-install
+. . .
+# This is the admin@REALM IPA account
+admin password:
+
+WARNING: The smb.conf already exists. Running ipa-adtrust-install will break your existing samba configuration.
+
+# Type 'yes' here
+Do you wish to continue? [no]: yes
+
+Do you want to enable support for trusted domains in Schema Compatibility plugin?
+This will allow clients older than SSSD 1.9 and non-Linux clients to work with trusted users.
+
+# You can press enter here to accept the default. If you have BSD, Solaris, Omnios, HP-UX, AIX, or RHEL 5 and older clients
+# you may want to enable this. Some apps may benefit from this also.
+Enable trusted domains support in slapi-nis? [no]:
+
+Enter the NetBIOS name for the IPA domain.
+Only up to 15 uppercase ASCII letters, digits and dashes are allowed.
+Example: EXAMPLE.
+
+# You can accept the default or put your own.
+NetBIOS domain name [IPA]: IPA0
+
+WARNING: 4 existing users or groups do not have a SID identifier assigned.
+Installer can run a task to have ipa-sidgen Directory Server plugin generate
+the SID identifier for all these users. Please note, in case of a high
+number of users and groups, the operation might lead to high replication
+traffic and performance degradation. Refer to ipa-adtrust-install(1) man page
+for details.
+
+# You should always say yes.
+Do you want to run the ipa-sidgen task? [no]: yes
+
+. . .
+
+# This will complete and list ports to open and such. We did this earlier.
+
+

Now that the AD trust components are prepped, depending on the setup, we'll need to do some DNS zone forwards. It is likely you have IPA and AD running their own DNS. Note: This may not be the case in a real world scenario.

+
# We need to create a forward zone here for the example.net zone
+% ipa dnsforwardzone-add example.net --forwarder=192.168.15.15 --forward-policy=only
+Server will check DNS forwarder(s).
+This may take some time, please wait ...
+  Zone name: example.net.
+  Active zone: TRUE
+  Zone forwarders: 192.168.15.15
+  Forward policy: only
+
+# We should probably create a few dns records...
+# Assuming the AD netbios name is EXAMPLEAD, use the syntax hostname.NETBIOS here
+% ipa dnsrecord-add example.com ad.EXAMPLEAD --a-ip-address=192.168.15.15
+# Same idea here, but we're only doing the netbios name and saying the name server record is the AD server
+% ipa dnsrecord-add example.com EXAMPLEAD --ns-hostname=ad.EXAMPLEAD
+
+# We need to allow the zones to be transferable to the AD domain
+% ipa dnszone-mod example.com --allow-transfer=192.168.15.15
+
+

On the AD side, we need to create the IPA zone. It's absolutely required.

+

C:\Windows\System32>dnscmd 127.0.0.1 /ZoneAdd example.com /Secondary 192.168.15.2
+
+You should probably double check that the DNS records are returning on the IDM servers.

+
% dig _ldap._tcp.example.com SRV
+; <<>> DiG 9.9.4-RedHat-9.9.4-61.el7 <<>> SRV _ldap._tcp.example.com
+;; global options: +cmd
+;; Got answer:
+;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 14793
+;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 2
+
+;; OPT PSEUDOSECTION:
+; EDNS: version: 0, flags:; udp: 4096
+;; QUESTION SECTION:
+;_ldap._tcp.example.com.      IN      SRV
+
+;; ANSWER SECTION:
+_ldap._tcp.example.com. 86400 IN      SRV     0 100 389 idm1.example.com.
+_ldap._tcp.example.com. 86400 IN      SRV     0 100 389 idm2.example.com.
+
+;; AUTHORITY SECTION:
+example.com.          86400   IN      NS      idm1.example.com.
+example.com.          86400   IN      NS      idm2.example.com.
+
+;; ADDITIONAL SECTION:
+idm1.example.com.      1200    IN      A       192.168.15.2
+idm2.example.com.      1200    IN      A       192.168.15.3
+
+# Same with the AD records
+% dig _ldap._tcp.example.net SRV
+; <<>> DiG 9.9.4-RedHat-9.9.4-61.el7 <<>> SRV _ldap._tcp.example.net
+;; global options: +cmd
+;; Got answer:
+;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12195
+;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 4, ADDITIONAL: 9
+
+;; OPT PSEUDOSECTION:
+; EDNS: version: 0, flags:; udp: 4096
+;; QUESTION SECTION:
+;_ldap._tcp.example.net.       IN      SRV
+
+;; ANSWER SECTION:
+_ldap._tcp.example.net. 600    IN      SRV     0 100 389 ad.example.net.
+
+. . .
+
+

Now that they are returning, intiate the trust.

+
% ipa trust-add --type=ad example.net --admin Administrator --password
+Active Directiron domain administrator's password: (type password here)
+-----------------------------------------------------
+Added Active Directory trust for realm "example.net"
+-----------------------------------------------------
+  Realm name: example.net
+  Domain NetBIOS name: EXAMPLEAD
+  Domain Security Identifier: S-1-5-21-XXXXXXXXXX-YYYYYYYYY-ZZZZZZZZZZ
+  Trust direction: Trusting forest
+  Trust type: Active Directory domain
+  Trust status: Established and verified
+
+# Check that an AD user is resolvable. You can do this with DOMAIN\name or name@DOMAIN
+% id EXAMPLEAD\\administrator
+% id administrator@example.net
+
+

Authenticate users with an Active Directory domain

+

As we disabled the allow_all rule, let's create a set of groups first and then the HBAC rule.

+
# Create the starting AD group
+% ipa group-add adusers
+# Create an external group. This is required for AD users.
+% ipa group-add --external adgroup_external
+# Add an AD user into the external group
+% ipa group-add-member --users=administrator@example.net adgroup_external
+# Make the external group a member of ad users
+% ipa group-add-member --groups=adgroup_external adusers
+
+

As we've made an HBAC rule before, this should be simple.

+
% ipa hbacrule-add --hostcat=all --servicecat=all --desc='ad users all access' adusers_access
+% ipa hbacrule-add-user --groups=adusers adusers_access
+% ipa hbactest --rules=adusers_access --user=administrator@example.net --host=client.example.com --service=sshd
+
+

The test should pass without any issues.

+
+

Note

+

Group Types

+

While this may not be required information while working on the exam, it's important to understand that there are different group types in AD and the behavior changes based on the group type.

+

Groups in Active Directory have three types. These three types can actually change the behavior of how SSSD on the IPA domain controllers resolve them or if they'll even be resolvable at all. The three types are 'Domain Local', 'Global', and 'Universal'. If at all possible, avoid groups being 'Global'. Domain Local or Universal is recommended.

+
+

IdM maintenance

+

Back up an IdM infrastructure

+

There are multiple ways you can backup IPA.

+
    +
  • Full backup: Default, shuts down IPA before performing a backup. This backs up with raw files. As such, it must be done offline.
  • +
  • Data backup: Backs up a copy of the ldap data and the changelog (the IPA-REALM instance, DogTag, IPA backend). This can be done online.
  • +
+
# Turns off IPA completely and perform a backup
+% ipa-backup
+# Backs up and gpg encrypts
+% ipa-backup --gpg --gpg-keyring=/root/keys
+
+

To restore a backup, the ipa-restore command is available.

+
% ipa-restore /var/lib/ipa/backup/
+
+

Perform a backup without interruption of services

+

The backup command allows you to pass an online flag to ensure a backup taken doesn't down the IPA services. Note that not everything can be backed up online.

+
# Backs up data only and doesn't take down IPA
+% ipa-backup --data --online
+# Backs up data only and gpg encrypts
+% ipa-backup --gpg --gpg-keyring=/root/keys --data --online
+
+

Value Add

+

Use the REST api to query IdM

+

When you invoke the ipa command, you are actually communicating with the API that runs on the IdM replicas. Operations done are sent via a POST with JSON data. The return data is also in JSON and translated to be readable in the terminal. Because it's JSON, custom scripts can be made with say perl or python that communicates with the API to send the calls, perhaps for specific tasks, jobs, or other operations that could be automated. You can also use curl to do this also if you so choose.

+

The question becomes, "well, how do I know the right data to send?" You can issue the -vv switch to see the request being sent.

+
% ipa -vv ping
+ipa: INFO: trying https://idm1.example.com/ipa/json
+ipa: INFO: [try 1]: Forwarding 'schema' to json server 'https://idm1.example.com/ipa/json'
+ipa: INFO: trying https://idm1.example.com/ipa/session/json
+ipa: INFO: [try 1]: Forwarding 'ping/1' to json server 'https://idm1.example.com/ipa/session/json'
+ipa: INFO: Request: {
+    "id": 0, 
+    "method": "ping/1", 
+    "params": [
+        [], 
+        {
+            "version": "2.251"
+        }
+    ]
+}
+ipa: INFO: Response: {
+    "error": null, 
+    "id": 0, 
+    "principal": "admin@EXAMPLE.COM", 
+    "result": {
+        "summary": "IPA server version 4.10.2. API version 2.251"
+    }, 
+    "version": "4.10.2"
+}
+--------------------------------------------
+IPA server version 4.10.2. API version 2.251
+--------------------------------------------
+
+

If you look at the 'request' section, you can see the data that is sent. Each request has a method and params, where method is a command to be excuted and params is simply an array that contains positional arguments and a dictionary of options. If you take a look at say, group-show, you would see a different request.

+
% ipa -vv group-show admins
+ipa: INFO: trying https://idm1.example.com/ipa/session/json
+ipa: INFO: [try 1]: Forwarding 'group_show/1' to json server 'https://idm1.example.com/ipa/session/json'
+ipa: INFO: Request: {
+    "id": 0, 
+    "method": "group_show/1", 
+    "params": [
+        [
+            "admins"
+        ], 
+        {
+            "version": "2.230"
+        }
+    ]
+}
+### Lots of output ###
+
+

Let's say I wanted to perform that in a simple bash script that uses curl. I would perform a kinit and then run the script below to have it login for me via kerberos and do the work.

+
#!/bin/bash
+ipaReplica=idm1.example.com
+cookieJar=my.cookie.jar
+
+# Login with Kerberos
+curl -v \
+  -H referer:https://$ipaReplica/ipa \
+  -c $cookieJar -b $cookieJar \
+  --cacert /etc/ipa/ca.crt \
+  --negotiate -u : \
+  -X POST \
+  https://$ipaReplica/ipa/session/login_kerberos
+
+# Send user_find method request
+curl -v \
+  -H referer:https://$ipaReplica/ipa \
+  -H "Content-Type:application/json" \
+  -H "Accept:applicaton/json"\
+  -c $cookieJar -b $cookieJar \
+  --cacert /etc/ipa/ca.crt \
+  -d  '{"method":"group_show/1","params":[["admins"],{}],"id":0}' \
+  -X POST \
+  https://$ipaReplica/ipa/session/json
+
+

Any of the commands ran via ipa can be reviewed with the -vv switch so you can see what kind of call it's making and how it's making it. Thus, making it easier to tie into your own scripts. On the Web UI, you can go to IPA Server -> API Browser to find more information on the specific API calls.

+

Implement an IdP

+

While not strictly a certification objective at this present time, there may be a chance it could be. FreeIPA 4.10+ have the ability to implement authentication to external identity providers. FreeIPA allows you to use RADIUS proxy authentication for example, but using this same method, an OAuth 2.0 authorization server could be used as well. When it comes to an IdP, you can instead configure IdP clients using ipa idp-add and use software like Keycloak or otherwise that supports OAuth 2.0 workflows. We recommend checking out the FreeIPA Workshop Unit 12 for more details.

+
+
+ +
+
+

Copyright 2024, remyabel, nazunalika

+

Documentation built with MkDocs.

+
+ + + + + + + + + +